Skip to content

Commit

Permalink
feat: Post login Idp
Browse files Browse the repository at this point in the history
Signed-off-by: Hoang Pham <[email protected]>
  • Loading branch information
hweihwang committed Aug 26, 2024
1 parent 20589d3 commit 93b9a22
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 30 deletions.
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.'));
?>

<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

0 comments on commit 93b9a22

Please sign in to comment.