Skip to content

Commit fe4d115

Browse files
feature(#22): this commit adds end-to-end tests for key rotation and key purging
1 parent 75ef229 commit fe4d115

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

Applications/Backend/Tests/Integration/Security/KeyRotationIntegrationTests.cs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)