3 namespace BookStack\Access\Oidc;
5 use GuzzleHttp\Psr7\Request;
6 use Illuminate\Contracts\Cache\Repository;
7 use InvalidArgumentException;
8 use Psr\Http\Client\ClientExceptionInterface;
9 use Psr\Http\Client\ClientInterface;
12 * OpenIdConnectProviderSettings
13 * Acts as a DTO for settings used within the oidc request and token handling.
14 * Performs auto-discovery upon request.
16 class OidcProviderSettings
18 public string $issuer;
19 public string $clientId;
20 public string $clientSecret;
21 public ?string $redirectUri;
22 public ?string $authorizationEndpoint;
23 public ?string $tokenEndpoint;
26 * @var string[]|array[]
28 public ?array $keys = [];
30 public function __construct(array $settings)
32 $this->applySettingsFromArray($settings);
33 $this->validateInitial();
37 * Apply an array of settings to populate setting properties within this class.
39 protected function applySettingsFromArray(array $settingsArray)
41 foreach ($settingsArray as $key => $value) {
42 if (property_exists($this, $key)) {
49 * Validate any core, required properties have been set.
51 * @throws InvalidArgumentException
53 protected function validateInitial()
55 $required = ['clientId', 'clientSecret', 'redirectUri', 'issuer'];
56 foreach ($required as $prop) {
57 if (empty($this->$prop)) {
58 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
62 if (strpos($this->issuer, 'https://') !== 0) {
63 throw new InvalidArgumentException('Issuer value must start with https://');
68 * Perform a full validation on these settings.
70 * @throws InvalidArgumentException
72 public function validate(): void
74 $this->validateInitial();
75 $required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];
76 foreach ($required as $prop) {
77 if (empty($this->$prop)) {
78 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
84 * Discover and autoload settings from the configured issuer.
86 * @throws OidcIssuerDiscoveryException
88 public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
91 $cacheKey = 'oidc-discovery::' . $this->issuer;
92 $discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function () use ($httpClient) {
93 return $this->loadSettingsFromIssuerDiscovery($httpClient);
95 $this->applySettingsFromArray($discoveredSettings);
96 } catch (ClientExceptionInterface $exception) {
97 throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
102 * @throws OidcIssuerDiscoveryException
103 * @throws ClientExceptionInterface
105 protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
107 $issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration';
108 $request = new Request('GET', $issuerUrl);
109 $response = $httpClient->sendRequest($request);
110 $result = json_decode($response->getBody()->getContents(), true);
112 if (empty($result) || !is_array($result)) {
113 throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
116 if ($result['issuer'] !== $this->issuer) {
117 throw new OidcIssuerDiscoveryException('Unexpected issuer value found on discovery response');
120 $discoveredSettings = [];
122 if (!empty($result['authorization_endpoint'])) {
123 $discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint'];
126 if (!empty($result['token_endpoint'])) {
127 $discoveredSettings['tokenEndpoint'] = $result['token_endpoint'];
130 if (!empty($result['jwks_uri'])) {
131 $keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient);
132 $discoveredSettings['keys'] = $this->filterKeys($keys);
135 return $discoveredSettings;
139 * Filter the given JWK keys down to just those we support.
141 protected function filterKeys(array $keys): array
143 return array_filter($keys, function (array $key) {
144 $alg = $key['alg'] ?? 'RS256';
145 $use = $key['use'] ?? 'sig';
147 return $key['kty'] === 'RSA' && $use === 'sig' && $alg === 'RS256';
152 * Return an array of jwks as PHP key=>value arrays.
154 * @throws ClientExceptionInterface
155 * @throws OidcIssuerDiscoveryException
157 protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
159 $request = new Request('GET', $uri);
160 $response = $httpClient->sendRequest($request);
161 $result = json_decode($response->getBody()->getContents(), true);
163 if (empty($result) || !is_array($result) || !isset($result['keys'])) {
164 throw new OidcIssuerDiscoveryException('Error reading keys from issuer jwks_uri');
167 return $result['keys'];
171 * Get the settings needed by an OAuth provider, as a key=>value array.
173 public function arrayForProvider(): array
175 $settingKeys = ['clientId', 'clientSecret', 'redirectUri', 'authorizationEndpoint', 'tokenEndpoint'];
177 foreach ($settingKeys as $setting) {
178 $settings[$setting] = $this->$setting;