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 Illuminate\Support\Str;
9 use Lcobucci\JWT\Token;
10 use OpenIDConnectClient\AccessToken;
11 use OpenIDConnectClient\OpenIDConnectProvider;
15 * Handles any app-specific OpenId tasks.
17 class OpenIdService extends ExternalAuthService
20 protected $registrationService;
24 * OpenIdService constructor.
26 public function __construct(RegistrationService $registrationService, User $user)
28 $this->config = config('openid');
29 $this->registrationService = $registrationService;
34 * Initiate a authorization flow.
37 public function login(): array
39 $provider = $this->getProvider();
41 'url' => $provider->getAuthorizationUrl(),
42 'state' => $provider->getState(),
47 * Initiate a logout flow.
50 public function logout(): array
52 $this->actionLogout();
56 return ['url' => $url, 'id' => $id];
60 * Process the Authorization response from the authorization server and
61 * return the matching, or new if registration active, user matched to
62 * the authorization server.
63 * Returns null if not authenticated.
65 * @throws OpenIdException
66 * @throws ValidationError
67 * @throws JsonDebugException
68 * @throws UserRegistrationException
70 public function processAuthorizeResponse(?string $authorizationCode): ?User
72 $provider = $this->getProvider();
74 // Try to exchange authorization code for access token
75 $accessToken = $provider->getAccessToken('authorization_code', [
76 'code' => $authorizationCode,
79 return $this->processAccessTokenCallback($accessToken);
83 * Do the required actions to log a user out.
85 protected function actionLogout()
88 session()->invalidate();
92 * Load the underlying Onelogin SAML2 toolkit.
96 protected function getProvider(): OpenIDConnectProvider
98 $settings = $this->config['openid'];
99 $overrides = $this->config['openid_overrides'] ?? [];
101 if ($overrides && is_string($overrides)) {
102 $overrides = json_decode($overrides, true);
105 $openIdSettings = $this->loadOpenIdDetails();
106 $settings = array_replace_recursive($settings, $openIdSettings, $overrides);
108 $signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
109 return new OpenIDConnectProvider($settings, ['signer' => $signer]);
113 * Load dynamic service provider options required by the onelogin toolkit.
115 protected function loadOpenIdDetails(): array
118 'redirectUri' => url('/openid/redirect'),
123 * Calculate the display name
125 protected function getUserDisplayName(Token $token, string $defaultValue): string
127 $displayNameAttr = $this->config['display_name_attributes'];
130 foreach ($displayNameAttr as $dnAttr) {
131 $dnComponent = $token->getClaim($dnAttr, '');
132 if ($dnComponent !== '') {
133 $displayName[] = $dnComponent;
137 if (count($displayName) == 0) {
138 $displayName = $defaultValue;
140 $displayName = implode(' ', $displayName);
147 * Get the value to use as the external id saved in BookStack
148 * used to link the user to an existing BookStack DB user.
150 protected function getExternalId(Token $token, string $defaultValue)
152 $userNameAttr = $this->config['external_id_attribute'];
153 if ($userNameAttr === null) {
154 return $defaultValue;
157 return $token->getClaim($userNameAttr, $defaultValue);
161 * Extract the details of a user from a SAML response.
163 protected function getUserDetails(Token $token): array
166 $emailAttr = $this->config['email_attribute'];
167 if ($token->hasClaim($emailAttr)) {
168 $email = $token->getClaim($emailAttr);
172 'external_id' => $token->getClaim('sub'),
174 'name' => $this->getUserDisplayName($token, $email),
179 * Get the user from the database for the specified details.
180 * @throws OpenIdException
181 * @throws UserRegistrationException
183 protected function getOrRegisterUser(array $userDetails): ?User
185 $user = $this->user->newQuery()
186 ->where('external_auth_id', '=', $userDetails['external_id'])
189 if (is_null($user)) {
191 'name' => $userDetails['name'],
192 'email' => $userDetails['email'],
193 'password' => Str::random(32),
194 'external_auth_id' => $userDetails['external_id'],
197 $user = $this->registrationService->registerUser($userData, null, false);
204 * Processes a received access token for a user. Login the user when
205 * they exist, optionally registering them automatically.
206 * @throws OpenIdException
207 * @throws JsonDebugException
208 * @throws UserRegistrationException
210 public function processAccessTokenCallback(AccessToken $accessToken): User
212 $userDetails = $this->getUserDetails($accessToken->getIdToken());
213 $isLoggedIn = auth()->check();
215 if ($this->config['dump_user_details']) {
216 throw new JsonDebugException($accessToken->jsonSerialize());
219 if ($userDetails['email'] === null) {
220 throw new OpenIdException(trans('errors.openid_no_email_address'));
224 throw new OpenIdException(trans('errors.openid_already_logged_in'), '/login');
227 $user = $this->getOrRegisterUser($userDetails);
228 if ($user === null) {
229 throw new OpenIdException(trans('errors.openid_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
232 auth()->login($user);