@@ -1041,6 +1041,122 @@ public function testGetJwtVerificationKeysCacheHit(): void
10411041 $ this ->assertEquals (self ::NONCE , $ claims ->nonce );
10421042 }
10431043
1044+ public function testGetConfigurationCachesFetchedDocument (): void
1045+ {
1046+ $ openIDConnectMetadataUrl = 'https://some.url/openid-configuration ' ;
1047+ $ configuration = $ this ->loadMockFixture ('mockOpenIDConfiguration.json ' );
1048+
1049+ $ mockConfigResponse = $ this ->getMockHttpSuccessResponse ('/../MockData/mockOpenIDConfiguration.json ' );
1050+ $ mockHttpClient = $ this ->createStub (ClientInterface::class);
1051+ $ mockHttpClient ->method ('request ' )->willReturn ($ mockConfigResponse );
1052+
1053+ // On a cache miss the fetched discovery document must be stored with
1054+ // the configured cache duration, under the namespaced cache key.
1055+ $ configCacheItem = $ this ->createMock (CacheItemInterface::class);
1056+ $ configCacheItem ->method ('isHit ' )->willReturn (false );
1057+ $ configCacheItem ->expects ($ this ->once ())->method ('set ' )->with ($ configuration )->willReturnSelf ();
1058+ $ configCacheItem ->expects ($ this ->once ())->method ('expiresAfter ' )->with (3600 )->willReturnSelf ();
1059+
1060+ $ expectedCacheKey = 'itk-openid-connect-configuration-|| ' .hash ('sha1 ' , $ openIDConnectMetadataUrl ).'||configuration ' ;
1061+
1062+ $ mockCacheItemPool = $ this ->createMock (CacheItemPoolInterface::class);
1063+ $ mockCacheItemPool ->expects ($ this ->once ())->method ('getItem ' )->with ($ expectedCacheKey )->willReturn ($ configCacheItem );
1064+ $ mockCacheItemPool ->expects ($ this ->once ())->method ('save ' )->with ($ configCacheItem )->willReturn (true );
1065+
1066+ $ provider = new OpenIdConfigurationProvider ([
1067+ 'openIDConnectMetadataUrl ' => $ openIDConnectMetadataUrl ,
1068+ 'cacheItemPool ' => $ mockCacheItemPool ,
1069+ 'clientId ' => self ::CLIENT_ID ,
1070+ 'clientSecret ' => self ::CLIENT_SECRET ,
1071+ 'redirectUri ' => self ::REDIRECT_URI ,
1072+ 'cacheDuration ' => 3600 ,
1073+ ], [
1074+ 'httpClient ' => $ mockHttpClient ,
1075+ ]);
1076+
1077+ $ authUrl = $ provider ->getBaseAuthorizationUrl ();
1078+ $ this ->assertSame ('https://azure_b2c_test.b2clogin.com/azure_b2c_test.onmicrosoft.com/oauth2/v2.0/authorize?p=test-policy ' , $ authUrl );
1079+ }
1080+
1081+ public function testGetJwtVerificationKeysCachesFetchedKeys (): void
1082+ {
1083+ $ openIDConnectMetadataUrl = 'https://some.url/openid-configuration ' ;
1084+ $ configuration = $ this ->loadMockFixture ('mockOpenIDConfiguration.json ' );
1085+
1086+ $ mockKeysResponse = $ this ->getMockHttpSuccessResponse ('/../MockData/mockOpenIDValidationKeys.json ' );
1087+ $ mockHttpClient = $ this ->createStub (ClientInterface::class);
1088+ $ mockHttpClient ->method ('request ' )->willReturn ($ mockKeysResponse );
1089+
1090+ $ configCacheItem = $ this ->createStub (CacheItemInterface::class);
1091+ $ configCacheItem ->method ('isHit ' )->willReturn (true );
1092+ $ configCacheItem ->method ('get ' )->willReturn ($ configuration );
1093+
1094+ // On a JWKS cache miss the built Key map must be stored with the
1095+ // configured cache duration and saved to the pool.
1096+ $ jwksCacheItem = $ this ->createMock (CacheItemInterface::class);
1097+ $ jwksCacheItem ->method ('isHit ' )->willReturn (false );
1098+ $ jwksCacheItem ->expects ($ this ->once ())->method ('set ' )->with ($ this ->callback (
1099+ static fn (array $ keys ): bool => 1 === count ($ keys )
1100+ && $ keys ['111111111111111111111111111111111111111111 ' ] instanceof Key
1101+ ))->willReturnSelf ();
1102+ $ jwksCacheItem ->expects ($ this ->once ())->method ('expiresAfter ' )->with (3600 )->willReturnSelf ();
1103+
1104+ $ mockCacheItemPool = $ this ->createMock (CacheItemPoolInterface::class);
1105+ $ mockCacheItemPool ->method ('getItem ' )->willReturnCallback (
1106+ static fn (string $ key ) => str_contains ($ key , 'jwks ' ) ? $ jwksCacheItem : $ configCacheItem
1107+ );
1108+ $ mockCacheItemPool ->expects ($ this ->once ())->method ('save ' )->with ($ jwksCacheItem )->willReturn (true );
1109+
1110+ $ provider = new OpenIdConfigurationProvider ([
1111+ 'openIDConnectMetadataUrl ' => $ openIDConnectMetadataUrl ,
1112+ 'cacheItemPool ' => $ mockCacheItemPool ,
1113+ 'clientId ' => self ::CLIENT_ID ,
1114+ 'clientSecret ' => self ::CLIENT_SECRET ,
1115+ 'redirectUri ' => self ::REDIRECT_URI ,
1116+ 'cacheDuration ' => 3600 ,
1117+ ], [
1118+ 'httpClient ' => $ mockHttpClient ,
1119+ ]);
1120+
1121+ /** @var \Mockery\MockInterface $mockJWT */
1122+ $ mockJWT = \Mockery::mock ('overload:Firebase\JWT\JWT ' , MockJWT::class);
1123+ $ mockJWT ->shouldReceive ('decode ' )->andReturn ($ this ->getMockClaims ());
1124+
1125+ /** @var object{nonce: string} $claims */
1126+ $ claims = $ provider ->validateIdToken ('token ' , self ::NONCE );
1127+ $ this ->assertEquals (self ::NONCE , $ claims ->nonce );
1128+ }
1129+
1130+ public function testGetJwtVerificationKeysBuildsAllJwksKeys (): void
1131+ {
1132+ // Two RSA keys in the JWKS: the full key map (not just the first
1133+ // entry) must reach JWT::decode, since the token's "kid" may match
1134+ // any key published by the IdP.
1135+ $ fixtureKeys = $ this ->loadMockFixture ('mockOpenIDValidationKeys.json ' );
1136+ $ this ->assertIsArray ($ fixtureKeys ['keys ' ]);
1137+ $ this ->assertIsArray ($ fixtureKeys ['keys ' ][0 ]);
1138+ $ template = $ fixtureKeys ['keys ' ][0 ];
1139+
1140+ $ jwks = ['keys ' => [
1141+ ['kid ' => 'key-a ' ] + $ template ,
1142+ ['kid ' => 'key-b ' ] + $ template ,
1143+ ]];
1144+ $ provider = $ this ->createProviderWithCustomJwks ((string ) json_encode ($ jwks ));
1145+
1146+ /** @var \Mockery\MockInterface $mockJWT */
1147+ $ mockJWT = \Mockery::mock ('overload:Firebase\JWT\JWT ' , MockJWT::class);
1148+ $ mockJWT ->shouldReceive ('decode ' )->with (
1149+ \Mockery::type ('string ' ),
1150+ \Mockery::on (static fn (array $ keys ): bool => 2 === count ($ keys )
1151+ && $ keys ['key-a ' ] instanceof Key
1152+ && $ keys ['key-b ' ] instanceof Key)
1153+ )->andReturn ($ this ->getMockClaims ());
1154+
1155+ /** @var object{nonce: string} $claims */
1156+ $ claims = $ provider ->validateIdToken ('token ' , self ::NONCE );
1157+ $ this ->assertEquals (self ::NONCE , $ claims ->nonce );
1158+ }
1159+
10441160 public function testGetConfigurationCacheInvalidArgument (): void
10451161 {
10461162 $ openIDConnectMetadataUrl = 'https://some.url/openid-configuration ' ;
0 commit comments