diff --git a/composer.json b/composer.json index d04c792..3ab00c1 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "type": "library", "license": "Apache-2.0", "require": { - "novvor/identity-contracts": "^1.0", - "novvor/identity-sdk-php": "^1.0", + "novvor/identity-contracts": "^1.0 || ^2.0", + "novvor/identity-sdk-php": "^1.0 || ^2.0", "php": "^8.2" }, "require-dev": { diff --git a/src/FakeIdentityProvider.php b/src/FakeIdentityProvider.php index e9444ea..9e90a53 100644 --- a/src/FakeIdentityProvider.php +++ b/src/FakeIdentityProvider.php @@ -5,14 +5,31 @@ final class FakeIdentityProvider { /** @return array */ - public function discovery(string $issuer = 'https://identity.test'): array + public function discovery(string $issuer = 'https://identity.test', bool $highAssurance = false): array { - return [ + $document = [ 'issuer' => $issuer, 'authorization_endpoint' => $issuer.'/oauth/authorize', 'token_endpoint' => $issuer.'/oauth/token', + 'userinfo_endpoint' => $issuer.'/oauth/userinfo', + 'introspection_endpoint' => $issuer.'/oauth/introspect', + 'revocation_endpoint' => $issuer.'/oauth/revoke', 'jwks_uri' => $issuer.'/.well-known/jwks.json', + 'response_types_supported' => ['code'], + 'response_modes_supported' => ['query'], + 'grant_types_supported' => ['authorization_code', 'refresh_token', 'client_credentials'], + 'token_endpoint_auth_methods_supported' => ['none', 'client_secret_post'], 'code_challenge_methods_supported' => ['S256'], + 'authorization_response_iss_parameter_supported' => true, ]; + if ($highAssurance) { + $document['pushed_authorization_request_endpoint'] = $issuer.'/oauth/par'; + $document['response_modes_supported'][] = 'query.jwt'; + $document['token_endpoint_auth_methods_supported'][] = 'private_key_jwt'; + $document['dpop_signing_alg_values_supported'] = ['RS256', 'ES256']; + $document['authorization_signing_alg_values_supported'] = ['RS256']; + } + + return $document; } } diff --git a/src/FakeOAuthEndpointError.php b/src/FakeOAuthEndpointError.php new file mode 100644 index 0000000..72facd8 --- /dev/null +++ b/src/FakeOAuthEndpointError.php @@ -0,0 +1,26 @@ +, json: array{error: string, error_description: string}} */ + public function response(int $status = 400): array + { + return [ + 'status' => $status, + 'headers' => array_filter([ + 'X-Correlation-ID' => $this->correlationId, + 'DPoP-Nonce' => $this->dpopNonce, + ], static fn (?string $value): bool => $value !== null && $value !== ''), + 'json' => ['error' => $this->error, 'error_description' => $this->description], + ]; + } +} diff --git a/tests/FakesTest.php b/tests/FakesTest.php index b941090..eefc4d1 100644 --- a/tests/FakesTest.php +++ b/tests/FakesTest.php @@ -7,6 +7,7 @@ use Novvor\IdentitySdkTesting\FakeBackChannelLogout; use Novvor\IdentitySdkTesting\FakeIdentityProvider; use Novvor\IdentitySdkTesting\FakeJwksDocument; +use Novvor\IdentitySdkTesting\FakeOAuthEndpointError; use Novvor\IdentitySdkTesting\FakeStepUpSession; use PHPUnit\Framework\TestCase; @@ -34,4 +35,29 @@ public function test_jwks_document_wraps_keys(): void { self::assertSame(['keys' => [['kid' => 'one']]], (new FakeJwksDocument([['kid' => 'one']]))->toArray()); } + + public function test_high_assurance_discovery_is_explicitly_opt_in(): void + { + $standard = (new FakeIdentityProvider())->discovery('https://identity.example.com'); + $highAssurance = (new FakeIdentityProvider())->discovery('https://identity.example.com', true); + + self::assertArrayNotHasKey('pushed_authorization_request_endpoint', $standard); + self::assertSame('https://identity.example.com/oauth/par', $highAssurance['pushed_authorization_request_endpoint']); + $authMethods = $highAssurance['token_endpoint_auth_methods_supported'] ?? null; + $responseModes = $highAssurance['response_modes_supported'] ?? null; + if (! is_array($authMethods) || ! is_array($responseModes)) { + self::fail('High-assurance discovery metadata must expose list capabilities.'); + } + self::assertContains('private_key_jwt', $authMethods); + self::assertContains('query.jwt', $responseModes); + } + + public function test_oauth_error_fake_never_contains_secrets(): void + { + $response = (new FakeOAuthEndpointError('use_dpop_nonce', 'Retry with the supplied nonce.', 'correlation-1', 'nonce-1'))->response(); + + self::assertSame(400, $response['status']); + self::assertSame('nonce-1', $response['headers']['DPoP-Nonce']); + self::assertArrayNotHasKey('access_token', $response['json']); + } }