]> BookStack Code Mirror - bookstack/blob - app/Auth/Access/OpenIdService.php
084adfb13aa4e1cb733479b0a9a06c66bca26ffd
[bookstack] / app / Auth / Access / OpenIdService.php
1 <?php namespace BookStack\Auth\Access;
2
3 use BookStack\Auth\User;
4 use BookStack\Exceptions\JsonDebugException;
5 use BookStack\Exceptions\OpenIdException;
6 use BookStack\Exceptions\UserRegistrationException;
7 use Exception;
8 use Lcobucci\JWT\Token;
9 use OpenIDConnectClient\AccessToken;
10 use OpenIDConnectClient\OpenIDConnectProvider;
11
12 /**
13  * Class OpenIdService
14  * Handles any app-specific OpenId tasks.
15  */
16 class OpenIdService extends ExternalAuthService
17 {
18     protected $config;
19
20     /**
21      * OpenIdService constructor.
22      */
23     public function __construct(RegistrationService $registrationService, User $user)
24     {
25         parent::__construct($registrationService, $user);
26         
27         $this->config = config('openid');
28     }
29
30     /**
31      * Initiate a authorization flow.
32      * @throws Error
33      */
34     public function login(): array
35     {
36         $provider = $this->getProvider();
37         return [
38             'url' => $provider->getAuthorizationUrl(),
39             'state' => $provider->getState(),
40         ];
41     }
42
43     /**
44      * Initiate a logout flow.
45      * @throws Error
46      */
47     public function logout(): array
48     {
49         $this->actionLogout();
50         $url = '/';
51         $id = null;
52
53         return ['url' => $url, 'id' => $id];
54     }
55
56     /**
57      * Process the Authorization response from the authorization server and
58      * return the matching, or new if registration active, user matched to
59      * the authorization server.
60      * Returns null if not authenticated.
61      * @throws Error
62      * @throws OpenIdException
63      * @throws ValidationError
64      * @throws JsonDebugException
65      * @throws UserRegistrationException
66      */
67     public function processAuthorizeResponse(?string $authorizationCode): ?User
68     {
69         $provider = $this->getProvider();
70
71         // Try to exchange authorization code for access token
72         $accessToken = $provider->getAccessToken('authorization_code', [
73             'code' => $authorizationCode,
74         ]);
75
76         return $this->processAccessTokenCallback($accessToken);
77     }
78
79     /**
80      * Do the required actions to log a user out.
81      */
82     protected function actionLogout()
83     {
84         auth()->logout();
85         session()->invalidate();
86     }
87
88     /**
89      * Load the underlying Onelogin SAML2 toolkit.
90      * @throws Error
91      * @throws Exception
92      */
93     protected function getProvider(): OpenIDConnectProvider
94     {
95         $settings = $this->config['openid'];
96         $overrides = $this->config['openid_overrides'] ?? [];
97
98         if ($overrides && is_string($overrides)) {
99             $overrides = json_decode($overrides, true);
100         }
101
102         $openIdSettings = $this->loadOpenIdDetails();
103         $settings = array_replace_recursive($settings, $openIdSettings, $overrides);
104
105         $signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
106         return new OpenIDConnectProvider($settings, ['signer' => $signer]);
107     }
108
109     /**
110      * Load dynamic service provider options required by the onelogin toolkit.
111      */
112     protected function loadOpenIdDetails(): array
113     {
114         return [
115             'redirectUri' => url('/openid/redirect'),
116         ];
117     }
118
119     /**
120      * Calculate the display name
121      */
122     protected function getUserDisplayName(Token $token, string $defaultValue): string
123     {
124         $displayNameAttr = $this->config['display_name_attributes'];
125
126         $displayName = [];
127         foreach ($displayNameAttr as $dnAttr) {
128             $dnComponent = $token->getClaim($dnAttr, '');
129             if ($dnComponent !== '') {
130                 $displayName[] = $dnComponent;
131             }
132         }
133
134         if (count($displayName) == 0) {
135             $displayName = $defaultValue;
136         } else {
137             $displayName = implode(' ', $displayName);
138         }
139
140         return $displayName;
141     }
142
143     /**
144      * Get the value to use as the external id saved in BookStack
145      * used to link the user to an existing BookStack DB user.
146      */
147     protected function getExternalId(Token $token, string $defaultValue)
148     {
149         $userNameAttr = $this->config['external_id_attribute'];
150         if ($userNameAttr === null) {
151             return $defaultValue;
152         }
153
154         return $token->getClaim($userNameAttr, $defaultValue);
155     }
156
157     /**
158      * Extract the details of a user from a SAML response.
159      */
160     protected function getUserDetails(Token $token): array
161     {
162         $email = null;
163         $emailAttr = $this->config['email_attribute'];
164         if ($token->hasClaim($emailAttr)) {
165             $email = $token->getClaim($emailAttr);
166         }
167
168         return [
169             'external_id' => $token->getClaim('sub'),
170             'email' => $email,
171             'name' => $this->getUserDisplayName($token, $email),
172         ];
173     }
174
175     /**
176      * Processes a received access token for a user. Login the user when
177      * they exist, optionally registering them automatically.
178      * @throws OpenIdException
179      * @throws JsonDebugException
180      * @throws UserRegistrationException
181      */
182     public function processAccessTokenCallback(AccessToken $accessToken): User
183     {
184         $userDetails = $this->getUserDetails($accessToken->getIdToken());
185         $isLoggedIn = auth()->check();
186
187         if ($this->config['dump_user_details']) {
188             throw new JsonDebugException($accessToken->jsonSerialize());
189         }
190
191         if ($userDetails['email'] === null) {
192             throw new OpenIdException(trans('errors.openid_no_email_address'));
193         }
194
195         if ($isLoggedIn) {
196             throw new OpenIdException(trans('errors.openid_already_logged_in'), '/login');
197         }
198
199         $user = $this->getOrRegisterUser($userDetails);
200         if ($user === null) {
201             throw new OpenIdException(trans('errors.openid_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
202         }
203
204         auth()->login($user);
205         return $user;
206     }
207 }