X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/d640411adb4d828cffefd1248407eb93db2eaee2..refs/pull/5721/head:/app/Access/Oidc/OidcService.php diff --git a/app/Access/Oidc/OidcService.php b/app/Access/Oidc/OidcService.php index 00ac2b6dc..d6f6ef156 100644 --- a/app/Access/Oidc/OidcService.php +++ b/app/Access/Oidc/OidcService.php @@ -11,8 +11,8 @@ use BookStack\Exceptions\UserRegistrationException; use BookStack\Facades\Theme; use BookStack\Http\HttpRequestService; use BookStack\Theming\ThemeEvents; +use BookStack\Uploads\UserAvatars; use BookStack\Users\Models\User; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Cache; use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; @@ -27,7 +27,8 @@ class OidcService protected RegistrationService $registrationService, protected LoginService $loginService, protected HttpRequestService $http, - protected GroupSyncService $groupService + protected GroupSyncService $groupService, + protected UserAvatars $userAvatars ) { } @@ -159,69 +160,6 @@ class OidcService return array_filter($scopeArr); } - /** - * Calculate the display name. - */ - protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string - { - $displayNameAttrString = $this->config()['display_name_claims'] ?? ''; - $displayNameAttrs = explode('|', $displayNameAttrString); - - $displayName = []; - foreach ($displayNameAttrs as $dnAttr) { - $dnComponent = $token->getClaim($dnAttr) ?? ''; - if ($dnComponent !== '') { - $displayName[] = $dnComponent; - } - } - - if (count($displayName) == 0) { - $displayName[] = $defaultValue; - } - - 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, groups: string[]} - */ - protected function getUserDetails(OidcIdToken $token): array - { - $idClaim = $this->config()['external_id_claim']; - $id = $token->getClaim($idClaim); - - return [ - 'external_id' => $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. @@ -241,17 +179,6 @@ class OidcService session()->put("oidc_id_token", $idTokenText); - if (!empty($settings->userinfoEndpoint)) { - $provider = $this->getProvider($settings); - $request = $provider->getAuthenticatedRequest('GET', $settings->userinfoEndpoint, $accessToken->getToken()); - $response = $provider->getParsedResponse($request); - $claims = $idToken->getAllClaims(); - foreach ($response as $key => $value) { - $claims[$key] = $value; - } - $idToken->replaceClaims($claims); - } - $returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [ 'access_token' => $accessToken->getToken(), 'expires_in' => $accessToken->getExpires(), @@ -269,34 +196,39 @@ class OidcService try { $idToken->validate($settings->clientId); } catch (OidcInvalidTokenException $exception) { - throw new OidcException("ID token validate failed with error: {$exception->getMessage()}"); + throw new OidcException("ID token validation failed with error: {$exception->getMessage()}"); } - $userDetails = $this->getUserDetails($idToken); - $isLoggedIn = auth()->check(); - - if (empty($userDetails['email'])) { + $userDetails = $this->getUserDetailsFromToken($idToken, $accessToken, $settings); + if (empty($userDetails->email)) { throw new OidcException(trans('errors.oidc_no_email_address')); } + if (empty($userDetails->name)) { + $userDetails->name = $userDetails->externalId; + } + $isLoggedIn = auth()->check(); if ($isLoggedIn) { throw new OidcException(trans('errors.oidc_already_logged_in')); } try { $user = $this->registrationService->findOrRegister( - $userDetails['name'], - $userDetails['email'], - $userDetails['external_id'] + $userDetails->name, + $userDetails->email, + $userDetails->externalId ); } catch (UserRegistrationException $exception) { throw new OidcException($exception->getMessage()); } + if ($this->config()['fetch_avatar'] && !$user->avatar()->exists() && $userDetails->picture) { + $this->userAvatars->assignToUserFromUrl($user, $userDetails->picture); + } + if ($this->shouldSyncGroups()) { - $groups = $userDetails['groups']; $detachExisting = $this->config()['remove_from_groups']; - $this->groupService->syncUserWithFoundGroups($user, $groups, $detachExisting); + $this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting); } $this->loginService->login($user, 'oidc'); @@ -304,6 +236,45 @@ class OidcService return $user; } + /** + * @throws OidcException + */ + protected function getUserDetailsFromToken(OidcIdToken $idToken, OidcAccessToken $accessToken, OidcProviderSettings $settings): OidcUserDetails + { + $userDetails = new OidcUserDetails(); + $userDetails->populate( + $idToken, + $this->config()['external_id_claim'], + $this->config()['display_name_claims'] ?? '', + $this->config()['groups_claim'] ?? '' + ); + + if (!$userDetails->isFullyPopulated($this->shouldSyncGroups()) && !empty($settings->userinfoEndpoint)) { + $provider = $this->getProvider($settings); + $request = $provider->getAuthenticatedRequest('GET', $settings->userinfoEndpoint, $accessToken->getToken()); + $response = new OidcUserinfoResponse( + $provider->getResponse($request), + $settings->issuer, + $settings->keys, + ); + + try { + $response->validate($idToken->getClaim('sub'), $settings->clientId); + } catch (OidcInvalidTokenException $exception) { + throw new OidcException("Userinfo endpoint response validation failed with error: {$exception->getMessage()}"); + } + + $userDetails->populate( + $response, + $this->config()['external_id_claim'], + $this->config()['display_name_claims'] ?? '', + $this->config()['groups_claim'] ?? '' + ); + } + + return $userDetails; + } + /** * Get the OIDC config from the application. */