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.
73 * @throws InvalidArgumentException
75 protected function validateInitial()
77 $required = ['clientId', 'clientSecret', 'redirectUri', 'issuer'];
78 foreach ($required as $prop) {
79 if (empty($this->$prop)) {
80 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
84 if (strpos($this->issuer, 'https://') !== 0) {
85 throw new InvalidArgumentException("Issuer value must start with https://");
90 * Perform a full validation on these settings.
91 * @throws InvalidArgumentException
93 public function validate(): void
95 $this->validateInitial();
96 $required = ['keys', 'tokenEndpoint', 'authorizationEndpoint'];
97 foreach ($required as $prop) {
98 if (empty($this->$prop)) {
99 throw new InvalidArgumentException("Missing required configuration \"{$prop}\" value");
105 * Discover and autoload settings from the configured issuer.
106 * @throws OidcIssuerDiscoveryException
108 public function discoverFromIssuer(ClientInterface $httpClient, Repository $cache, int $cacheMinutes)
111 $cacheKey = 'oidc-discovery::' . $this->issuer;
112 $discoveredSettings = $cache->remember($cacheKey, $cacheMinutes * 60, function() use ($httpClient) {
113 return $this->loadSettingsFromIssuerDiscovery($httpClient);
115 $this->applySettingsFromArray($discoveredSettings);
116 } catch (ClientExceptionInterface $exception) {
117 throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}");
122 * @throws OidcIssuerDiscoveryException
123 * @throws ClientExceptionInterface
125 protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array
127 $issuerUrl = rtrim($this->issuer, '/') . '/.well-known/openid-configuration';
128 $request = new Request('GET', $issuerUrl);
129 $response = $httpClient->sendRequest($request);
130 $result = json_decode($response->getBody()->getContents(), true);
132 if (empty($result) || !is_array($result)) {
133 throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}");
136 if ($result['issuer'] !== $this->issuer) {
137 throw new OidcIssuerDiscoveryException("Unexpected issuer value found on discovery response");
140 $discoveredSettings = [];
142 if (!empty($result['authorization_endpoint'])) {
143 $discoveredSettings['authorizationEndpoint'] = $result['authorization_endpoint'];
146 if (!empty($result['token_endpoint'])) {
147 $discoveredSettings['tokenEndpoint'] = $result['token_endpoint'];
150 if (!empty($result['jwks_uri'])) {
151 $keys = $this->loadKeysFromUri($result['jwks_uri'], $httpClient);
152 $discoveredSettings['keys'] = $this->filterKeys($keys);
155 return $discoveredSettings;
159 * Filter the given JWK keys down to just those we support.
161 protected function filterKeys(array $keys): array
163 return array_filter($keys, function(array $key) {
164 return $key['kty'] === 'RSA' && $key['use'] === 'sig' && $key['alg'] === 'RS256';
169 * Return an array of jwks as PHP key=>value arrays.
170 * @throws ClientExceptionInterface
171 * @throws OidcIssuerDiscoveryException
173 protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array
175 $request = new Request('GET', $uri);
176 $response = $httpClient->sendRequest($request);
177 $result = json_decode($response->getBody()->getContents(), true);
179 if (empty($result) || !is_array($result) || !isset($result['keys'])) {
180 throw new OidcIssuerDiscoveryException("Error reading keys from issuer jwks_uri");
183 return $result['keys'];
187 * Get the settings needed by an OAuth provider, as a key=>value array.
189 public function arrayForProvider(): array
191 $settingKeys = ['clientId', 'clientSecret', 'redirectUri', 'authorizationEndpoint', 'tokenEndpoint'];
193 foreach ($settingKeys as $setting) {
194 $settings[$setting] = $this->$setting;