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;
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpRequestService $http,
- protected GroupSyncService $groupService
+ protected GroupSyncService $groupService,
+ protected UserAvatars $userAvatars
) {
}
/**
* Initiate an authorization flow.
+ * Provides back an authorize redirect URL, in addition to other
+ * details which may be required for the auth flow.
*
* @throws OidcException
*
{
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
+
+ $url = $provider->getAuthorizationUrl();
+ session()->put('oidc_pkce_code', $provider->getPkceCode() ?? '');
+
return [
- 'url' => $provider->getAuthorizationUrl(),
+ 'url' => $url,
'state' => $provider->getState(),
];
}
$settings = $this->getProviderSettings();
$provider = $this->getProvider($settings);
+ // Set PKCE code flashed at login
+ $pkceCode = session()->pull('oidc_pkce_code', '');
+ $provider->setPkceCode($pkceCode);
+
// Try to exchange authorization code for access token
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $authorizationCode,
'issuer' => $config['issuer'],
'clientId' => $config['client_id'],
'clientSecret' => $config['client_secret'],
- 'redirectUri' => url('/oidc/callback'),
'authorizationEndpoint' => $config['authorization_endpoint'],
'tokenEndpoint' => $config['token_endpoint'],
- 'endSessionEndpoint' => $config['end_session_endpoint'],
+ 'endSessionEndpoint' => is_string($config['end_session_endpoint']) ? $config['end_session_endpoint'] : null,
+ 'userinfoEndpoint' => $config['userinfo_endpoint'],
]);
// Use keys if configured
}
// Prevent use of RP-initiated logout if specifically disabled
+ // Or force use of a URL if specifically set.
if ($config['end_session_endpoint'] === false) {
$settings->endSessionEndpoint = null;
+ } else if (is_string($config['end_session_endpoint'])) {
+ $settings->endSessionEndpoint = $config['end_session_endpoint'];
}
$settings->validate();
*/
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
{
- $provider = new OidcOAuthProvider($settings->arrayForProvider(), [
+ $provider = new OidcOAuthProvider([
+ ...$settings->arrayForOAuthProvider(),
+ 'redirectUri' => url('/oidc/callback'),
+ ], [
'httpClient' => $this->http->buildClient(5),
'optionProvider' => new HttpBasicAuthOptionProvider(),
]);
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.
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');
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.
*/
'post_logout_redirect_uri' => $defaultLogoutUrl,
];
- return $oidcSettings->endSessionEndpoint . '?' . http_build_query($endpointParams);
+ $joiner = str_contains($oidcSettings->endSessionEndpoint, '?') ? '&' : '?';
+
+ return $oidcSettings->endSessionEndpoint . $joiner . http_build_query($endpointParams);
}
}