X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/c429cf78187e80deb63982a282a1c6889f30291a..refs/pull/3210/head:/app/Auth/Access/Saml2Service.php diff --git a/app/Auth/Access/Saml2Service.php b/app/Auth/Access/Saml2Service.php index 28d4d4030..f5d0cd7cc 100644 --- a/app/Auth/Access/Saml2Service.php +++ b/app/Auth/Access/Saml2Service.php @@ -2,17 +2,14 @@ namespace BookStack\Auth\Access; -use BookStack\Actions\ActivityType; use BookStack\Auth\User; use BookStack\Exceptions\JsonDebugException; use BookStack\Exceptions\SamlException; +use BookStack\Exceptions\StoppedAuthenticationException; use BookStack\Exceptions\UserRegistrationException; -use BookStack\Facades\Activity; -use BookStack\Facades\Theme; -use BookStack\Theming\ThemeEvents; use Exception; -use Illuminate\Support\Str; use OneLogin\Saml2\Auth; +use OneLogin\Saml2\Constants; use OneLogin\Saml2\Error; use OneLogin\Saml2\IdPMetadataParser; use OneLogin\Saml2\ValidationError; @@ -21,20 +18,25 @@ use OneLogin\Saml2\ValidationError; * Class Saml2Service * Handles any app-specific SAML tasks. */ -class Saml2Service extends ExternalAuthService +class Saml2Service { protected $config; protected $registrationService; - protected $user; + protected $loginService; + protected $groupSyncService; /** * Saml2Service constructor. */ - public function __construct(RegistrationService $registrationService, User $user) - { + public function __construct( + RegistrationService $registrationService, + LoginService $loginService, + GroupSyncService $groupSyncService + ) { $this->config = config('saml2'); $this->registrationService = $registrationService; - $this->user = $user; + $this->loginService = $loginService; + $this->groupSyncService = $groupSyncService; } /** @@ -58,13 +60,20 @@ class Saml2Service extends ExternalAuthService * * @throws Error */ - public function logout(): array + public function logout(User $user): array { $toolKit = $this->getToolkit(); $returnRoute = url('/'); try { - $url = $toolKit->logout($returnRoute, [], null, null, true); + $url = $toolKit->logout( + $returnRoute, + [], + $user->email, + null, + true, + Constants::NAMEID_EMAIL_ADDRESS + ); $id = $toolKit->getLastRequestID(); } catch (Error $error) { if ($error->getCode() !== Error::SAML_SINGLE_LOGOUT_NOT_SUPPORTED) { @@ -90,8 +99,11 @@ class Saml2Service extends ExternalAuthService * @throws JsonDebugException * @throws UserRegistrationException */ - public function processAcsResponse(?string $requestId): ?User + public function processAcsResponse(?string $requestId, string $samlResponse): ?User { + // The SAML2 toolkit expects the response to be within the $_POST superglobal + // so we need to manually put it back there at this point. + $_POST['SAMLResponse'] = $samlResponse; $toolkit = $this->getToolkit(); $toolkit->processResponse($requestId); $errors = $toolkit->getErrors(); @@ -120,8 +132,13 @@ class Saml2Service extends ExternalAuthService public function processSlsResponse(?string $requestId): ?string { $toolkit = $this->getToolkit(); - $redirect = $toolkit->processSLO(true, $requestId, false, null, true); + // The $retrieveParametersFromServer in the call below will mean the library will take the query + // parameters, used for the response signing, from the raw $_SERVER['QUERY_STRING'] + // value so that the exact encoding format is matched when checking the signature. + // This is primarily due to ADFS encoding query params with lowercase percent encoding while + // PHP (And most other sensible providers) standardise on uppercase. + $redirect = $toolkit->processSLO(true, $requestId, true, null, true); $errors = $toolkit->getErrors(); if (!empty($errors)) { @@ -261,6 +278,8 @@ class Saml2Service extends ExternalAuthService /** * Extract the details of a user from a SAML response. + * + * @return array{external_id: string, name: string, email: string, saml_id: string} */ protected function getUserDetails(string $samlID, $samlAttributes): array { @@ -325,31 +344,6 @@ class Saml2Service extends ExternalAuthService return $defaultValue; } - /** - * Get the user from the database for the specified details. - * - * @throws UserRegistrationException - */ - protected function getOrRegisterUser(array $userDetails): ?User - { - $user = $this->user->newQuery() - ->where('external_auth_id', '=', $userDetails['external_id']) - ->first(); - - if (is_null($user)) { - $userData = [ - 'name' => $userDetails['name'], - 'email' => $userDetails['email'], - 'password' => Str::random(32), - 'external_auth_id' => $userDetails['external_id'], - ]; - - $user = $this->registrationService->registerUser($userData, null, false); - } - - return $user; - } - /** * Process the SAML response for a user. Login the user when * they exist, optionally registering them automatically. @@ -357,6 +351,7 @@ class Saml2Service extends ExternalAuthService * @throws SamlException * @throws JsonDebugException * @throws UserRegistrationException + * @throws StoppedAuthenticationException */ public function processLoginCallback(string $samlID, array $samlAttributes): User { @@ -379,19 +374,22 @@ class Saml2Service extends ExternalAuthService throw new SamlException(trans('errors.saml_already_logged_in'), '/login'); } - $user = $this->getOrRegisterUser($userDetails); + $user = $this->registrationService->findOrRegister( + $userDetails['name'], + $userDetails['email'], + $userDetails['external_id'] + ); + if ($user === null) { throw new SamlException(trans('errors.saml_user_not_registered', ['name' => $userDetails['external_id']]), '/login'); } if ($this->shouldSyncGroups()) { $groups = $this->getUserGroups($samlAttributes); - $this->syncWithGroups($user, $groups); + $this->groupSyncService->syncUserWithFoundGroups($user, $groups, $this->config['remove_from_groups']); } - auth()->login($user); - Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}"); - Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user); + $this->loginService->login($user, 'saml2'); return $user; }