@@ -187,4 +187,160 @@ public async Task WhenRotateKeys_ShouldKeepOldTokenValidWhileOldKeyIsPublished()
187187
188188 Assert . True ( validationSucceeded ) ;
189189 }
190+
191+ [ Fact ( DisplayName = "[e2e] - when rotating keys jwks should contain multiple keys during grace period" ) ]
192+ public async Task WhenRotateKeys_JwksShouldContainMultipleKeysDuringGracePeriod ( )
193+ {
194+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
195+
196+ var rotationService = factory . Services . GetRequiredService < ISecretRotationService > ( ) ;
197+ var realmCollection = factory . Services . GetRequiredService < IRealmCollection > ( ) ;
198+
199+ var jwksResponseBefore = await httpClient . GetAsync ( "master/.well-known/jwks.json" ) ;
200+ var jwksRawBefore = await jwksResponseBefore . Content . ReadAsStringAsync ( ) ;
201+
202+ using var jwksBefore = JsonDocument . Parse ( jwksRawBefore ) ;
203+
204+ Assert . NotNull ( jwksBefore ) ;
205+ Assert . True ( jwksBefore . RootElement . TryGetProperty ( "keys" , out var keysBefore ) ) ;
206+
207+ var oldKidsCount = keysBefore . EnumerateArray ( )
208+ . Where ( key => key . TryGetProperty ( "kid" , out _ ) )
209+ . Count ( ) ;
210+
211+ var realmFilters = RealmFilters . WithSpecifications ( )
212+ . WithName ( "master" )
213+ . Build ( ) ;
214+
215+ var realms = await realmCollection . GetRealmsAsync ( realmFilters , CancellationToken . None ) ;
216+ var realm = realms . FirstOrDefault ( ) ;
217+
218+ Assert . NotNull ( realm ) ;
219+
220+ await rotationService . RotateSecretAsync ( realm , CancellationToken . None ) ;
221+
222+ string ? newKid = null ;
223+ var started = DateTimeOffset . UtcNow ;
224+
225+ while ( DateTimeOffset . UtcNow - started < TimeSpan . FromSeconds ( 10 ) )
226+ {
227+ var response = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , new AuthenticationCredentials
228+ {
229+ Username = "federation.testing.user" ,
230+ Password = "federation.testing.password"
231+ } ) ;
232+
233+ var authentication = await response . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
234+
235+ Assert . NotNull ( authentication ) ;
236+ Assert . NotEmpty ( authentication . AccessToken ) ;
237+
238+ var jwt = new JwtSecurityTokenHandler ( ) . ReadJwtToken ( authentication . AccessToken ) ;
239+
240+ newKid = jwt . Header . Kid ;
241+
242+ var jwksResponse = await httpClient . GetAsync ( "master/.well-known/jwks.json" ) ;
243+ var jwksRaw = await jwksResponse . Content . ReadAsStringAsync ( ) ;
244+
245+ using var jwks = JsonDocument . Parse ( jwksRaw ) ;
246+
247+ Assert . True ( jwks . RootElement . TryGetProperty ( "keys" , out var keysAfter ) ) ;
248+
249+ var newKidsCount = keysAfter . EnumerateArray ( )
250+ . Where ( key => key . TryGetProperty ( "kid" , out _ ) )
251+ . Count ( ) ;
252+
253+ if ( newKidsCount > oldKidsCount )
254+ {
255+ var jwksKids = keysAfter . EnumerateArray ( )
256+ . Where ( key => key . TryGetProperty ( "kid" , out _ ) )
257+ . Select ( key => key . GetProperty ( "kid" ) . GetString ( ) )
258+ . Where ( key => ! string . IsNullOrWhiteSpace ( key ) )
259+ . Cast < string > ( )
260+ . ToArray ( ) ;
261+
262+ Assert . NotEmpty ( jwksKids ) ;
263+ Assert . True ( jwksKids . Length >= 2 , "JWKS should contain at least 2 keys during grace period" ) ;
264+
265+ return ;
266+ }
267+
268+ await Task . Delay ( 300 ) ;
269+ }
270+
271+ Assert . Fail ( "JWKS never showed multiple keys during grace period" ) ;
272+ }
273+
274+ [ Fact ( DisplayName = "[e2e] - when grace period expires old key should be removed from database" ) ]
275+ public async Task WhenGracePeriodExpires_ShouldRemoveOldKeyFromDatabase ( )
276+ {
277+ var httpClient = factory . HttpClient . WithRealmHeader ( "master" ) ;
278+
279+ var rotationService = factory . Services . GetRequiredService < ISecretRotationService > ( ) ;
280+ var realmCollection = factory . Services . GetRequiredService < IRealmCollection > ( ) ;
281+ var secretCollection = factory . Services . GetRequiredService < ISecretCollection > ( ) ;
282+
283+ var realmFilters = RealmFilters . WithSpecifications ( )
284+ . WithName ( "master" )
285+ . Build ( ) ;
286+
287+ var realms = await realmCollection . GetRealmsAsync ( realmFilters , CancellationToken . None ) ;
288+ var realm = realms . FirstOrDefault ( ) ;
289+
290+ Assert . NotNull ( realm ) ;
291+
292+ var secretFiltersBefore = SecretFilters . WithSpecifications ( )
293+ . WithRealm ( realm . Id )
294+ . Build ( ) ;
295+
296+ var secretsBefore = await secretCollection . GetSecretsAsync ( secretFiltersBefore , CancellationToken . None ) ;
297+ var countBefore = secretsBefore . Count ;
298+
299+ await rotationService . RotateSecretAsync ( realm , CancellationToken . None ) ;
300+
301+ var started = DateTimeOffset . UtcNow ;
302+ while ( DateTimeOffset . UtcNow - started < TimeSpan . FromSeconds ( 10 ) )
303+ {
304+ var auth = await httpClient . PostAsJsonAsync ( "api/v1/identity/authenticate" , new AuthenticationCredentials
305+ {
306+ Username = "federation.testing.user" ,
307+ Password = "federation.testing.password"
308+ } ) ;
309+
310+ var authResult = await auth . Content . ReadFromJsonAsync < AuthenticationResult > ( ) ;
311+
312+ Assert . NotNull ( authResult ) ;
313+
314+ var secretsAfterRotation = await secretCollection . GetSecretsAsync ( secretFiltersBefore , CancellationToken . None ) ;
315+ var countAfterRotation = secretsAfterRotation . Count ;
316+
317+ if ( countAfterRotation > countBefore )
318+ {
319+ var oldSecret = secretsAfterRotation . FirstOrDefault ( secret => secret . GracePeriodEndsAt is not null ) ;
320+
321+ Assert . NotNull ( oldSecret ) ;
322+ Assert . NotNull ( oldSecret . GracePeriodEndsAt ) ;
323+
324+ oldSecret . GracePeriodEndsAt = DateTime . UtcNow . AddSeconds ( - 1 ) ;
325+
326+ await secretCollection . UpdateAsync ( oldSecret , cancellation : CancellationToken . None ) ;
327+ await rotationService . PruneSecretsAsync ( realm , CancellationToken . None ) ;
328+
329+ var secretsAfterPrune = await secretCollection . GetSecretsAsync ( secretFiltersBefore , CancellationToken . None ) ;
330+ var countAfterPrune = secretsAfterPrune . Count ;
331+
332+ Assert . True ( countAfterPrune <= countAfterRotation ) ;
333+
334+ var removedSecret = secretsAfterPrune . FirstOrDefault ( secret => secret . Id == oldSecret . Id ) ;
335+
336+ Assert . Null ( removedSecret ) ;
337+
338+ return ;
339+ }
340+
341+ await Task . Delay ( 300 ) ;
342+ }
343+
344+ Assert . Fail ( "Grace period test timeout: rotation did not complete in time" ) ;
345+ }
190346}
0 commit comments