1 <?php namespace BookStack\Auth\Access;
3 use BookStack\Auth\User;
4 use BookStack\Exceptions\JsonDebugException;
5 use BookStack\Exceptions\OpenIdException;
6 use BookStack\Exceptions\UserRegistrationException;
8 use Lcobucci\JWT\Token;
9 use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
10 use OpenIDConnectClient\AccessToken;
11 use OpenIDConnectClient\Exception\InvalidTokenException;
12 use OpenIDConnectClient\OpenIDConnectProvider;
16 * Handles any app-specific OpenId tasks.
18 class OpenIdService extends ExternalAuthService
23 * OpenIdService constructor.
25 public function __construct(RegistrationService $registrationService, User $user)
27 parent::__construct($registrationService, $user);
29 $this->config = config('openid');
33 * Initiate a authorization flow.
36 public function login(): array
38 $provider = $this->getProvider();
40 'url' => $provider->getAuthorizationUrl(),
41 'state' => $provider->getState(),
46 * Initiate a logout flow.
49 public function logout(): array
51 $this->actionLogout();
55 return ['url' => $url, 'id' => $id];
59 * Refresh the currently logged in user.
62 public function refresh(): bool
64 // Retrieve access token for current session
65 $json = session()->get('openid_token');
66 $accessToken = new AccessToken(json_decode($json, true));
68 // Check if both the access token and the ID token (if present) are unexpired
69 $idToken = $accessToken->getIdToken();
70 if (!$accessToken->hasExpired() && (!$idToken || !$idToken->isExpired())) {
74 // If no refresh token available, logout
75 if ($accessToken->getRefreshToken() === null) {
76 $this->actionLogout();
80 // ID token or access token is expired, we refresh it using the refresh token
82 $provider = $this->getProvider();
84 $accessToken = $provider->getAccessToken('refresh_token', [
85 'refresh_token' => $accessToken->getRefreshToken(),
87 } catch (IdentityProviderException $e) {
88 // Refreshing failed, logout
89 $this->actionLogout();
91 } catch (\Exception $e) {
92 // Unknown error, logout and throw
93 $this->actionLogout();
97 // A valid token was obtained, we update the access token
98 session()->put('openid_token', json_encode($accessToken));
104 * Process the Authorization response from the authorization server and
105 * return the matching, or new if registration active, user matched to
106 * the authorization server.
107 * Returns null if not authenticated.
109 * @throws OpenIdException
110 * @throws ValidationError
111 * @throws JsonDebugException
112 * @throws UserRegistrationException
114 public function processAuthorizeResponse(?string $authorizationCode): ?User
116 $provider = $this->getProvider();
118 // Try to exchange authorization code for access token
119 $accessToken = $provider->getAccessToken('authorization_code', [
120 'code' => $authorizationCode,
123 return $this->processAccessTokenCallback($accessToken);
127 * Do the required actions to log a user out.
129 protected function actionLogout()
132 session()->invalidate();
136 * Load the underlying OpenID Connect Provider.
140 protected function getProvider(): OpenIDConnectProvider
143 $settings = $this->config['openid'];
144 $overrides = $this->config['openid_overrides'] ?? [];
146 if ($overrides && is_string($overrides)) {
147 $overrides = json_decode($overrides, true);
150 $openIdSettings = $this->loadOpenIdDetails();
151 $settings = array_replace_recursive($settings, $openIdSettings, $overrides);
154 $services = $this->loadOpenIdServices();
155 $overrides = $this->config['openid_services'] ?? [];
157 $services = array_replace_recursive($services, $overrides);
159 return new OpenIDConnectProvider($settings, $services);
163 * Load services utilized by the OpenID Connect provider.
165 protected function loadOpenIdServices(): array
168 'signer' => new \Lcobucci\JWT\Signer\Rsa\Sha256(),
173 * Load dynamic service provider options required by the OpenID Connect provider.
175 protected function loadOpenIdDetails(): array
178 'redirectUri' => url('/openid/redirect'),
183 * Calculate the display name
185 protected function getUserDisplayName(Token $token, string $defaultValue): string
187 $displayNameAttr = $this->config['display_name_attributes'];
190 foreach ($displayNameAttr as $dnAttr) {
191 $dnComponent = $token->getClaim($dnAttr, '');
192 if ($dnComponent !== '') {
193 $displayName[] = $dnComponent;
197 if (count($displayName) == 0) {
198 $displayName = $defaultValue;
200 $displayName = implode(' ', $displayName);
207 * Get the value to use as the external id saved in BookStack
208 * used to link the user to an existing BookStack DB user.
210 protected function getExternalId(Token $token, string $defaultValue)
212 $userNameAttr = $this->config['external_id_attribute'];
213 if ($userNameAttr === null) {
214 return $defaultValue;
217 return $token->getClaim($userNameAttr, $defaultValue);
221 * Extract the details of a user from an ID token.
223 protected function getUserDetails(Token $token): array
226 $emailAttr = $this->config['email_attribute'];
227 if ($token->hasClaim($emailAttr)) {
228 $email = $token->getClaim($emailAttr);
232 'external_id' => $token->getClaim('sub'),
234 'name' => $this->getUserDisplayName($token, $email),
239 * Processes a received access token for a user. Login the user when
240 * they exist, optionally registering them automatically.
241 * @throws OpenIdException
242 * @throws JsonDebugException
243 * @throws UserRegistrationException
245 public function processAccessTokenCallback(AccessToken $accessToken): User
247 $userDetails = $this->getUserDetails($accessToken->getIdToken());
248 $isLoggedIn = auth()->check();
250 if ($this->config['dump_user_details']) {
251 throw new JsonDebugException($accessToken->jsonSerialize());
254 if ($userDetails['email'] === null) {
255 throw new OpenIdException(trans('errors.openid_no_email_address'));
259 throw new OpenIdException(trans('errors.openid_already_logged_in'), '/login');
262 $user = $this->getOrRegisterUser($userDetails);
263 if ($user === null) {
264 throw new OpenIdException(trans('errors.openid_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
267 auth()->login($user);
268 session()->put('openid_token', json_encode($accessToken));