From c167f40af32a45f0905c6d2961865fbe8c52d996 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 Oct 2021 23:04:28 +0100 Subject: [PATCH] Renamed OIDC files to all be aligned --- .../OidcAccessToken.php} | 4 +- .../OidcIdToken.php} | 46 +++++++++---------- .../Access/Oidc/OidcInvalidKeyException.php | 8 ++++ .../Access/Oidc/OidcInvalidTokenException.php | 10 ++++ .../Oidc/OidcIssuerDiscoveryException.php | 8 ++++ .../OidcJwtSigningKey.php} | 28 +++++------ .../OidcOAuthProvider.php} | 8 ++-- .../OidcProviderSettings.php} | 18 ++++---- .../OidcService.php} | 24 +++++----- .../OpenIdConnect/InvalidKeyException.php | 8 ---- .../OpenIdConnect/InvalidTokenException.php | 10 ---- .../IssuerDiscoveryException.php | 8 ---- .../Auth/OpenIdConnectController.php | 4 +- ...ectIdTokenTest.php => OidcIdTokenTest.php} | 40 ++++++++-------- 14 files changed, 112 insertions(+), 112 deletions(-) rename app/Auth/Access/{OpenIdConnect/OpenIdConnectAccessToken.php => Oidc/OidcAccessToken.php} (94%) rename app/Auth/Access/{OpenIdConnect/OpenIdConnectIdToken.php => Oidc/OidcIdToken.php} (78%) create mode 100644 app/Auth/Access/Oidc/OidcInvalidKeyException.php create mode 100644 app/Auth/Access/Oidc/OidcInvalidTokenException.php create mode 100644 app/Auth/Access/Oidc/OidcIssuerDiscoveryException.php rename app/Auth/Access/{OpenIdConnect/JwtSigningKey.php => Oidc/OidcJwtSigningKey.php} (65%) rename app/Auth/Access/{OpenIdConnect/OpenIdConnectOAuthProvider.php => Oidc/OidcOAuthProvider.php} (94%) rename app/Auth/Access/{OpenIdConnect/OpenIdConnectProviderSettings.php => Oidc/OidcProviderSettings.php} (89%) rename app/Auth/Access/{OpenIdConnect/OpenIdConnectService.php => Oidc/OidcService.php} (86%) delete mode 100644 app/Auth/Access/OpenIdConnect/InvalidKeyException.php delete mode 100644 app/Auth/Access/OpenIdConnect/InvalidTokenException.php delete mode 100644 app/Auth/Access/OpenIdConnect/IssuerDiscoveryException.php rename tests/Unit/{OpenIdConnectIdTokenTest.php => OidcIdTokenTest.php} (86%) diff --git a/app/Auth/Access/OpenIdConnect/OpenIdConnectAccessToken.php b/app/Auth/Access/Oidc/OidcAccessToken.php similarity index 94% rename from app/Auth/Access/OpenIdConnect/OpenIdConnectAccessToken.php rename to app/Auth/Access/Oidc/OidcAccessToken.php index 6731ec4be..63853e08a 100644 --- a/app/Auth/Access/OpenIdConnect/OpenIdConnectAccessToken.php +++ b/app/Auth/Access/Oidc/OidcAccessToken.php @@ -1,11 +1,11 @@ $prop) || !is_array($this->$prop)) { - throw new InvalidTokenException("Could not parse out a valid {$prop} within the provided token"); + throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token"); } } if (empty($this->signature) || !is_string($this->signature)) { - throw new InvalidTokenException("Could not parse out a valid signature within the provided token"); + throw new OidcInvalidTokenException("Could not parse out a valid signature within the provided token"); } } /** * Validate the signature of the given token and ensure it validates against the provided key. - * @throws InvalidTokenException + * @throws OidcInvalidTokenException */ protected function validateTokenSignature(): void { if ($this->header['alg'] !== 'RS256') { - throw new InvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}"); + throw new OidcInvalidTokenException("Only RS256 signature validation is supported. Token reports using {$this->header['alg']}"); } $parsedKeys = array_map(function($key) { try { - return new JwtSigningKey($key); - } catch (InvalidKeyException $e) { + return new OidcJwtSigningKey($key); + } catch (OidcInvalidKeyException $e) { return null; } }, $this->keys); @@ -141,27 +141,27 @@ class OpenIdConnectIdToken $parsedKeys = array_filter($parsedKeys); $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1]; - /** @var JwtSigningKey $parsedKey */ + /** @var OidcJwtSigningKey $parsedKey */ foreach ($parsedKeys as $parsedKey) { if ($parsedKey->verify($contentToSign, $this->signature)) { return; } } - throw new InvalidTokenException('Token signature could not be validated using the provided keys'); + throw new OidcInvalidTokenException('Token signature could not be validated using the provided keys'); } /** * Validate the claims of the token. * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation - * @throws InvalidTokenException + * @throws OidcInvalidTokenException */ protected function validateTokenClaims(string $clientId): void { // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. if (empty($this->payload['iss']) || $this->issuer !== $this->payload['iss']) { - throw new InvalidTokenException('Missing or non-matching token issuer value'); + throw new OidcInvalidTokenException('Missing or non-matching token issuer value'); } // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered @@ -169,16 +169,16 @@ class OpenIdConnectIdToken // if the ID Token does not list the Client as a valid audience, or if it contains additional // audiences not trusted by the Client. if (empty($this->payload['aud'])) { - throw new InvalidTokenException('Missing token audience value'); + throw new OidcInvalidTokenException('Missing token audience value'); } $aud = is_string($this->payload['aud']) ? [$this->payload['aud']] : $this->payload['aud']; if (count($aud) !== 1) { - throw new InvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1'); + throw new OidcInvalidTokenException('Token audience value has ' . count($aud) . ' values, Expected 1'); } if ($aud[0] !== $clientId) { - throw new InvalidTokenException('Token audience value did not match the expected client_id'); + throw new OidcInvalidTokenException('Token audience value did not match the expected client_id'); } // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present. @@ -187,32 +187,32 @@ class OpenIdConnectIdToken // 4. If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id // is the Claim Value. if (isset($this->payload['azp']) && $this->payload['azp'] !== $clientId) { - throw new InvalidTokenException('Token authorized party exists but does not match the expected client_id'); + throw new OidcInvalidTokenException('Token authorized party exists but does not match the expected client_id'); } // 5. The current time MUST be before the time represented by the exp Claim // (possibly allowing for some small leeway to account for clock skew). if (empty($this->payload['exp'])) { - throw new InvalidTokenException('Missing token expiration time value'); + throw new OidcInvalidTokenException('Missing token expiration time value'); } $skewSeconds = 120; $now = time(); if ($now >= (intval($this->payload['exp']) + $skewSeconds)) { - throw new InvalidTokenException('Token has expired'); + throw new OidcInvalidTokenException('Token has expired'); } // 6. The iat Claim can be used to reject tokens that were issued too far away from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks. // The acceptable range is Client specific. if (empty($this->payload['iat'])) { - throw new InvalidTokenException('Missing token issued at time value'); + throw new OidcInvalidTokenException('Missing token issued at time value'); } $dayAgo = time() - 86400; $iat = intval($this->payload['iat']); if ($iat > ($now + $skewSeconds) || $iat < $dayAgo) { - throw new InvalidTokenException('Token issue at time is not recent or is invalid'); + throw new OidcInvalidTokenException('Token issue at time is not recent or is invalid'); } // 7. If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate. @@ -225,7 +225,7 @@ class OpenIdConnectIdToken // Custom: Ensure the "sub" (Subject) Claim exists and has a value. if (empty($this->payload['sub'])) { - throw new InvalidTokenException('Missing token subject value'); + throw new OidcInvalidTokenException('Missing token subject value'); } } diff --git a/app/Auth/Access/Oidc/OidcInvalidKeyException.php b/app/Auth/Access/Oidc/OidcInvalidKeyException.php new file mode 100644 index 000000000..17c32f416 --- /dev/null +++ b/app/Auth/Access/Oidc/OidcInvalidKeyException.php @@ -0,0 +1,8 @@ + 'RSA', 'alg' => 'RS256', 'n' => 'abc123...'] * @param array|string $jwkOrKeyPath - * @throws InvalidKeyException + * @throws OidcInvalidKeyException */ public function __construct($jwkOrKeyPath) { @@ -29,12 +29,12 @@ class JwtSigningKey } else if (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) { $this->loadFromPath($jwkOrKeyPath); } else { - throw new InvalidKeyException('Unexpected type of key value provided'); + throw new OidcInvalidKeyException('Unexpected type of key value provided'); } } /** - * @throws InvalidKeyException + * @throws OidcInvalidKeyException */ protected function loadFromPath(string $path) { @@ -43,37 +43,37 @@ class JwtSigningKey file_get_contents($path) )->withPadding(RSA::SIGNATURE_PKCS1); } catch (\Exception $exception) { - throw new InvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}"); + throw new OidcInvalidKeyException("Failed to load key from file path with error: {$exception->getMessage()}"); } if (!($this->key instanceof RSA)) { - throw new InvalidKeyException("Key loaded from file path is not an RSA key as expected"); + throw new OidcInvalidKeyException("Key loaded from file path is not an RSA key as expected"); } } /** - * @throws InvalidKeyException + * @throws OidcInvalidKeyException */ protected function loadFromJwkArray(array $jwk) { if ($jwk['alg'] !== 'RS256') { - throw new InvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}"); + throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$jwk['alg']}"); } if (empty($jwk['use'])) { - throw new InvalidKeyException('A "use" parameter on the provided key is expected'); + throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected'); } if ($jwk['use'] !== 'sig') { - throw new InvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}"); + throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}"); } if (empty($jwk['e'])) { - throw new InvalidKeyException('An "e" parameter on the provided key is expected'); + throw new OidcInvalidKeyException('An "e" parameter on the provided key is expected'); } if (empty($jwk['n'])) { - throw new InvalidKeyException('A "n" parameter on the provided key is expected'); + throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected'); } $n = strtr($jwk['n'] ?? '', '-_', '+/'); @@ -85,7 +85,7 @@ class JwtSigningKey 'n' => new BigInteger(base64_decode($n), 256), ])->withPadding(RSA::SIGNATURE_PKCS1); } catch (\Exception $exception) { - throw new InvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}"); + throw new OidcInvalidKeyException("Failed to load key from JWK parameters with error: {$exception->getMessage()}"); } } diff --git a/app/Auth/Access/OpenIdConnect/OpenIdConnectOAuthProvider.php b/app/Auth/Access/Oidc/OidcOAuthProvider.php similarity index 94% rename from app/Auth/Access/OpenIdConnect/OpenIdConnectOAuthProvider.php rename to app/Auth/Access/Oidc/OidcOAuthProvider.php index 074f463cc..03230e373 100644 --- a/app/Auth/Access/OpenIdConnect/OpenIdConnectOAuthProvider.php +++ b/app/Auth/Access/Oidc/OidcOAuthProvider.php @@ -1,6 +1,6 @@ applySettingsFromArray($discoveredSettings); } catch (ClientExceptionInterface $exception) { - throw new IssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}"); + throw new OidcIssuerDiscoveryException("HTTP request failed during discovery with error: {$exception->getMessage()}"); } } /** - * @throws IssuerDiscoveryException + * @throws OidcIssuerDiscoveryException * @throws ClientExceptionInterface */ protected function loadSettingsFromIssuerDiscovery(ClientInterface $httpClient): array @@ -130,11 +130,11 @@ class OpenIdConnectProviderSettings $result = json_decode($response->getBody()->getContents(), true); if (empty($result) || !is_array($result)) { - throw new IssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}"); + throw new OidcIssuerDiscoveryException("Error discovering provider settings from issuer at URL {$issuerUrl}"); } if ($result['issuer'] !== $this->issuer) { - throw new IssuerDiscoveryException("Unexpected issuer value found on discovery response"); + throw new OidcIssuerDiscoveryException("Unexpected issuer value found on discovery response"); } $discoveredSettings = []; @@ -168,7 +168,7 @@ class OpenIdConnectProviderSettings /** * Return an array of jwks as PHP key=>value arrays. * @throws ClientExceptionInterface - * @throws IssuerDiscoveryException + * @throws OidcIssuerDiscoveryException */ protected function loadKeysFromUri(string $uri, ClientInterface $httpClient): array { @@ -177,7 +177,7 @@ class OpenIdConnectProviderSettings $result = json_decode($response->getBody()->getContents(), true); if (empty($result) || !is_array($result) || !isset($result['keys'])) { - throw new IssuerDiscoveryException("Error reading keys from issuer jwks_uri"); + throw new OidcIssuerDiscoveryException("Error reading keys from issuer jwks_uri"); } return $result['keys']; diff --git a/app/Auth/Access/OpenIdConnect/OpenIdConnectService.php b/app/Auth/Access/Oidc/OidcService.php similarity index 86% rename from app/Auth/Access/OpenIdConnect/OpenIdConnectService.php rename to app/Auth/Access/Oidc/OidcService.php index 57c9d1238..be6a5c3c4 100644 --- a/app/Auth/Access/OpenIdConnect/OpenIdConnectService.php +++ b/app/Auth/Access/Oidc/OidcService.php @@ -1,4 +1,4 @@ - $this->config['issuer'], 'clientId' => $this->config['client_id'], 'clientSecret' => $this->config['client_secret'], @@ -104,15 +104,15 @@ class OpenIdConnectService /** * Load the underlying OpenID Connect Provider. */ - protected function getProvider(OpenIdConnectProviderSettings $settings): OpenIdConnectOAuthProvider + protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider { - return new OpenIdConnectOAuthProvider($settings->arrayForProvider()); + return new OidcOAuthProvider($settings->arrayForProvider()); } /** * Calculate the display name */ - protected function getUserDisplayName(OpenIdConnectIdToken $token, string $defaultValue): string + protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string { $displayNameAttr = $this->config['display_name_claims']; @@ -135,7 +135,7 @@ class OpenIdConnectService * Extract the details of a user from an ID token. * @return array{name: string, email: string, external_id: string} */ - protected function getUserDetails(OpenIdConnectIdToken $token): array + protected function getUserDetails(OidcIdToken $token): array { $id = $token->getClaim('sub'); return [ @@ -153,10 +153,10 @@ class OpenIdConnectService * @throws UserRegistrationException * @throws StoppedAuthenticationException */ - protected function processAccessTokenCallback(OpenIdConnectAccessToken $accessToken, OpenIdConnectProviderSettings $settings): User + protected function processAccessTokenCallback(OidcAccessToken $accessToken, OidcProviderSettings $settings): User { $idTokenText = $accessToken->getIdToken(); - $idToken = new OpenIdConnectIdToken( + $idToken = new OidcIdToken( $idTokenText, $settings->issuer, $settings->keys, @@ -168,7 +168,7 @@ class OpenIdConnectService try { $idToken->validate($settings->clientId); - } catch (InvalidTokenException $exception) { + } catch (OidcInvalidTokenException $exception) { throw new OpenIdConnectException("ID token validate failed with error: {$exception->getMessage()}"); } diff --git a/app/Auth/Access/OpenIdConnect/InvalidKeyException.php b/app/Auth/Access/OpenIdConnect/InvalidKeyException.php deleted file mode 100644 index 85746cb6a..000000000 --- a/app/Auth/Access/OpenIdConnect/InvalidKeyException.php +++ /dev/null @@ -1,8 +0,0 @@ -oidcService = $oidcService; $this->middleware('guard:oidc'); diff --git a/tests/Unit/OpenIdConnectIdTokenTest.php b/tests/Unit/OidcIdTokenTest.php similarity index 86% rename from tests/Unit/OpenIdConnectIdTokenTest.php rename to tests/Unit/OidcIdTokenTest.php index d3394daf6..abc811f75 100644 --- a/tests/Unit/OpenIdConnectIdTokenTest.php +++ b/tests/Unit/OidcIdTokenTest.php @@ -2,16 +2,16 @@ namespace Tests\Unit; -use BookStack\Auth\Access\OpenIdConnect\InvalidTokenException; -use BookStack\Auth\Access\OpenIdConnect\OpenIdConnectIdToken; +use BookStack\Auth\Access\Oidc\OidcInvalidTokenException; +use BookStack\Auth\Access\Oidc\OidcIdToken; use phpseclib3\Crypt\RSA; use Tests\TestCase; -class OpenIdConnectIdTokenTest extends TestCase +class OidcIdTokenTest extends TestCase { public function test_valid_token_passes_validation() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [ + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [ $this->jwkKeyArray() ]); @@ -20,20 +20,20 @@ class OpenIdConnectIdTokenTest extends TestCase public function test_get_claim_returns_value_if_existing() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []); + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []); $this->assertEquals('bscott@example.com', $token->getClaim('email')); } public function test_get_claim_returns_null_if_not_existing() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []); + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []); $this->assertEquals(null, $token->getClaim('emails')); } public function test_get_all_claims_returns_all_payload_claims() { $defaultPayload = $this->getDefaultPayload(); - $token = new OpenIdConnectIdToken($this->idToken($defaultPayload), 'https://auth.example.com', []); + $token = new OidcIdToken($this->idToken($defaultPayload), 'https://auth.example.com', []); $this->assertEquals($defaultPayload, $token->getAllClaims()); } @@ -52,7 +52,7 @@ class OpenIdConnectIdTokenTest extends TestCase ]; foreach ($messagesAndTokenValues as [$message, $tokenValue]) { - $token = new OpenIdConnectIdToken($tokenValue, 'https://auth.example.com', []); + $token = new OidcIdToken($tokenValue, 'https://auth.example.com', []); $err = null; try { $token->validate('abc'); @@ -60,43 +60,43 @@ class OpenIdConnectIdTokenTest extends TestCase $err = $exception; } - $this->assertInstanceOf(InvalidTokenException::class, $err, $message); + $this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message); $this->assertEquals($message, $err->getMessage()); } } public function test_error_thrown_if_token_signature_not_validated_from_no_keys() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', []); - $this->expectException(InvalidTokenException::class); + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', []); + $this->expectException(OidcInvalidTokenException::class); $this->expectExceptionMessage('Token signature could not be validated using the provided keys'); $token->validate('abc'); } public function test_error_thrown_if_token_signature_not_validated_from_non_matching_key() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [ + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [ array_merge($this->jwkKeyArray(), [ 'n' => 'iqK-1QkICMf_cusNLpeNnN-bhT0-9WLBvzgwKLALRbrevhdi5ttrLHIQshaSL0DklzfyG2HWRmAnJ9Q7sweEjuRiiqRcSUZbYu8cIv2hLWYu7K_NH67D2WUjl0EnoHEuiVLsZhQe1CmdyLdx087j5nWkd64K49kXRSdxFQUlj8W3NeK3CjMEUdRQ3H4RZzJ4b7uuMiFA29S2ZhMNG20NPbkUVsFL-jiwTd10KSsPT8yBYipI9O7mWsUWt_8KZs1y_vpM_k3SyYihnWpssdzDm1uOZ8U3mzFr1xsLAO718GNUSXk6npSDzLl59HEqa6zs4O9awO2qnSHvcmyELNk31w' ]) ]); - $this->expectException(InvalidTokenException::class); + $this->expectException(OidcInvalidTokenException::class); $this->expectExceptionMessage('Token signature could not be validated using the provided keys'); $token->validate('abc'); } public function test_error_thrown_if_token_signature_not_validated_from_invalid_key() { - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', ['url://example.com']); - $this->expectException(InvalidTokenException::class); + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', ['url://example.com']); + $this->expectException(OidcInvalidTokenException::class); $this->expectExceptionMessage('Token signature could not be validated using the provided keys'); $token->validate('abc'); } public function test_error_thrown_if_token_algorithm_is_not_rs256() { - $token = new OpenIdConnectIdToken($this->idToken([], ['alg' => 'HS256']), 'https://auth.example.com', []); - $this->expectException(InvalidTokenException::class); + $token = new OidcIdToken($this->idToken([], ['alg' => 'HS256']), 'https://auth.example.com', []); + $this->expectException(OidcInvalidTokenException::class); $this->expectExceptionMessage("Only RS256 signature validation is supported. Token reports using HS256"); $token->validate('abc'); } @@ -133,7 +133,7 @@ class OpenIdConnectIdTokenTest extends TestCase ]; foreach ($claimOverridesByErrorMessage as [$message, $overrides]) { - $token = new OpenIdConnectIdToken($this->idToken($overrides), 'https://auth.example.com', [ + $token = new OidcIdToken($this->idToken($overrides), 'https://auth.example.com', [ $this->jwkKeyArray() ]); @@ -144,7 +144,7 @@ class OpenIdConnectIdTokenTest extends TestCase $err = $exception; } - $this->assertInstanceOf(InvalidTokenException::class, $err, $message); + $this->assertInstanceOf(OidcInvalidTokenException::class, $err, $message); $this->assertEquals($message, $err->getMessage()); } } @@ -154,7 +154,7 @@ class OpenIdConnectIdTokenTest extends TestCase $file = tmpfile(); $testFilePath = 'file://' . stream_get_meta_data($file)['uri']; file_put_contents($testFilePath, $this->pemKey()); - $token = new OpenIdConnectIdToken($this->idToken(), 'https://auth.example.com', [ + $token = new OidcIdToken($this->idToken(), 'https://auth.example.com', [ $testFilePath ]);