Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: IdP Post #861

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,10 +355,12 @@ $(function() {
} else {
$(this).val("0");
}

if(key === 'require_provisioned_account') {
$('#user-saml-attribute-mapping').toggleClass('hidden');
$('#user-saml-filtering').toggleClass('hidden');
}

OCA.User_SAML.Admin.setSamlConfigValue('general', key, $(this).val(), true);
});
});
Expand Down
92 changes: 65 additions & 27 deletions lib/Controller/SAMLController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace OCA\User_SAML\Controller;

use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use OC\Core\Controller\ClientFlowLoginController;
Expand Down Expand Up @@ -33,6 +34,8 @@
use OneLogin\Saml2\Error;
use OneLogin\Saml2\Settings;
use OneLogin\Saml2\ValidationError;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;

class SAMLController extends Controller {
Expand Down Expand Up @@ -65,19 +68,19 @@ class SAMLController extends Controller {
private ITrustedDomainHelper $trustedDomainHelper;

public function __construct(
string $appName,
IRequest $request,
ISession $session,
IUserSession $userSession,
SAMLSettings $samlSettings,
UserBackend $userBackend,
IConfig $config,
IURLGenerator $urlGenerator,
LoggerInterface $logger,
IL10N $l,
UserResolver $userResolver,
UserData $userData,
ICrypto $crypto,
string $appName,
IRequest $request,
ISession $session,
IUserSession $userSession,
SAMLSettings $samlSettings,
UserBackend $userBackend,
IConfig $config,
IURLGenerator $urlGenerator,
LoggerInterface $logger,
IL10N $l,
UserResolver $userResolver,
UserData $userData,
ICrypto $crypto,
ITrustedDomainHelper $trustedDomainHelper
) {
parent::__construct($appName, $request);
Expand Down Expand Up @@ -177,9 +180,9 @@ protected function assertGroupMemberships(): void {
* @OnlyUnauthenticatedUsers
* @NoCSRFRequired
*
* @throws \Exception
* @throws Exception
*/
public function login(int $idp = 1): Http\RedirectResponse {
public function login(int $idp = 1): Http\RedirectResponse|Http\TemplateResponse {
$originalUrl = (string)$this->request->getParam('originalUrl', '');
if (!$this->trustedDomainHelper->isTrustedUrl($originalUrl)) {
$originalUrl = '';
Expand All @@ -192,7 +195,34 @@ public function login(int $idp = 1): Http\RedirectResponse {

$returnUrl = $originalUrl ?: $this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.login');
$ssoUrl = $auth->login($returnUrl, [], false, false, true);
$response = new Http\RedirectResponse($ssoUrl);

$settings = $this->samlSettings->get($idp);
$isSAMLRequestUsingPost = isset($settings['general-is_saml_request_using_post']) && $settings['general-is_saml_request_using_post'] === '1';
if ($isSAMLRequestUsingPost) {
$query = parse_url($ssoUrl, PHP_URL_QUERY);
parse_str($query, $params);

$samlRequest = $params['SAMLRequest'];
$relayState = $params['RelayState'] ?? '';
$sigAlg = $params['SigAlg'] ?? '';
$signature = $params['Signature'] ?? '';
$ssoUrl = explode('?', $ssoUrl)[0];

$nonce = base64_encode(random_bytes(16));

$response = new Http\TemplateResponse($this->appName, 'login_post', [
'ssoUrl' => $ssoUrl,
'samlRequest' => $samlRequest,
'relayState' => $relayState,
'sigAlg' => $sigAlg,
'signature' => $signature,
'nonce' => $nonce,
], 'guest');

$response->addHeader('Content-Security-Policy', "script-src 'self' 'nonce-$nonce' 'strict-dynamic' 'unsafe-eval';");
} else {
$response = new Http\RedirectResponse($ssoUrl);
}

// Small hack to make user_saml work with the loginflows
$flowData = [];
Expand Down Expand Up @@ -257,7 +287,7 @@ public function login(int $idp = 1): Http\RedirectResponse {
}
break;
default:
throw new \Exception(
throw new Exception(
sprintf(
'Type of "%s" is not supported for user_saml',
$type
Expand All @@ -283,7 +313,7 @@ public function getMetadata(int $idp = 1): Http\DataDownloadResponse {
return new Http\DataDownloadResponse($metadata, 'metadata.xml', 'text/xml');
} else {
throw new Error(
'Invalid SP metadata: '.implode(', ', $errors),
'Invalid SP metadata: ' . implode(', ', $errors),
Error::METADATA_SP_INVALID
);
}
Expand Down Expand Up @@ -314,7 +344,7 @@ public function assertionConsumerService(): Http\RedirectResponse {
// Decrypt and deserialize
try {
$cookie = $this->crypto->decrypt($cookie);
} catch (\Exception) {
} catch (Exception) {
$this->logger->debug('Could not decrypt SAML cookie', ['app' => 'user_saml']);
return new Http\RedirectResponse($this->urlGenerator->getAbsoluteURL('/'));
}
Expand Down Expand Up @@ -394,7 +424,7 @@ public function assertionConsumerService(): Http\RedirectResponse {
}
} catch (NoUserFoundException) {
throw new \InvalidArgumentException('User "' . $this->userBackend->getCurrentUserId() . '" is not valid');
} catch (\Exception $e) {
} catch (Exception $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => $this->appName]);
$response = new Http\RedirectResponse($this->urlGenerator->linkToRouteAbsolute('user_saml.SAML.notProvisioned'));
$response->invalidateCookie('saml_data');
Expand Down Expand Up @@ -426,7 +456,7 @@ public function assertionConsumerService(): Http\RedirectResponse {
*/
public function singleLogoutService(): Http\RedirectResponse {
$isFromGS = ($this->config->getSystemValue('gs.enabled', false) &&
$this->config->getSystemValue('gss.mode', '') === 'master');
$this->config->getSystemValue('gss.mode', '') === 'master');

// Some IDPs send the SLO request via POST, but OneLogin php-saml only handles GET.
// To hack around this issue we copy the request from _POST to _GET.
Expand All @@ -439,7 +469,7 @@ public function singleLogoutService(): Http\RedirectResponse {
if ($isFromIDP) {
// requests comes from the IDP so let it manage the logout
// (or raise Error if request is invalid)
$pass = true ;
$pass = true;
} elseif ($isFromGS) {
// Request is from master GlobalScale
$jwt = $this->request->getParam('jwt', '');
Expand All @@ -450,7 +480,7 @@ public function singleLogoutService(): Http\RedirectResponse {

$idp = $decoded['idp'] ?? null;
$pass = true;
} catch (\Exception) {
} catch (Exception) {
}
} else {
// standard request : need read CRSF check
Expand Down Expand Up @@ -603,6 +633,11 @@ private function getIdps(string $redirectUrl): array {
return $result;
}

/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws \OCP\DB\Exception
*/
private function getSSOUrl(string $redirectUrl, string $idp): string {
$originalUrl = '';
if (!empty($redirectUrl)) {
Expand All @@ -612,16 +647,19 @@ private function getSSOUrl(string $redirectUrl, string $idp): string {
/** @var CsrfTokenManager $csrfTokenManager */
$csrfTokenManager = Server::get(CsrfTokenManager::class);
$csrfToken = $csrfTokenManager->getToken();
$ssoUrl = $this->urlGenerator->linkToRouteAbsolute(

$settings = $this->samlSettings->get((int)$idp);
$method = $settings['general-is_saml_request_using_post'] ?? 'get';

return $this->urlGenerator->linkToRouteAbsolute(
'user_saml.SAML.login',
[
'requesttoken' => $csrfToken->getEncryptedValue(),
'originalUrl' => $originalUrl,
'idp' => $idp
'idp' => $idp,
'method' => $method,
]
);

return $ssoUrl;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions lib/SAMLSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class SAMLSettings {
public const IDP_CONFIG_KEYS = [
'general-idp0_display_name',
'general-uid_mapping',
'general-is_saml_request_using_post',
'general-saml_request_method',
'idp-entityId',
'idp-singleLogoutService.responseUrl',
'idp-singleLogoutService.url',
Expand Down
7 changes: 6 additions & 1 deletion lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public function getForm() {
'text' => $this->l10n->t('Only allow authentication if an account exists on some other backend (e.g. LDAP).'),
'type' => 'checkbox',
'global' => true,
]
],
];
$attributeMappingSettings = [
'displayName_mapping' => [
Expand Down Expand Up @@ -199,6 +199,11 @@ public function getForm() {
'type' => 'line',
'required' => false,
];
$generalSettings['is_saml_request_using_post'] = [
'text' => $this->l10n->t('Use POST method for SAML request (default: GET)'),
'type' => 'checkbox',
'required' => false,
];
$generalSettings['allow_multiple_user_back_ends'] = [
'text' => $this->l10n->t('Allow the use of multiple user back-ends (e.g. LDAP)'),
'type' => 'checkbox',
Expand Down
34 changes: 34 additions & 0 deletions templates/login_post.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* @var array $_
* @var IL10N $l
*
*/

use OCP\IL10N;

p($l->t('Please wait while you are redirected to the SSO server.'));
blizzz marked this conversation as resolved.
Show resolved Hide resolved
?>

<form action="<?= $_['ssoUrl'] ?>" method="post">
<input type="hidden" name="SAMLRequest" value="<?= $_['samlRequest'] ?>" />
<input type="hidden" name="RelayState" value="<?= $_['relayState'] ?>" />
<input type="hidden" name="SigAlg" value="<?= $_['sigAlg'] ?>" />
<input type="hidden" name="Signature" value="<?= $_['signature'] ?>" />
<noscript>
<p>
<?php p($l->t('JavaScript is disabled in your browser. Please enable it to continue.')) ?>
</p>
<input type="submit" value="Continue" />
</noscript>
</form>
<script nonce="<?= $_['nonce'] ?>">
document.addEventListener('DOMContentLoaded', function() {
document.forms[0].submit()
})
</script>
18 changes: 18 additions & 0 deletions tests/integration/features/Shibboleth.feature
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
Feature: Shibboleth

Scenario: Authenticating using Shibboleth with SAML and POST binding and no check if user exists on backend
Given The setting "type" is set to "saml"
And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1"
And The setting "general-is_saml_request_using_post" is set to "1"
And The setting "idp-entityId" is set to "https://shibboleth-integration-nextcloud.localdomain/idp/shibboleth"
And The setting "idp-singleSignOnService.url" is set to "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And The setting "idp-x509cert" is set to "MIIDnTCCAoWgAwIBAgIUGPx9uPjCu7c172IUgV84Tm94pBcwDQYJKoZIhvcNAQEL BQAwNzE1MDMGA1UEAwwsc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQu bG9jYWxkb21haW4wHhcNMTcwMTA0MTAxMTI3WhcNMzcwMTA0MTAxMTI3WjA3MTUw MwYDVQQDDCxzaGliYm9sZXRoLWludGVncmF0aW9uLW5leHRjbG91ZC5sb2NhbGRv bWFpbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKH+bPu45tk8/JRk XOxkyfbxocWZlY4mRumEUxscd3fn0oVzOrdWbHH7lCZV4bus4KxvJljc0Nm2K+Zr LoiRUUnf/LQ4LlehWVm5Kbc4kRgOXS0iGZN3SslAWPKyIg0tywg+TLOBPoS6EtST 1WuYg1JPMFxPfeFDWQ0dQYPlXIJWBFh6F2JMTb0FLECqA5l/ryYE13QisX5l+Mqo 6y3Dh7qIgaH0IJNobXoAcEWza7Kb2RnfhZRh9e0qjZIwBqTJUFM/6I86RYXn829s INUvYQQbez6VkGTdUQJ/GuXb/dD5sMQfOyK8hrRY5MozOmK32cz3JaAzSXpiSRS9 NxFwvicCAwEAAaOBoDCBnTAdBgNVHQ4EFgQUKn8+TV0WXSDeavvF0M8mWn1o8ukw fAYDVR0RBHUwc4Isc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xvdWQubG9j YWxkb21haW6GQ2h0dHBzOi8vc2hpYmJvbGV0aC1pbnRlZ3JhdGlvbi1uZXh0Y2xv dWQubG9jYWxkb21haW4vaWRwL3NoaWJib2xldGgwDQYJKoZIhvcNAQELBQADggEB ABI6uzoIeLZT9Az2KTlLxIc6jZ4MDmhaVja4ZuBxTXEb7BFLfeASEJmQm1wgIMOn pJId3Kh3njW+3tOBWKm7lj8JxVVpAu4yMFSoQGPaVUgYB1AVm+pmAyPLzfJ/XGhf esCU2F/b0eHWcaIb3x+BZFX089Cd/PBtP84MNXdo+TccibxC8N39sr45qJM/7SC7 TfDYU0L4q2WZHJr4S7+0F+F4KaxLx9NzCvN4h6XaoWofZWir2iHO4NzbrVQGC0ei QybS/neBfni4A2g1lyzCb6xFB58JBvNCn7AAnDJULOE7S5XWUKsDAQVQrxTNkUq7 pnhlCQqZDwUdgmIXd1KB1So="
And The setting "security-authnRequestsSigned" is set to "1"
And The setting "security-wantAssertionsEncrypted" is set to "1"
And The setting "sp-x509cert" is set to "-----BEGIN CERTIFICATE-----MIIC+zCCAeOgAwIBAgIJAIgZuvWDBIrdMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzAxMDQxMTM5MjFaFw0yNzAxMDIxMTM5MjFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3ESWaDH1JiJTy9yRJQV7kahPOxgBkIH2xwcYDL1k9deKNhSKLx7aGfxE244+HBcC6WLHKVUnOm0ld2qxQ4bMYiJXzZuqL67r07L5wxGAssv12lO92qohGmlHy3+VzRYUBmovu6upqOv3R2F8HBbo7Jc7Hvt7hOEJn/jPuFuF/fHit3mqU8l6IkrIZjpaW8T9fIWOXRq98U4+hkgWpqEZWsqlfE8BxAs9DeIMZab0GxO9stHLp+GYKx10uE4ezFcaDS8W+g2C8enCTt1HXGvcnj4o5zkC1lITGvcFTsiFqfIWyXeSufcxdc0W7HoG6J3ks0WJyK38sfFn0t2Ao6kX0CAwEAAaNQME4wHQYDVR0OBBYEFAoJzX6TVYAwC1GSPe6nObBG54zaMB8GA1UdIwQYMBaAFAoJzX6TVYAwC1GSPe6nObBG54zaMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJia9R70uXdUZtgujUPjLas4+sVajzlBFmqhBqpLAo934vljf9HISsHrPtdBcbS0d0rucqXwabDf0MlR18ksnT/NYpsTwMbMx76CrXi4zYEEW5lISKEO65aIkzVTcqKWSuhjtSnRdB6iOLsFiKmNMWXaIKMR5T0+AbR9wdQgn08W+3EEeHGvafVQfE3STVsSgNb1ft7DvcSUnfPXGU7KzvmTpZa0Hfmc7uY4vpdEEhLAdRhgLReS7USZskov7ooiPSoD+JRFi2gM4klBxTemHdNUa9oFnHMXuYKOkLbkgFvHxyy+QlLq2ELQTga5e7I83ZyOfGctyf8Ul6vGw10vbQ=-----END CERTIFICATE-----"
And The setting "sp-privateKey" is set to "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEA3cRJZoMfUmIlPL3JElBXuRqE87GAGQgfbHBxgMvWT114o2FIovHtoZ/ETbjj4cFwLpYscpVSc6bSV3arFDhsxiIlfNm6ovruvTsvnDEYCyy/XaU73aqiEaaUfLf5XNFhQGai+7q6mo6/dHYXwcFujslzse+3uE4Qmf+M+4W4X98eK3eapTyXoiSshmOlpbxP18hY5dGr3xTj6GSBamoRlayqV8TwHECz0N4gxlpvQbE72y0cun4ZgrHXS4Th7MVxoNLxb6DYLx6cJO3Udca9yePijnOQLWUhMa9wVOyIWp8hbJd5K59zF1zRbsegboneSzRYnIrfyx8WfS3YCjqRfQIDAQABAoIBAQC5CQAdcqZ9vLpJNilBCJxJLCFmm+HAAREHD8MErg9A5UK1P4S1wJp/0qieGPi68wXBOTgY2xKSwMycgb04/+NyZidVRu388takOW/+KNBg8pMxdZ6/05GqnI0kivSbR3CXpYuz8hekwhpo9+fWmKjApsHL47ItK6WaeKmPbAFsq1YJGzfp/DXg7LIvh9GA3C1LWWGV7SuCGOyX/2Moi8xRa7qBtH4hDo/0NRhTx7zjYjlBgNEr330pJUopc3+AtHE40R+xMr2zkGvq9RsCZxYxD2VWbLwQW0yNjWmQ2OTuMgJJvk2+N73QLHcB+tea82ZTszsNzRS9DLtc6qbsKEPZAoGBAO78U3vEuRyY56f/1hpo0xuCDwOkWGzgBQWkjJl6dlyVz/zKkhXBHpEYImyt8XRN0W3iGZYpZ2hCFJGTcDp32R6UiEyGLz0Uc8R/tva/TiRVW1FdNczzSHcB24b9OMK4vE9JLs8mA8Rp8YBgtLr5DDuMfYt/a/rZJbg/HIfIN98nAoGBAO2OInCX93t2I6zzRPIqKtI6q6FYNp64VIQjvw9Y8l0x3IdJZRP9H5C8ZhCeYPsgEqTXcXa4j5hL4rQzoUtxfxflBUUH60bcnd4LGaTCMYLS14G011E3GZlIP0sJi5OjEhy8fq3zt6jVzS9V/lPHB8i+w1D7CbPrMpW7B3k32vC7AoGAX/HvdkYhZyjAAEOG6m1hK68IZhbp5TP+8CgCxm9S65K9wKh3A8LXibrdvzIKOP4w8WOPkCipOkMlTNibeu24vj01hztr5aK7Y40+oEtnjNCz67N3MQQO+LBHOSeaTRqrh01DPKjvZECAU2D/zfzEe3fIw2Nxr3DUYub7hkvMmosCgYAzxbVVypjiLGYsDDyrdmsstCKxoDMPNmcdAVljc+QmUXaZeXJw/8qAVb78wjeqo1vM1zNgR2rsKyW2VkZB1fN39q7GU6qAIBa7zLmDAduegmr7VrlSduq6UFeS9/qWa4TIBICrUqFlR2tXdKtgANF+e6y/mmaL8qdsoH1JetXZfwKBgQC1vscRpdAXivjOOZAh+mzJWzS4BUl4CTJLYYIuOEXikmN5g0EdV2fhUEdkewmyKnXHsd0x83167bYgpTDNs71jUxDHy5NXlg2qIjLkf09X9wr19gBzDApfWzfh3vUqttyMZuQMLVNepGCWM2vjlY9KGl5OvZqY6d+7yO0mLV9GmQ==-----END RSA PRIVATE KEY-----"
And The setting "security-wantAssertionsSigned" is set to "1"
When I send a GET request to "http://localhost:8080/index.php/login"
Then I should be redirected to "http://localhost:8080/index.php/apps/user_saml/saml/login"
And The response should contain the form with action "https://localhost:4443/idp/profile/SAML2/Redirect/SSO"
And The form method should be POST
And The form should contain input fields "SAMLRequest,RelayState,SigAlg,Signature"

Scenario: Authenticating using Shibboleth with SAML and no check if user exists on backend
Given The setting "type" is set to "saml"
And The setting "general-uid_mapping" is set to "urn:oid:0.9.2342.19200300.100.1.1"
Expand Down
Loading
Loading