X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/263384cf99864ebdb0408fd4e478f783aa487c1a..refs/pull/3918/head:/app/Auth/Access/Oidc/OidcService.php diff --git a/app/Auth/Access/Oidc/OidcService.php b/app/Auth/Access/Oidc/OidcService.php index d59d274e8..a9323d423 100644 --- a/app/Auth/Access/Oidc/OidcService.php +++ b/app/Auth/Access/Oidc/OidcService.php @@ -1,21 +1,19 @@ -registrationService = $registrationService; $this->loginService = $loginService; $this->httpClient = $httpClient; + $this->groupService = $groupService; } /** * Initiate an authorization flow. + * + * @throws OidcException + * * @return array{url: string, state: string} */ public function login(): array @@ -46,20 +53,22 @@ class OidcService $settings = $this->getProviderSettings(); $provider = $this->getProvider($settings); return [ - 'url' => $provider->getAuthorizationUrl(), + 'url' => $provider->getAuthorizationUrl(), 'state' => $provider->getState(), ]; } /** * Process the Authorization response from the authorization server and - * return the matching, or new if registration active, user matched to - * the authorization server. - * Returns null if not authenticated. - * @throws Exception - * @throws ClientExceptionInterface + * return the matching, or new if registration active, user matched to the + * authorization server. Throws if the user cannot be auth if not authenticated. + * + * @throws JsonDebugException + * @throws OidcException + * @throws StoppedAuthenticationException + * @throws IdentityProviderException */ - public function processAuthorizeResponse(?string $authorizationCode): ?User + public function processAuthorizeResponse(?string $authorizationCode): User { $settings = $this->getProviderSettings(); $provider = $this->getProvider($settings); @@ -73,19 +82,18 @@ class OidcService } /** - * @throws OidcIssuerDiscoveryException - * @throws ClientExceptionInterface + * @throws OidcException */ protected function getProviderSettings(): OidcProviderSettings { $config = $this->config(); $settings = new OidcProviderSettings([ - 'issuer' => $config['issuer'], - 'clientId' => $config['client_id'], - 'clientSecret' => $config['client_secret'], - 'redirectUri' => url('/http/source.bookstackapp.com/oidc/callback'), + 'issuer' => $config['issuer'], + 'clientId' => $config['client_id'], + 'clientSecret' => $config['client_secret'], + 'redirectUri' => url('/http/source.bookstackapp.com/oidc/callback'), 'authorizationEndpoint' => $config['authorization_endpoint'], - 'tokenEndpoint' => $config['token_endpoint'], + 'tokenEndpoint' => $config['token_endpoint'], ]); // Use keys if configured @@ -95,7 +103,11 @@ class OidcService // Run discovery if ($config['discover'] ?? false) { - $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15); + try { + $settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15); + } catch (OidcIssuerDiscoveryException $exception) { + throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage()); + } } $settings->validate(); @@ -108,14 +120,35 @@ class OidcService */ protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider { - return new OidcOAuthProvider($settings->arrayForProvider(), [ - 'httpClient' => $this->httpClient, + $provider = new OidcOAuthProvider($settings->arrayForProvider(), [ + 'httpClient' => $this->httpClient, 'optionProvider' => new HttpBasicAuthOptionProvider(), ]); + + foreach ($this->getAdditionalScopes() as $scope) { + $provider->addScope($scope); + } + + return $provider; } /** - * Calculate the display name + * Get any user-defined addition/custom scopes to apply to the authentication request. + * + * @return string[] + */ + protected function getAdditionalScopes(): array + { + $scopeConfig = $this->config()['additional_scopes'] ?: ''; + + $scopeArr = explode(',', $scopeConfig); + $scopeArr = array_map(fn (string $scope) => trim($scope), $scopeArr); + + return array_filter($scopeArr); + } + + /** + * Calculate the display name. */ protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string { @@ -136,26 +169,51 @@ class OidcService return implode(' ', $displayName); } + /** + * Extract the assigned groups from the id token. + * + * @return string[] + */ + protected function getUserGroups(OidcIdToken $token): array + { + $groupsAttr = $this->config()['groups_claim']; + if (empty($groupsAttr)) { + return []; + } + + $groupsList = Arr::get($token->getAllClaims(), $groupsAttr); + if (!is_array($groupsList)) { + return []; + } + + return array_values(array_filter($groupsList, function ($val) { + return is_string($val); + })); + } + /** * Extract the details of a user from an ID token. - * @return array{name: string, email: string, external_id: string} + * + * @return array{name: string, email: string, external_id: string, groups: string[]} */ protected function getUserDetails(OidcIdToken $token): array { $id = $token->getClaim('sub'); + return [ 'external_id' => $id, - 'email' => $token->getClaim('email'), - 'name' => $this->getUserDisplayName($token, $id), + 'email' => $token->getClaim('email'), + 'name' => $this->getUserDisplayName($token, $id), + 'groups' => $this->getUserGroups($token), ]; } /** * Processes a received access token for a user. Login the user when * they exist, optionally registering them automatically. - * @throws OpenIdConnectException + * + * @throws OidcException * @throws JsonDebugException - * @throws UserRegistrationException * @throws StoppedAuthenticationException */ protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User @@ -174,29 +232,38 @@ class OidcService try { $idToken->validate($settings->clientId); } catch (OidcInvalidTokenException $exception) { - throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}"); + throw new OidcException("ID token validate failed with error: {$exception->getMessage()}"); } $userDetails = $this->getUserDetails($idToken); $isLoggedIn = auth()->check(); if (empty($userDetails['email'])) { - throw new OpenIdConnectException(trans('errors.oidc_no_email_address')); + throw new OidcException(trans('errors.oidc_no_email_address')); } if ($isLoggedIn) { - throw new OpenIdConnectException(trans('errors.oidc_already_logged_in'), '/login'); + throw new OidcException(trans('errors.oidc_already_logged_in')); } - $user = $this->registrationService->findOrRegister( - $userDetails['name'], $userDetails['email'], $userDetails['external_id'] - ); + try { + $user = $this->registrationService->findOrRegister( + $userDetails['name'], + $userDetails['email'], + $userDetails['external_id'] + ); + } catch (UserRegistrationException $exception) { + throw new OidcException($exception->getMessage()); + } - if ($user === null) { - throw new OpenIdConnectException(trans('errors.oidc_user_not_registered', ['name' => $userDetails['external_id']]), '/login'); + if ($this->shouldSyncGroups()) { + $groups = $userDetails['groups']; + $detachExisting = $this->config()['remove_from_groups']; + $this->groupService->syncUserWithFoundGroups($user, $groups, $detachExisting); } $this->loginService->login($user, 'oidc'); + return $user; } @@ -207,4 +274,12 @@ class OidcService { return config('oidc'); } + + /** + * Check if groups should be synced. + */ + protected function shouldSyncGroups(): bool + { + return $this->config()['user_to_groups'] !== false; + } }