From 3c41b15be65ca59a89f1588522d2d58f775e71cc Mon Sep 17 00:00:00 2001 From: Daniel Seiler Date: Mon, 5 Aug 2019 20:06:39 +0200 Subject: [PATCH 1/4] Initial work on SAML integration --- app/Config/app.php | 1 + app/Config/saml2_settings.php | 230 ++++++++++++++++++ app/Http/Controllers/Auth/LoginController.php | 7 +- composer.json | 3 +- composer.lock | 152 +++++++++++- resources/views/auth/login.blade.php | 12 +- 6 files changed, 400 insertions(+), 5 deletions(-) create mode 100644 app/Config/saml2_settings.php diff --git a/app/Config/app.php b/app/Config/app.php index 88052e94c..23025a6c4 100755 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -106,6 +106,7 @@ return [ Intervention\Image\ImageServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\Snappy\ServiceProvider::class, + Aacotroneo\Saml2\Saml2ServiceProvider::class, // BookStack replacement service providers (Extends Laravel) diff --git a/app/Config/saml2_settings.php b/app/Config/saml2_settings.php new file mode 100644 index 000000000..a6d7a0204 --- /dev/null +++ b/app/Config/saml2_settings.php @@ -0,0 +1,230 @@ + env("SAML2_ENABLED", false), + + /** + * If 'useRoutes' is set to true, the package defines five new routes: + * + * Method | URI | Name + * -------|--------------------------|------------------ + * POST | {routesPrefix}/acs | saml_acs + * GET | {routesPrefix}/login | saml_login + * GET | {routesPrefix}/logout | saml_logout + * GET | {routesPrefix}/metadata | saml_metadata + * GET | {routesPrefix}/sls | saml_sls + */ + 'useRoutes' => true, + + 'routesPrefix' => '/saml2', + + /** + * which middleware group to use for the saml routes + * Laravel 5.2 will need a group which includes StartSession + */ + 'routesMiddleware' => [], + + /** + * Indicates how the parameters will be + * retrieved from the sls request for signature validation + */ + 'retrieveParametersFromServer' => false, + + /** + * Where to redirect after logout + */ + 'logoutRoute' => '/', + + /** + * Where to redirect after login if no other option was provided + */ + 'loginRoute' => '/', + + + /** + * Where to redirect after login if no other option was provided + */ + 'errorRoute' => '/', + + + + + /***** + * One Login Settings + */ + + + + // If 'strict' is True, then the PHP Toolkit will reject unsigned + // or unencrypted messages if it expects them signed or encrypted + // Also will reject the messages if not strictly follow the SAML + // standard: Destination, NameId, Conditions ... are validated too. + 'strict' => true, //@todo: make this depend on laravel config + + // Enable debug mode (to print errors) + 'debug' => env('APP_DEBUG', false), + + // If 'proxyVars' is True, then the Saml lib will trust proxy headers + // e.g X-Forwarded-Proto / HTTP_X_FORWARDED_PROTO. This is useful if + // your application is running behind a load balancer which terminates + // SSL. + 'proxyVars' => false, + + // Service Provider Data that we are deploying + 'sp' => array( + + // Specifies constraints on the name identifier to be used to + // represent the requested subject. + // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + + // Usually x509cert and privateKey of the SP are provided by files placed at + // the certs folder. But we can also provide them with the following parameters + 'x509cert' => env('SAML2_SP_x509',''), + 'privateKey' => env('SAML2_SP_PRIVATEKEY',''), + + // Identifier (URI) of the SP entity. + // Leave blank to use the 'saml_metadata' route. + 'entityId' => env('SAML2_SP_ENTITYID',''), + + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'assertionConsumerService' => array( + // URL Location where the from the IdP will be returned, + // using HTTP-POST binding. + // Leave blank to use the 'saml_acs' route + 'url' => '', + ), + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + // Remove this part to not include any URL Location in the metadata. + 'singleLogoutService' => array( + // URL Location where the from the IdP will be returned, + // using HTTP-Redirect binding. + // Leave blank to use the 'saml_sls' route + 'url' => '', + ), + ), + + // Identity Provider Data that we want connect with our SP + 'idp' => array( + // Identifier of the IdP entity (must be a URI) + 'entityId' => env('SAML2_IDP_ENTITYID', $idp_host . '/saml2/idp/metadata.php'), + // SSO endpoint info of the IdP. (Authentication Request protocol) + 'singleSignOnService' => array( + // URL Target of the IdP where the SP will send the Authentication Request Message, + // using HTTP-Redirect binding. + 'url' => env('SAML2_IDP_SSO', $idp_host . '/saml2/idp/SSOService.php'), + ), + // SLO endpoint info of the IdP. + 'singleLogoutService' => array( + // URL Location of the IdP where the SP will send the SLO Request, + // using HTTP-Redirect binding. + 'url' => env('SAML2_IDP_SLO', $idp_host . '/saml2/idp/SingleLogoutService.php'), + ), + // Public x509 certificate of the IdP + 'x509cert' => env('SAML2_IDP_x509', 'MIID/TCCAuWgAwIBAgIJAI4R3WyjjmB1MA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJBUjEVMBMGA1UECAwMQnVlbm9zIEFpcmVzMRUwEwYDVQQHDAxCdWVub3MgQWlyZXMxDDAKBgNVBAoMA1NJVTERMA8GA1UECwwIU2lzdGVtYXMxFDASBgNVBAMMC09yZy5TaXUuQ29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbmlAc2l1LmVkdS5hcjAeFw0xNDEyMDExNDM2MjVaFw0yNDExMzAxNDM2MjVaMIGUMQswCQYDVQQGEwJBUjEVMBMGA1UECAwMQnVlbm9zIEFpcmVzMRUwEwYDVQQHDAxCdWVub3MgQWlyZXMxDDAKBgNVBAoMA1NJVTERMA8GA1UECwwIU2lzdGVtYXMxFDASBgNVBAMMC09yZy5TaXUuQ29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbmlAc2l1LmVkdS5hcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbzW/EpEv+qqZzfT1Buwjg9nnNNVrxkCfuR9fQiQw2tSouS5X37W5h7RmchRt54wsm046PDKtbSz1NpZT2GkmHN37yALW2lY7MyVUC7itv9vDAUsFr0EfKIdCKgxCKjrzkZ5ImbNvjxf7eA77PPGJnQ/UwXY7W+cvLkirp0K5uWpDk+nac5W0JXOCFR1BpPUJRbz2jFIEHyChRt7nsJZH6ejzNqK9lABEC76htNy1Ll/D3tUoPaqo8VlKW3N3MZE0DB9O7g65DmZIIlFqkaMH3ALd8adodJtOvqfDU/A6SxuwMfwDYPjoucykGDu1etRZ7dF2gd+W+1Pn7yizPT1q8CAwEAAaNQME4wHQYDVR0OBBYEFPsn8tUHN8XXf23ig5Qro3beP8BuMB8GA1UdIwQYMBaAFPsn8tUHN8XXf23ig5Qro3beP8BuMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAGu60odWFiK+DkQekozGnlpNBQz5lQ/bwmOWdktnQj6HYXu43e7sh9oZWArLYHEOyMUekKQAxOK51vbTHzzw66BZU91/nqvaOBfkJyZKGfluHbD0/hfOl/D5kONqI9kyTu4wkLQcYGyuIi75CJs15uA03FSuULQdY/Liv+czS/XYDyvtSLnu43VuAQWN321PQNhuGueIaLJANb2C5qq5ilTBUw6PxY9Z+vtMjAjTJGKEkE/tQs7CvzLPKXX3KTD9lIILmX5yUC3dLgjVKi1KGDqNApYGOMtjr5eoxPQrqDBmyx3flcy0dQTdLXud3UjWVW3N0PYgJtw5yBsS74QTGD4='), + /* + * Instead of use the whole x509cert you can use a fingerprint + * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it) + */ + // 'certFingerprint' => '', + ), + + + + /*** + * + * OneLogin advanced settings + * + * + */ + // Security settings + 'security' => array( + + /** signatures and encryptions offered */ + + // Indicates that the nameID of the sent by this SP + // will be encrypted. + 'nameIdEncrypted' => false, + + // Indicates whether the messages sent by this SP + // will be signed. [The Metadata of the SP will offer this info] + 'authnRequestsSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutRequestSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutResponseSigned' => false, + + /* Sign the Metadata + False || True (use sp certs) || array ( + keyFileName => 'metadata.key', + certFileName => 'metadata.crt' + ) + */ + 'signMetadata' => false, + + + /** signatures and encryptions required **/ + + // Indicates a requirement for the , and + // elements received by this SP to be signed. + 'wantMessagesSigned' => false, + + // Indicates a requirement for the elements received by + // this SP to be signed. [The Metadata of the SP will offer this info] + 'wantAssertionsSigned' => false, + + // Indicates a requirement for the NameID received by + // this SP to be encrypted. + 'wantNameIdEncrypted' => false, + + // Authentication context. + // Set to false and no AuthContext will be sent in the AuthNRequest, + // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + // Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), + 'requestedAuthnContext' => true, + ), + + // Contact information template, it is recommended to suply a technical and support contacts + 'contactPerson' => array( + 'technical' => array( + 'givenName' => 'name', + 'emailAddress' => 'no@reply.com' + ), + 'support' => array( + 'givenName' => 'Support', + 'emailAddress' => 'no@reply.com' + ), + ), + + // Organization information template, the info in en_US lang is recomended, add more if required + 'organization' => array( + 'en-US' => array( + 'name' => 'Name', + 'displayname' => 'Display Name', + 'url' => 'http://url' + ), + ), + +/* Interoperable SAML 2.0 Web Browser SSO Profile [saml2int] http://saml2int.org/profile/current + + 'authnRequestsSigned' => false, // SP SHOULD NOT sign the , + // MUST NOT assume that the IdP validates the sign + 'wantAssertionsSigned' => true, + 'wantAssertionsEncrypted' => true, // MUST be enabled if SSL/HTTPs is disabled + 'wantNameIdEncrypted' => false, +*/ + +); diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index c739fd9a3..9c5467e25 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -118,6 +118,7 @@ class LoginController extends Controller { $socialDrivers = $this->socialAuthService->getActiveDrivers(); $authMethod = config('auth.method'); + $samlEnabled = config('saml2_settings.enabled') == true; if ($request->has('email')) { session()->flashInput([ @@ -126,7 +127,11 @@ class LoginController extends Controller ]); } - return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]); + return view('auth.login', [ + 'socialDrivers' => $socialDrivers, + 'authMethod' => $authMethod, + 'samlEnabled' => $samlEnabled, + ]); } /** diff --git a/composer.json b/composer.json index 61bb8509e..457ce5093 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "socialiteproviders/gitlab": "^3.0", "socialiteproviders/twitch": "^3.0", "socialiteproviders/discord": "^2.0", - "doctrine/dbal": "^2.5" + "doctrine/dbal": "^2.5", + "aacotroneo/laravel-saml2": "^1.0" }, "require-dev": { "filp/whoops": "~2.0", diff --git a/composer.lock b/composer.lock index d7734ce1a..d35594642 100644 --- a/composer.lock +++ b/composer.lock @@ -1,11 +1,70 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0946a07729a7a1bfef9bac185a870afd", + "content-hash": "26a2c3ad0409c970f4f0c9b6dad49322", "packages": [ + { + "name": "aacotroneo/laravel-saml2", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/aacotroneo/laravel-saml2.git", + "reference": "5045701a07bcd7600a17c92971368669870f546a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aacotroneo/laravel-saml2/zipball/5045701a07bcd7600a17c92971368669870f546a", + "reference": "5045701a07bcd7600a17c92971368669870f546a", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "illuminate/support": ">=5.0.0", + "onelogin/php-saml": "^3.0.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "0.9.*" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Aacotroneo\\Saml2\\Saml2ServiceProvider" + ], + "aliases": { + "Saml2": "Aacotroneo\\Saml2\\Facades\\Saml2Auth" + } + } + }, + "autoload": { + "psr-0": { + "Aacotroneo\\Saml2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "aacotroneo", + "email": "aacotroneo@gmail.com" + } + ], + "description": "A Laravel package for Saml2 integration as a SP (service provider) based on OneLogin toolkit, which is much lightweight than simplesamlphp", + "homepage": "https://github.com/aacotroneo/laravel-saml2", + "keywords": [ + "SAML2", + "laravel", + "onelogin", + "saml" + ], + "time": "2018-11-08T14:03:58+00:00" + }, { "name": "aws/aws-sdk-php", "version": "3.86.2", @@ -1947,6 +2006,56 @@ ], "time": "2018-12-28T10:07:33+00:00" }, + { + "name": "onelogin/php-saml", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/onelogin/php-saml.git", + "reference": "845a6ce39e839ed9e687f80bffb02ffde16a70d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/845a6ce39e839ed9e687f80bffb02ffde16a70d0", + "reference": "845a6ce39e839ed9e687f80bffb02ffde16a70d0", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "robrichards/xmlseclibs": ">=3.0.3" + }, + "require-dev": { + "pdepend/pdepend": "^2.5.0", + "php-coveralls/php-coveralls": "^1.0.2 || ^2.0", + "phploc/phploc": "^2.1 || ^3.0 || ^4.0", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1", + "sebastian/phpcpd": "^2.0 || ^3.0 || ^4.0", + "squizlabs/php_codesniffer": "^3.1.1" + }, + "suggest": { + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-gettext": "Install gettext and php5-gettext libs to handle translations", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)" + }, + "type": "library", + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "OneLogin PHP SAML Toolkit", + "homepage": "https://developers.onelogin.com/saml/php", + "keywords": [ + "SAML2", + "onelogin", + "saml" + ], + "time": "2019-06-25T10:28:20+00:00" + }, { "name": "paragonie/random_compat", "version": "v9.99.99", @@ -2435,6 +2544,44 @@ ], "time": "2018-07-19T23:38:55+00:00" }, + { + "name": "robrichards/xmlseclibs", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "406c68ac9124db033d079284b719958b829cb830" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/406c68ac9124db033d079284b719958b829cb830", + "reference": "406c68ac9124db033d079284b719958b829cb830", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "time": "2018-11-15T11:59:02+00:00" + }, { "name": "sabberworm/php-css-parser", "version": "8.1.0", @@ -5416,6 +5563,7 @@ "mock", "xunit" ], + "abandoned": true, "time": "2018-08-09T05:50:03+00:00" }, { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 76aa3a6e9..72d8d00aa 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -46,6 +46,16 @@ @endforeach @endif + @if($samlEnabled) +
+ + @endif + @if(setting('registration-enabled', false))

@@ -55,4 +65,4 @@
-@stop \ No newline at end of file +@stop From bda0082461c4609b7333c8e3d9373f8d68da3da7 Mon Sep 17 00:00:00 2001 From: Daniel Seiler Date: Tue, 6 Aug 2019 23:42:46 +0200 Subject: [PATCH 2/4] Add login and automatic registration; Prepare Group sync --- app/Auth/Access/Saml2Service.php | 241 ++++++++++++++++++ app/Config/saml2_settings.php | 15 +- app/Config/services.php | 25 +- app/Exceptions/SamlException.php | 6 + app/Http/Controllers/Auth/LoginController.php | 2 +- app/Http/Kernel.php | 5 + app/Listeners/Saml2LoginEventListener.php | 42 +++ app/Providers/EventServiceProvider.php | 4 + resources/views/settings/roles/form.blade.php | 4 +- resources/views/users/form.blade.php | 4 +- 10 files changed, 334 insertions(+), 14 deletions(-) create mode 100644 app/Auth/Access/Saml2Service.php create mode 100644 app/Exceptions/SamlException.php create mode 100644 app/Listeners/Saml2LoginEventListener.php diff --git a/app/Auth/Access/Saml2Service.php b/app/Auth/Access/Saml2Service.php new file mode 100644 index 000000000..0b6cbe805 --- /dev/null +++ b/app/Auth/Access/Saml2Service.php @@ -0,0 +1,241 @@ +config = config('services.saml'); + $this->userRepo = $userRepo; + $this->user = $user; + $this->enabled = config('saml2_settings.enabled') === true; + } + + /** + * Check if groups should be synced. + * @return bool + */ + public function shouldSyncGroups() + { + return $this->enabled && $this->config['user_to_groups'] !== false; + } + + /** + * Extract the details of a user from a SAML response. + * @param $samlID + * @param $samlAttributes + * @return array + */ + public function getUserDetails($samlID, $samlAttributes) + { + $emailAttr = $this->config['email_attribute']; + $displayNameAttr = $this->config['display_name_attribute']; + $userNameAttr = $this->config['user_name_attribute']; + + $email = $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null); + + if ($userNameAttr === null) { + $userName = $samlID; + } else { + $userName = $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $samlID); + } + + $displayName = []; + foreach ($displayNameAttr as $dnAttr) { + $dnComponent = $this->getSamlResponseAttribute($samlAttributes, $dnAttr, null); + if ($dnComponent !== null) { + $displayName[] = $dnComponent; + } + } + + if (count($displayName) == 0) { + $displayName = $userName; + } else { + $displayName = implode(' ', $displayName); + } + + return [ + 'uid' => $userName, + 'name' => $displayName, + 'dn' => $samlID, + 'email' => $email, + ]; + } + + /** + * Get the groups a user is a part of from the SAML response. + * @param array $samlAttributes + * @return array + */ + public function getUserGroups($samlAttributes) + { + $groupsAttr = $this->config['group_attribute']; + $userGroups = $samlAttributes[$groupsAttr]; + + if (!is_array($userGroups)) { + $userGroups = []; + } + + return $userGroups; + } + + /** + * Get a property from an SAML response. + * Handles properties potentially being an array. + * @param array $userDetails + * @param string $propertyKey + * @param $defaultValue + * @return mixed + */ + protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue) + { + if (isset($samlAttributes[$propertyKey])) { + $data = $samlAttributes[$propertyKey]; + if (!is_array($data)) { + return $data; + } else if (count($data) == 0) { + return $defaultValue; + } else if (count($data) == 1) { + return $data[0]; + } else { + return $data; + } + } + + return $defaultValue; + } + + protected function registerUser($userDetails) { + + // Create an array of the user data to create a new user instance + $userData = [ + 'name' => $userDetails['name'], + 'email' => $userDetails['email'], + 'password' => str_random(30), + 'external_auth_id' => $userDetails['uid'], + 'email_confirmed' => true, + ]; + + $user = $this->user->forceCreate($userData); + $this->userRepo->attachDefaultRole($user); + $this->userRepo->downloadAndAssignUserAvatar($user); + return $user; + } + + public function processLoginCallback($samlID, $samlAttributes) { + + $userDetails = $this->getUserDetails($samlID, $samlAttributes); + $user = $this->user + ->where('external_auth_id', $userDetails['uid']) + ->first(); + + $isLoggedIn = auth()->check(); + + if (!$isLoggedIn) { + if ($user === null && config('services.saml.auto_register') === true) { + $user = $this->registerUser($userDetails); + } + + if ($user !== null) { + auth()->login($user); + } + } + + return $user; + } + + /** + * Sync the SAML groups to the user roles for the current user + * @param \BookStack\Auth\User $user + * @param array $samlAttributes + */ + public function syncGroups(User $user, array $samlAttributes) + { + $userSamlGroups = $this->getUserGroups($samlAttributes); + + // Get the ids for the roles from the names + $samlGroupsAsRoles = $this->matchSamlGroupsToSystemsRoles($userSamlGroups); + + // Sync groups + if ($this->config['remove_from_groups']) { + $user->roles()->sync($samlGroupsAsRoles); + $this->userRepo->attachDefaultRole($user); + } else { + $user->roles()->syncWithoutDetaching($samlGroupsAsRoles); + } + } + + /** + * Match an array of group names from SAML to BookStack system roles. + * Formats group names to be lower-case and hyphenated. + * @param array $groupNames + * @return \Illuminate\Support\Collection + */ + protected function matchSamlGroupsToSystemsRoles(array $groupNames) + { + foreach ($groupNames as $i => $groupName) { + $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); + } + + $roles = Role::query()->where(function (Builder $query) use ($groupNames) { + $query->whereIn('name', $groupNames); + foreach ($groupNames as $groupName) { + $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); + } + })->get(); + + $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { + return $this->roleMatchesGroupNames($role, $groupNames); + }); + + return $matchedRoles->pluck('id'); + } + + /** + * Check a role against an array of group names to see if it matches. + * Checked against role 'external_auth_id' if set otherwise the name of the role. + * @param \BookStack\Auth\Role $role + * @param array $groupNames + * @return bool + */ + protected function roleMatchesGroupNames(Role $role, array $groupNames) + { + if ($role->external_auth_id) { + $externalAuthIds = explode(',', strtolower($role->external_auth_id)); + foreach ($externalAuthIds as $externalAuthId) { + if (in_array(trim($externalAuthId), $groupNames)) { + return true; + } + } + return false; + } + + $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); + return in_array($roleName, $groupNames); + } + +} diff --git a/app/Config/saml2_settings.php b/app/Config/saml2_settings.php index a6d7a0204..015763b46 100644 --- a/app/Config/saml2_settings.php +++ b/app/Config/saml2_settings.php @@ -29,7 +29,7 @@ return $settings = array( * which middleware group to use for the saml routes * Laravel 5.2 will need a group which includes StartSession */ - 'routesMiddleware' => [], + 'routesMiddleware' => ['saml'], /** * Indicates how the parameters will be @@ -101,6 +101,8 @@ return $settings = array( // using HTTP-POST binding. // Leave blank to use the 'saml_acs' route 'url' => '', + + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', ), // Specifies info about where and how the message MUST be // returned to the requester, in this case our SP. @@ -138,7 +140,16 @@ return $settings = array( // 'certFingerprint' => '', ), - + /*** + * OneLogin compression settings + * + */ + 'compress' => array( + /** Whether requests should be GZ encoded */ + 'requests' => true, + /** Whether responses should be GZ compressed */ + 'responses' => true, + ), /*** * diff --git a/app/Config/services.php b/app/Config/services.php index 97cb71ddc..9cd647e6d 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -98,8 +98,8 @@ return [ 'okta' => [ 'client_id' => env('OKTA_APP_ID'), 'client_secret' => env('OKTA_APP_SECRET'), - 'redirect' => env('APP_URL') . '/login/service/okta/callback', - 'base_url' => env('OKTA_BASE_URL'), + 'redirect' => env('APP_URL') . '/login/service/okta/callback', + 'base_url' => env('OKTA_BASE_URL'), 'name' => 'Okta', 'auto_register' => env('OKTA_AUTO_REGISTER', false), 'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false), @@ -143,10 +143,21 @@ return [ 'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'), 'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'), 'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false), - 'user_to_groups' => env('LDAP_USER_TO_GROUPS',false), - 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), - 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false), - 'tls_insecure' => env('LDAP_TLS_INSECURE', false), - ] + 'user_to_groups' => env('LDAP_USER_TO_GROUPS',false), + 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), + 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false), + 'tls_insecure' => env('LDAP_TLS_INSECURE', false), + ], + + 'saml' => [ + 'enabled' => env('SAML2_ENABLED', false), + 'auto_register' => env('SAML_AUTO_REGISTER', false), + 'email_attribute' => env('SAML_EMAIL_ATTRIBUTE', 'email'), + 'display_name_attribute' => explode('|', env('SAML_DISPLAY_NAME_ATTRIBUTE', 'username')), + 'user_name_attribute' => env('SAML_USER_NAME_ATTRIBUTE', null), + 'group_attribute' => env('SAML_GROUP_ATTRIBUTE', 'group'), + 'user_to_groups' => env('SAML_USER_TO_GROUPS', false), + 'id_is_user_name' => env('SAML_ID_IS_USER_NAME', true), + ] ]; diff --git a/app/Exceptions/SamlException.php b/app/Exceptions/SamlException.php new file mode 100644 index 000000000..f9668919c --- /dev/null +++ b/app/Exceptions/SamlException.php @@ -0,0 +1,6 @@ +socialAuthService->getActiveDrivers(); $authMethod = config('auth.method'); - $samlEnabled = config('saml2_settings.enabled') == true; + $samlEnabled = config('services.saml.enabled') == true; if ($request->has('email')) { session()->flashInput([ diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index cd894de95..7794f3401 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -37,6 +37,11 @@ class Kernel extends HttpKernel 'throttle:60,1', 'bindings', ], + 'saml' => [ + \BookStack\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + ], ]; /** diff --git a/app/Listeners/Saml2LoginEventListener.php b/app/Listeners/Saml2LoginEventListener.php new file mode 100644 index 000000000..74c4d6f27 --- /dev/null +++ b/app/Listeners/Saml2LoginEventListener.php @@ -0,0 +1,42 @@ +saml = $saml; + } + + /** + * Handle the event. + * + * @param Saml2LoginEvent $event + * @return void + */ + public function handle(Saml2LoginEvent $event) + { + $messageId = $event->getSaml2Auth()->getLastMessageId(); + // TODO: Add your own code preventing reuse of a $messageId to stop replay attacks + + $samlUser = $event->getSaml2User(); + + $attrs = $samlUser->getAttributes(); + $id = $samlUser->getUserId(); + //$assertion = $user->getRawSamlAssertion() + + $user = $this->saml->processLoginCallback($id, $attrs); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a826185d8..50436916a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -4,6 +4,7 @@ namespace BookStack\Providers; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use SocialiteProviders\Manager\SocialiteWasCalled; +use Aacotroneo\Saml2\Events\Saml2LoginEvent; class EventServiceProvider extends ServiceProvider { @@ -21,6 +22,9 @@ class EventServiceProvider extends ServiceProvider 'SocialiteProviders\Twitch\TwitchExtendSocialite@handle', 'SocialiteProviders\Discord\DiscordExtendSocialite@handle', ], + Saml2LoginEvent::class => [ + 'BookStack\Listeners\Saml2LoginEventListener@handle', + ] ]; /** diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 68b841e03..d7c1fc47c 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -19,7 +19,7 @@ @include('form.text', ['name' => 'description']) - @if(config('auth.method') === 'ldap') + @if(config('auth.method') === 'ldap' || config('services.saml.enabled') === true)
@include('form.text', ['name' => 'external_auth_id']) @@ -254,4 +254,4 @@ {{ trans('settings.role_users_none') }}

@endif -
\ No newline at end of file + diff --git a/resources/views/users/form.blade.php b/resources/views/users/form.blade.php index 96beb7b2f..7a3d44935 100644 --- a/resources/views/users/form.blade.php +++ b/resources/views/users/form.blade.php @@ -25,7 +25,7 @@ -@if($authMethod === 'ldap' && userCan('users-manage')) +@if(($authMethod === 'ldap' || config('services.saml.enabled') === true) && userCan('users-manage'))
@@ -67,4 +67,4 @@
-@endif \ No newline at end of file +@endif From 03dbe32f9926b53c1a0c35534e57f526c5d2bc2b Mon Sep 17 00:00:00 2001 From: Daniel Seiler Date: Wed, 7 Aug 2019 12:07:21 +0200 Subject: [PATCH 3/4] Refactor for codestyle --- app/Auth/Access/ExternalAuthService.php | 75 ++++++++++ app/Auth/Access/LdapService.php | 64 +------- app/Auth/Access/Saml2Service.php | 189 ++++++++++-------------- 3 files changed, 157 insertions(+), 171 deletions(-) create mode 100644 app/Auth/Access/ExternalAuthService.php diff --git a/app/Auth/Access/ExternalAuthService.php b/app/Auth/Access/ExternalAuthService.php new file mode 100644 index 000000000..b1c036018 --- /dev/null +++ b/app/Auth/Access/ExternalAuthService.php @@ -0,0 +1,75 @@ +external_auth_id) { + $externalAuthIds = explode(',', strtolower($role->external_auth_id)); + foreach ($externalAuthIds as $externalAuthId) { + if (in_array(trim($externalAuthId), $groupNames)) { + return true; + } + } + return false; + } + + $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); + return in_array($roleName, $groupNames); + } + + /** + * Match an array of group names to BookStack system roles. + * Formats group names to be lower-case and hyphenated. + * @param array $groupNames + * @return \Illuminate\Support\Collection + */ + protected function matchGroupsToSystemsRoles(array $groupNames) + { + foreach ($groupNames as $i => $groupName) { + $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); + } + + $roles = Role::query()->where(function (Builder $query) use ($groupNames) { + $query->whereIn('name', $groupNames); + foreach ($groupNames as $groupName) { + $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); + } + })->get(); + + $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { + return $this->roleMatchesGroupNames($role, $groupNames); + }); + + return $matchedRoles->pluck('id'); + } + + /** + * Sync the groups to the user roles for the current user + * @param \BookStack\Auth\User $user + * @param array $samlAttributes + */ + public function syncWithGroups(User $user, array $userGroups) + { + // Get the ids for the roles from the names + $samlGroupsAsRoles = $this->matchGroupsToSystemsRoles($userSamlGroups); + + // Sync groups + if ($this->config['remove_from_groups']) { + $user->roles()->sync($samlGroupsAsRoles); + $this->userRepo->attachDefaultRole($user); + } else { + $user->roles()->syncWithoutDetaching($samlGroupsAsRoles); + } + } +} diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index c7415e1f7..3111ea9fa 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -1,7 +1,6 @@ getUserGroups($username); - - // Get the ids for the roles from the names - $ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups); - - // Sync groups - if ($this->config['remove_from_groups']) { - $user->roles()->sync($ldapGroupsAsRoles); - $this->userRepo->attachDefaultRole($user); - } else { - $user->roles()->syncWithoutDetaching($ldapGroupsAsRoles); - } - } - - /** - * Match an array of group names from LDAP to BookStack system roles. - * Formats LDAP group names to be lower-case and hyphenated. - * @param array $groupNames - * @return \Illuminate\Support\Collection - */ - protected function matchLdapGroupsToSystemsRoles(array $groupNames) - { - foreach ($groupNames as $i => $groupName) { - $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); - } - - $roles = Role::query()->where(function (Builder $query) use ($groupNames) { - $query->whereIn('name', $groupNames); - foreach ($groupNames as $groupName) { - $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); - } - })->get(); - - $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { - return $this->roleMatchesGroupNames($role, $groupNames); - }); - - return $matchedRoles->pluck('id'); - } - - /** - * Check a role against an array of group names to see if it matches. - * Checked against role 'external_auth_id' if set otherwise the name of the role. - * @param \BookStack\Auth\Role $role - * @param array $groupNames - * @return bool - */ - protected function roleMatchesGroupNames(Role $role, array $groupNames) - { - if ($role->external_auth_id) { - $externalAuthIds = explode(',', strtolower($role->external_auth_id)); - foreach ($externalAuthIds as $externalAuthId) { - if (in_array(trim($externalAuthId), $groupNames)) { - return true; - } - } - return false; - } - - $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); - return in_array($roleName, $groupNames); + $this->syncWithGroups($user, $userLdapGroups); } } diff --git a/app/Auth/Access/Saml2Service.php b/app/Auth/Access/Saml2Service.php index 0b6cbe805..95049efd2 100644 --- a/app/Auth/Access/Saml2Service.php +++ b/app/Auth/Access/Saml2Service.php @@ -1,13 +1,11 @@ enabled && $this->config['user_to_groups'] !== false; } - /** - * Extract the details of a user from a SAML response. - * @param $samlID - * @param $samlAttributes - * @return array + /** Calculate the display name + * @param array $samlAttributes + * @param string $defaultValue + * @return string */ - public function getUserDetails($samlID, $samlAttributes) + protected function getUserDisplayName(array $samlAttributes, string $defaultValue) { - $emailAttr = $this->config['email_attribute']; $displayNameAttr = $this->config['display_name_attribute']; - $userNameAttr = $this->config['user_name_attribute']; - - $email = $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null); - - if ($userNameAttr === null) { - $userName = $samlID; - } else { - $userName = $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $samlID); - } $displayName = []; foreach ($displayNameAttr as $dnAttr) { @@ -73,16 +60,43 @@ class Saml2Service } if (count($displayName) == 0) { - $displayName = $userName; + $displayName = $defaultValue; } else { $displayName = implode(' ', $displayName); } + return $displayName; + } + + protected function getUserName(array $samlAttributes, string $defaultValue) + { + $userNameAttr = $this->config['user_name_attribute']; + + if ($userNameAttr === null) { + $userName = $defaultValue; + } else { + $userName = $this->getSamlResponseAttribute($samlAttributes, $userNameAttr, $defaultValue); + } + + return $userName; + } + + /** + * Extract the details of a user from a SAML response. + * @param $samlID + * @param $samlAttributes + * @return array + */ + public function getUserDetails($samlID, $samlAttributes) + { + $emailAttr = $this->config['email_attribute']; + $userName = $this->getUserName($samlAttributes, $samlID); + return [ 'uid' => $userName, - 'name' => $displayName, + 'name' => $this->getUserDisplayName($samlAttributes, $userName), 'dn' => $samlID, - 'email' => $email, + 'email' => $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null), ]; } @@ -115,22 +129,28 @@ class Saml2Service { if (isset($samlAttributes[$propertyKey])) { $data = $samlAttributes[$propertyKey]; - if (!is_array($data)) { - return $data; - } else if (count($data) == 0) { - return $defaultValue; - } else if (count($data) == 1) { - return $data[0]; - } else { - return $data; + if (is_array($data)) { + if (count($data) == 0) { + $data = $defaultValue; + } else if (count($data) == 1) { + $data = $data[0]; + } } + } else { + $data = $defaultValue; } - return $defaultValue; + return $data; } - protected function registerUser($userDetails) { - + /** + * Register a user that is authenticated but not + * already registered. + * @param array $userDetails + * @return User + */ + protected function registerUser($userDetails) + { // Create an array of the user data to create a new user instance $userData = [ 'name' => $userDetails['name'], @@ -146,96 +166,47 @@ class Saml2Service return $user; } - public function processLoginCallback($samlID, $samlAttributes) { - - $userDetails = $this->getUserDetails($samlID, $samlAttributes); + /** + * Get the user from the database for the specified details. + * @param array $userDetails + * @return User|null + */ + protected function getOrRegisterUser($userDetails) + { + $isRegisterEnabled = config('services.saml.auto_register') === true; $user = $this->user - ->where('external_auth_id', $userDetails['uid']) - ->first(); + ->where('external_auth_id', $userDetails['uid']) + ->first(); - $isLoggedIn = auth()->check(); - - if (!$isLoggedIn) { - if ($user === null && config('services.saml.auto_register') === true) { - $user = $this->registerUser($userDetails); - } - - if ($user !== null) { - auth()->login($user); - } + if ($user === null && $isRegisterEnabled) { + $user = $this->registerUser($userDetails); } return $user; } /** - * Sync the SAML groups to the user roles for the current user - * @param \BookStack\Auth\User $user - * @param array $samlAttributes + * Process the SAML response for a user. Login the user when + * they exist, optionally registering them automatically. + * @param string $samlID + * @param array $samlAttributes */ - public function syncGroups(User $user, array $samlAttributes) + public function processLoginCallback($samlID, $samlAttributes) { - $userSamlGroups = $this->getUserGroups($samlAttributes); + $userDetails = $this->getUserDetails($samlID, $samlAttributes); + $isLoggedIn = auth()->check(); - // Get the ids for the roles from the names - $samlGroupsAsRoles = $this->matchSamlGroupsToSystemsRoles($userSamlGroups); - - // Sync groups - if ($this->config['remove_from_groups']) { - $user->roles()->sync($samlGroupsAsRoles); - $this->userRepo->attachDefaultRole($user); + if ($isLoggedIn) { + logger()->error("Already logged in"); } else { - $user->roles()->syncWithoutDetaching($samlGroupsAsRoles); - } - } - - /** - * Match an array of group names from SAML to BookStack system roles. - * Formats group names to be lower-case and hyphenated. - * @param array $groupNames - * @return \Illuminate\Support\Collection - */ - protected function matchSamlGroupsToSystemsRoles(array $groupNames) - { - foreach ($groupNames as $i => $groupName) { - $groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName))); - } - - $roles = Role::query()->where(function (Builder $query) use ($groupNames) { - $query->whereIn('name', $groupNames); - foreach ($groupNames as $groupName) { - $query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%'); + $user = $this->getOrRegisterUser($userDetails); + if ($user === null) { + logger()->error("User does not exist"); + } else { + auth()->login($user); } - })->get(); - - $matchedRoles = $roles->filter(function (Role $role) use ($groupNames) { - return $this->roleMatchesGroupNames($role, $groupNames); - }); - - return $matchedRoles->pluck('id'); - } - - /** - * Check a role against an array of group names to see if it matches. - * Checked against role 'external_auth_id' if set otherwise the name of the role. - * @param \BookStack\Auth\Role $role - * @param array $groupNames - * @return bool - */ - protected function roleMatchesGroupNames(Role $role, array $groupNames) - { - if ($role->external_auth_id) { - $externalAuthIds = explode(',', strtolower($role->external_auth_id)); - foreach ($externalAuthIds as $externalAuthId) { - if (in_array(trim($externalAuthId), $groupNames)) { - return true; - } - } - return false; } - $roleName = str_replace(' ', '-', trim(strtolower($role->display_name))); - return in_array($roleName, $groupNames); + return $user; } - } From 8e723f10dc3db49df9dc66ea5a90e3153eda54e8 Mon Sep 17 00:00:00 2001 From: Daniel Seiler Date: Wed, 7 Aug 2019 15:31:10 +0200 Subject: [PATCH 4/4] Add error messages, fix LDAP error --- app/Auth/Access/ExternalAuthService.php | 9 +++--- app/Auth/Access/LdapService.php | 1 - app/Auth/Access/Saml2Service.php | 41 +++++++++++++++++-------- app/Config/services.php | 2 ++ app/Exceptions/SamlException.php | 2 +- resources/lang/de/errors.php | 2 ++ resources/lang/de_informal/errors.php | 1 + resources/lang/en/errors.php | 2 ++ resources/views/auth/login.blade.php | 2 +- 9 files changed, 42 insertions(+), 20 deletions(-) diff --git a/app/Auth/Access/ExternalAuthService.php b/app/Auth/Access/ExternalAuthService.php index b1c036018..77c7d1351 100644 --- a/app/Auth/Access/ExternalAuthService.php +++ b/app/Auth/Access/ExternalAuthService.php @@ -2,6 +2,7 @@ use BookStack\Auth\Role; use BookStack\Auth\User; +use Illuminate\Database\Eloquent\Builder; class ExternalAuthService { @@ -57,19 +58,19 @@ class ExternalAuthService /** * Sync the groups to the user roles for the current user * @param \BookStack\Auth\User $user - * @param array $samlAttributes + * @param array $userGroups */ public function syncWithGroups(User $user, array $userGroups) { // Get the ids for the roles from the names - $samlGroupsAsRoles = $this->matchGroupsToSystemsRoles($userSamlGroups); + $groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups); // Sync groups if ($this->config['remove_from_groups']) { - $user->roles()->sync($samlGroupsAsRoles); + $user->roles()->sync($groupsAsRoles); $this->userRepo->attachDefaultRole($user); } else { - $user->roles()->syncWithoutDetaching($samlGroupsAsRoles); + $user->roles()->syncWithoutDetaching($groupsAsRoles); } } } diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index 3111ea9fa..b0700322f 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -5,7 +5,6 @@ use BookStack\Auth\User; use BookStack\Auth\UserRepo; use BookStack\Exceptions\LdapException; use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Database\Eloquent\Builder; /** * Class LdapService diff --git a/app/Auth/Access/Saml2Service.php b/app/Auth/Access/Saml2Service.php index 95049efd2..056977a3d 100644 --- a/app/Auth/Access/Saml2Service.php +++ b/app/Auth/Access/Saml2Service.php @@ -5,8 +5,6 @@ use BookStack\Auth\User; use BookStack\Auth\UserRepo; use BookStack\Exceptions\SamlException; use Illuminate\Contracts\Auth\Authenticatable; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\Log; /** @@ -117,6 +115,27 @@ class Saml2Service extends Access\ExternalAuthService return $userGroups; } + /** + * For an array of strings, return a default for an empty array, + * a string for an array with one element and the full array for + * more than one element. + * + * @param array $data + * @param $defaultValue + * @return string + */ + protected function simplifyValue(array $data, $defaultValue) { + switch (count($data)) { + case 0: + $data = $defaultValue; + break; + case 1: + $data = $data[0]; + break; + } + return $data; + } + /** * Get a property from an SAML response. * Handles properties potentially being an array. @@ -128,16 +147,9 @@ class Saml2Service extends Access\ExternalAuthService protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue) { if (isset($samlAttributes[$propertyKey])) { - $data = $samlAttributes[$propertyKey]; - if (is_array($data)) { - if (count($data) == 0) { - $data = $defaultValue; - } else if (count($data) == 1) { - $data = $data[0]; - } - } + $data = $this->simplifyValue($samlAttributes[$propertyKey], $defaultValue); } else { - $data = $defaultValue; + $data = $defaultValue; } return $data; @@ -190,6 +202,7 @@ class Saml2Service extends Access\ExternalAuthService * they exist, optionally registering them automatically. * @param string $samlID * @param array $samlAttributes + * @throws SamlException */ public function processLoginCallback($samlID, $samlAttributes) { @@ -197,12 +210,14 @@ class Saml2Service extends Access\ExternalAuthService $isLoggedIn = auth()->check(); if ($isLoggedIn) { - logger()->error("Already logged in"); + throw new SamlException(trans('errors.saml_already_logged_in'), '/login'); } else { $user = $this->getOrRegisterUser($userDetails); if ($user === null) { - logger()->error("User does not exist"); + throw new SamlException(trans('errors.saml_user_not_registered', ['name' => $userDetails['uid']]), '/login'); } else { + $groups = $this->getUserGroups($samlAttributes); + $this->syncWithGroups($user, $groups); auth()->login($user); } } diff --git a/app/Config/services.php b/app/Config/services.php index 9cd647e6d..b3dc9f087 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -150,12 +150,14 @@ return [ ], 'saml' => [ + 'name' => env('SAML_NAME', 'SSO'), 'enabled' => env('SAML2_ENABLED', false), 'auto_register' => env('SAML_AUTO_REGISTER', false), 'email_attribute' => env('SAML_EMAIL_ATTRIBUTE', 'email'), 'display_name_attribute' => explode('|', env('SAML_DISPLAY_NAME_ATTRIBUTE', 'username')), 'user_name_attribute' => env('SAML_USER_NAME_ATTRIBUTE', null), 'group_attribute' => env('SAML_GROUP_ATTRIBUTE', 'group'), + 'remove_from_groups' => env('SAML_REMOVE_FROM_GROUPS',false), 'user_to_groups' => env('SAML_USER_TO_GROUPS', false), 'id_is_user_name' => env('SAML_ID_IS_USER_NAME', true), ] diff --git a/app/Exceptions/SamlException.php b/app/Exceptions/SamlException.php index f9668919c..13db23f27 100644 --- a/app/Exceptions/SamlException.php +++ b/app/Exceptions/SamlException.php @@ -1,6 +1,6 @@ 'LDAP-Zugriff mit DN und Passwort ist fehlgeschlagen', 'ldap_extension_not_installed' => 'LDAP-PHP-Erweiterung ist nicht installiert.', 'ldap_cannot_connect' => 'Die Verbindung zum LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.', + 'saml_already_logged_in' => 'Sie sind bereits angemeldet', + 'saml_user_not_registered' => 'Kein Benutzer mit ID :name registriert und die automatische Registrierung ist deaktiviert', 'social_no_action_defined' => 'Es ist keine Aktion definiert', 'social_login_bad_response' => "Fehler bei der :socialAccount-Anmeldung: \n:error", 'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount-Konto an.', diff --git a/resources/lang/de_informal/errors.php b/resources/lang/de_informal/errors.php index 924deee0d..420c35c8d 100644 --- a/resources/lang/de_informal/errors.php +++ b/resources/lang/de_informal/errors.php @@ -9,6 +9,7 @@ return [ // Auth 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melde dich an.', 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registriere dich erneut.', + 'saml_already_logged_in' => 'Du bist bereits angemeldet', 'social_account_in_use' => 'Dieses :socialAccount-Konto wird bereits verwendet. Bitte melde dich mit dem :socialAccount-Konto an.', 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Du bereits registriert bist, kannst Du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.', 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in Deinen Profil-Einstellungen tun.', diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index b91a0c3e1..40c0bbffb 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -17,6 +17,8 @@ return [ 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', + 'saml_already_logged_in' => 'Already logged in', + 'saml_user_not_registered' => 'The user :name is not registered and automatic registration is disabled', 'social_no_action_defined' => 'No action defined', 'social_login_bad_response' => "Error received during :socialAccount login: \n:error", 'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.', diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 72d8d00aa..8d89c1288 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -51,7 +51,7 @@ @endif