3 namespace BookStack\Auth\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
41 public $authorizationEndpoint;
46 public $tokenEndpoint;
49 * @var string[]|array[]
53 public function __construct(array $settings)
55 $this->applySettingsFromArray($settings);
56 $this->validateInitial();
60 * Apply an array of settings to populate setting properties within this class.
62 protected function applySettingsFromArray(array $settingsArray)
64 foreach ($settingsArray as $key => $value) {
65 if (property_exists($this, $key)) {
72 * Validate any core, required properties have been set.
74 * @throws InvalidArgumentException
76 protected function validateInitial()
78 $required = ['clientId', 'clientSecret', 'redirectUri', 'issuer'];
79 foreach ($required as $prop) {
80 if (empty($this->$prop)) {
81 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
85 if (strpos($this->issuer, 'https://') !== 0) {
86 throw new InvalidArgumentException('Issuer value must start with https://');
91 * Perform a full validation on these settings.
93 * @throws InvalidArgumentException
95 public function validate(): void
97 $this->validateInitial();
98 $required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];
99 foreach ($required as $prop) {
100 if (empty($this->$prop)) {
101 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
107 * Discover and autoload settings from the configured issuer.
109 * @throws OidcIssuerDiscoveryException
111 public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
114 $cacheKey = 'oidc-discovery::' . $this->issuer;
115 $discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function () use ($httpClient) {
116 return $this->loadSettingsFromIssuerDiscovery($httpClient);
118 $this->applySettingsFromArray($discoveredSettings);
119 } catch (ClientExceptionInterface $exception) {
120 throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
125 * @throws OidcIssuerDiscoveryException
126 * @throws ClientExceptionInterface
128 protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
130 $issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration';
131 $request = new Request('GET', $issuerUrl);
132 $response = $httpClient->sendRequest($request);
133 $result = json_decode($response->getBody()->getContents(), true);
135 if (empty($result) || !is_array($result)) {
136 throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
139 if ($result['issuer'] !== $this->issuer) {
140 throw new OidcIssuerDiscoveryException('Unexpected issuer value found on discovery response');
143 $discoveredSettings = [];
145 if (!empty($result['authorization_endpoint'])) {
146 $discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint'];
149 if (!empty($result['token_endpoint'])) {
150 $discoveredSettings['tokenEndpoint'] = $result['token_endpoint'];
153 if (!empty($result['jwks_uri'])) {
154 $keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient);
155 $discoveredSettings['keys'] = $this->filterKeys($keys);
158 return $discoveredSettings;
162 * Filter the given JWK keys down to just those we support.
164 protected function filterKeys(array $keys): array
166 return array_filter($keys, function (array $key) {
167 $alg = $key['alg'] ?? null;
169 return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256');
174 * Return an array of jwks as PHP key=>value arrays.
176 * @throws ClientExceptionInterface
177 * @throws OidcIssuerDiscoveryException
179 protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
181 $request = new Request('GET', $uri);
182 $response = $httpClient->sendRequest($request);
183 $result = json_decode($response->getBody()->getContents(), true);
185 if (empty($result) || !is_array($result) || !isset($result['keys'])) {
186 throw new OidcIssuerDiscoveryException('Error reading keys from issuer jwks_uri');
189 return $result['keys'];
193 * Get the settings needed by an OAuth provider, as a key=>value array.
195 public function arrayForProvider(): array
197 $settingKeys = ['clientId', 'clientSecret', 'redirectUri', 'authorizationEndpoint', 'tokenEndpoint'];
199 foreach ($settingKeys as $setting) {
200 $settings[$setting] = $this->$setting;