| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  | namespace BookStack\Auth\Access\Oidc; | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | use GuzzleHttp\Psr7\Request; | 
					
						
							|  |  |  | use Illuminate\Contracts\Cache\Repository; | 
					
						
							|  |  |  | use InvalidArgumentException; | 
					
						
							|  |  |  | use Psr\Http\Client\ClientExceptionInterface; | 
					
						
							|  |  |  | use Psr\Http\Client\ClientInterface; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * OpenIdConnectProviderSettings | 
					
						
							|  |  |  |  * Acts as a DTO for settings used within the oidc request and token handling. | 
					
						
							|  |  |  |  * Performs auto-discovery upon request. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  | class OidcProviderSettings | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  | { | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $issuer; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $clientId; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $clientSecret; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $redirectUri; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $authorizationEndpoint; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $tokenEndpoint; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * @var string[]|array[] | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public $keys = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     public function __construct(array $settings) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->applySettingsFromArray($settings); | 
					
						
							|  |  |  |         $this->validateInitial(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Apply an array of settings to populate setting properties within this class. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function applySettingsFromArray(array $settingsArray) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         foreach ($settingsArray as $key => $value) { | 
					
						
							|  |  |  |             if (property_exists($this, $key)) { | 
					
						
							|  |  |  |                 $this->$key = $value; | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Validate any core, required properties have been set. | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      * @throws InvalidArgumentException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function validateInitial() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $required = ['clientId', 'clientSecret', 'redirectUri', 'issuer']; | 
					
						
							|  |  |  |         foreach ($required as $prop) { | 
					
						
							|  |  |  |             if (empty($this->$prop)) { | 
					
						
							|  |  |  |                 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (strpos($this->issuer, 'https://') !== 0) { | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |             throw new InvalidArgumentException('Issuer value must start with https://'); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Perform a full validation on these settings. | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      * @throws InvalidArgumentException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function validate(): void | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $this->validateInitial(); | 
					
						
							|  |  |  |         $required = ['keys', 'tokenEndpoint', 'authorizationEndpoint']; | 
					
						
							|  |  |  |         foreach ($required as $prop) { | 
					
						
							|  |  |  |             if (empty($this->$prop)) { | 
					
						
							|  |  |  |                 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value"); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Discover and autoload settings from the configured issuer. | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  |      * @throws OidcIssuerDiscoveryException | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |             $cacheKey = 'oidc-discovery::' . $this->issuer; | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |             $discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function () use ($httpClient) { | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |                 return $this->loadSettingsFromIssuerDiscovery($httpClient); | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |             $this->applySettingsFromArray($discoveredSettings); | 
					
						
							|  |  |  |         } catch (ClientExceptionInterface $exception) { | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  |             throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}"); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  |      * @throws OidcIssuerDiscoveryException | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      * @throws ClientExceptionInterface | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration'; | 
					
						
							|  |  |  |         $request = new Request('GET', $issuerUrl); | 
					
						
							|  |  |  |         $response = $httpClient->sendRequest($request); | 
					
						
							|  |  |  |         $result = json_decode($response->getBody()->getContents(), true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (empty($result) || !is_array($result)) { | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  |             throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}"); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if ($result['issuer'] !== $this->issuer) { | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |             throw new OidcIssuerDiscoveryException('Unexpected issuer value found on discovery response'); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $discoveredSettings = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!empty($result['authorization_endpoint'])) { | 
					
						
							|  |  |  |             $discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint']; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!empty($result['token_endpoint'])) { | 
					
						
							|  |  |  |             $discoveredSettings['tokenEndpoint'] = $result['token_endpoint']; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (!empty($result['jwks_uri'])) { | 
					
						
							|  |  |  |             $keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient); | 
					
						
							| 
									
										
										
										
											2021-10-14 20:37:55 +08:00
										 |  |  |             $discoveredSettings['keys'] = $this->filterKeys($keys); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $discoveredSettings; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Filter the given JWK keys down to just those we support. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function filterKeys(array $keys): array | 
					
						
							|  |  |  |     { | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |         return array_filter($keys, function (array $key) { | 
					
						
							| 
									
										
										
										
											2022-01-28 22:00:55 +08:00
										 |  |  |             $alg = $key['alg'] ?? null; | 
					
						
							| 
									
										
										
										
											2022-01-31 00:44:19 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-01-28 22:00:55 +08:00
										 |  |  |             return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256'); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Return an array of jwks as PHP key=>value arrays. | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      * @throws ClientExceptionInterface | 
					
						
							| 
									
										
										
										
											2021-10-13 06:04:28 +08:00
										 |  |  |      * @throws OidcIssuerDiscoveryException | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $request = new Request('GET', $uri); | 
					
						
							|  |  |  |         $response = $httpClient->sendRequest($request); | 
					
						
							|  |  |  |         $result = json_decode($response->getBody()->getContents(), true); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (empty($result) || !is_array($result) || !isset($result['keys'])) { | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  |             throw new OidcIssuerDiscoveryException('Error reading keys from issuer jwks_uri'); | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $result['keys']; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the settings needed by an OAuth provider, as a key=>value array. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public function arrayForProvider(): array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $settingKeys = ['clientId', 'clientSecret', 'redirectUri', 'authorizationEndpoint', 'tokenEndpoint']; | 
					
						
							|  |  |  |         $settings = []; | 
					
						
							|  |  |  |         foreach ($settingKeys as $setting) { | 
					
						
							|  |  |  |             $settings[$setting] = $this->$setting; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-10-13 06:00:52 +08:00
										 |  |  |         return $settings; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-10-16 23:01:59 +08:00
										 |  |  | } |