diff --git a/app/Auth/Access/ExternalAuthService.php b/app/Auth/Access/ExternalAuthService.php new file mode 100644 index 000000000..77c7d1351 --- /dev/null +++ b/app/Auth/Access/ExternalAuthService.php @@ -0,0 +1,76 @@ +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 $userGroups + */ + public function syncWithGroups(User $user, array $userGroups) + { + // Get the ids for the roles from the names + $groupsAsRoles = $this->matchGroupsToSystemsRoles($userGroups); + + // Sync groups + if ($this->config['remove_from_groups']) { + $user->roles()->sync($groupsAsRoles); + $this->userRepo->attachDefaultRole($user); + } else { + $user->roles()->syncWithoutDetaching($groupsAsRoles); + } + } +} diff --git a/app/Auth/Access/LdapService.php b/app/Auth/Access/LdapService.php index c7415e1f7..b0700322f 100644 --- a/app/Auth/Access/LdapService.php +++ b/app/Auth/Access/LdapService.php @@ -1,19 +1,17 @@ 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 new file mode 100644 index 000000000..056977a3d --- /dev/null +++ b/app/Auth/Access/Saml2Service.php @@ -0,0 +1,227 @@ +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; + } + + /** Calculate the display name + * @param array $samlAttributes + * @param string $defaultValue + * @return string + */ + protected function getUserDisplayName(array $samlAttributes, string $defaultValue) + { + $displayNameAttr = $this->config['display_name_attribute']; + + $displayName = []; + foreach ($displayNameAttr as $dnAttr) { + $dnComponent = $this->getSamlResponseAttribute($samlAttributes, $dnAttr, null); + if ($dnComponent !== null) { + $displayName[] = $dnComponent; + } + } + + if (count($displayName) == 0) { + $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' => $this->getUserDisplayName($samlAttributes, $userName), + 'dn' => $samlID, + 'email' => $this->getSamlResponseAttribute($samlAttributes, $emailAttr, null), + ]; + } + + /** + * 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; + } + + /** + * 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. + * @param array $userDetails + * @param string $propertyKey + * @param $defaultValue + * @return mixed + */ + protected function getSamlResponseAttribute(array $samlAttributes, string $propertyKey, $defaultValue) + { + if (isset($samlAttributes[$propertyKey])) { + $data = $this->simplifyValue($samlAttributes[$propertyKey], $defaultValue); + } else { + $data = $defaultValue; + } + + return $data; + } + + /** + * 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'], + '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; + } + + /** + * 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(); + + if ($user === null && $isRegisterEnabled) { + $user = $this->registerUser($userDetails); + } + + return $user; + } + + /** + * Process the SAML response for a user. Login the user when + * they exist, optionally registering them automatically. + * @param string $samlID + * @param array $samlAttributes + * @throws SamlException + */ + public function processLoginCallback($samlID, $samlAttributes) + { + $userDetails = $this->getUserDetails($samlID, $samlAttributes); + $isLoggedIn = auth()->check(); + + if ($isLoggedIn) { + throw new SamlException(trans('errors.saml_already_logged_in'), '/login'); + } else { + $user = $this->getOrRegisterUser($userDetails); + if ($user === null) { + 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); + } + } + + return $user; + } +} diff --git a/app/Config/app.php b/app/Config/app.php index 0d06a9b21..9dae697da 100755 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -105,6 +105,7 @@ return [ Intervention\Image\ImageServiceProvider::class, Barryvdh\DomPDF\ServiceProvider::class, Barryvdh\Snappy\ServiceProvider::class, + Aacotroneo\Saml2\Saml2ServiceProvider::class, // BookStack replacement service providers (Extends Laravel) BookStack\Providers\PaginationServiceProvider::class, diff --git a/app/Config/saml2_settings.php b/app/Config/saml2_settings.php new file mode 100644 index 000000000..015763b46 --- /dev/null +++ b/app/Config/saml2_settings.php @@ -0,0 +1,241 @@ + 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' => ['saml'], + + /** + * 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' => '', + + '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. + // 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 compression settings + * + */ + 'compress' => array( + /** Whether requests should be GZ encoded */ + 'requests' => true, + /** Whether responses should be GZ compressed */ + 'responses' => true, + ), + + /*** + * + * 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/Config/services.php b/app/Config/services.php index 923015f6e..0f80a9fc1 100644 --- a/app/Config/services.php +++ b/app/Config/services.php @@ -130,6 +130,19 @@ return [ 'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'), 'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false), 'tls_insecure' => env('LDAP_TLS_INSECURE', false), + ], + + '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 new file mode 100644 index 000000000..13db23f27 --- /dev/null +++ b/app/Exceptions/SamlException.php @@ -0,0 +1,6 @@ +socialAuthService->getActiveDrivers(); $authMethod = config('auth.method'); + $samlEnabled = config('services.saml.enabled') == true; if ($request->has('email')) { session()->flashInput([ @@ -127,7 +128,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/app/Http/Kernel.php b/app/Http/Kernel.php index f9752da09..027e469c8 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -39,6 +39,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/composer.json b/composer.json index a8b9456a1..1d952a0c5 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "socialiteproviders/microsoft-azure": "^3.0", "socialiteproviders/okta": "^1.0", "socialiteproviders/slack": "^3.0", - "socialiteproviders/twitch": "^5.0" + "socialiteproviders/twitch": "^5.0", + "aacotroneo/laravel-saml2": "^1.0" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.2.8", diff --git a/resources/lang/de/errors.php b/resources/lang/de/errors.php index ccec60561..ff2bf8653 100644 --- a/resources/lang/de/errors.php +++ b/resources/lang/de/errors.php @@ -17,6 +17,8 @@ return [ 'ldap_fail_authed' => '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 9b5b5166b..e62350156 100644 --- a/resources/lang/de_informal/errors.php +++ b/resources/lang/de_informal/errors.php @@ -9,16 +9,11 @@ return [ 'permissionJson' => 'Du hast keine Berechtigung, die angeforderte Aktion auszuführen.', // Auth + 'saml_already_logged_in' => 'Du bist bereits angemeldet', 'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten registriert.', '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.', - 'email_confirmation_expired' => 'Der Bestätigungslink ist abgelaufen. Es wurde eine neue Bestätigungs-E-Mail gesendet.', - 'ldap_fail_anonymous' => 'Anonymer LDAP-Zugriff ist fehlgeschlafgen', - 'ldap_fail_authed' => '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.', - '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 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_existing' => 'Dieses :socialAccount-Konto ist bereits mit Ihrem Profil verknüpft.', diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index c3b47744d..37de5dcc1 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 fbf540d71..7e4a3992b 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -45,6 +45,16 @@ @endforeach @endif + @if($samlEnabled) +
+ + @endif + @if(setting('registration-enabled', false))

@@ -54,4 +64,4 @@
-@stop \ No newline at end of file +@stop diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 20b8d65ed..94e63b158 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']) @@ -255,4 +255,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 32b717ec8..6c08cad44 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'))
@@ -84,4 +84,4 @@
-@endif \ No newline at end of file +@endif