Skip to content

Commit 4ebbd5e

Browse files
tests(#22): this commit introduces tests for signature key rotation
1 parent d46793b commit 4ebbd5e

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
using System.Text.Json;
2+
3+
namespace HttpsRichardy.Federation.TestSuite.Integration.Security;
4+
5+
public sealed class KeyRotationIntegrationTests(IntegrationEnvironmentFixture factory) :
6+
IClassFixture<IntegrationEnvironmentFixture>
7+
{
8+
[Fact(DisplayName = "[e2e] - when rotating keys should publish new kid on realm jwks")]
9+
public async Task WhenRotateKeys_ShouldPublishNewKidOnRealmJwks()
10+
{
11+
var httpClient = factory.HttpClient.WithRealmHeader("master");
12+
var rotationService = factory.Services.GetRequiredService<ISecretRotationService>();
13+
14+
var authBefore = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", new AuthenticationCredentials
15+
{
16+
Username = "federation.testing.user",
17+
Password = "federation.testing.password"
18+
});
19+
20+
var authentication = await authBefore.Content.ReadFromJsonAsync<AuthenticationResult>();
21+
22+
Assert.NotNull(authentication);
23+
Assert.False(string.IsNullOrWhiteSpace(authentication.AccessToken));
24+
25+
var handlerBefore = new JwtSecurityTokenHandler();
26+
27+
var jwtBefore = handlerBefore.ReadJwtToken(authentication.AccessToken);
28+
var kidBefore = jwtBefore.Header.Kid;
29+
30+
Assert.False(string.IsNullOrWhiteSpace(kidBefore));
31+
32+
var realmCollection = factory.Services.GetRequiredService<IRealmCollection>();
33+
var realmFilters = RealmFilters.WithSpecifications()
34+
.WithName("master")
35+
.Build();
36+
37+
var realms = await realmCollection.GetRealmsAsync(realmFilters, CancellationToken.None);
38+
var realm = realms.FirstOrDefault();
39+
40+
Assert.NotNull(realm);
41+
42+
await rotationService.RotateSecretAsync(realm, CancellationToken.None);
43+
44+
string? kidAfter = null;
45+
46+
var started = DateTimeOffset.UtcNow;
47+
48+
while (DateTimeOffset.UtcNow - started < TimeSpan.FromSeconds(10))
49+
{
50+
var authAfter = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", new AuthenticationCredentials
51+
{
52+
Username = "federation.testing.user",
53+
Password = "federation.testing.password"
54+
});
55+
56+
var authenticationResult = await authAfter.Content.ReadFromJsonAsync<AuthenticationResult>();
57+
58+
Assert.NotNull(authenticationResult);
59+
60+
var handlerAfter = new JwtSecurityTokenHandler();
61+
var jwtAfter = handlerAfter.ReadJwtToken(authenticationResult.AccessToken);
62+
63+
kidAfter = jwtAfter.Header.Kid;
64+
65+
if (!string.Equals(kidAfter, kidBefore, StringComparison.Ordinal))
66+
break;
67+
68+
await Task.Delay(300);
69+
}
70+
71+
Assert.False(string.IsNullOrWhiteSpace(kidAfter));
72+
Assert.NotEqual(kidBefore, kidAfter);
73+
74+
var jwksResponse = await httpClient.GetAsync("master/.well-known/jwks.json");
75+
var jwksRaw = await jwksResponse.Content.ReadAsStringAsync();
76+
77+
using var jwks = JsonDocument.Parse(jwksRaw);
78+
79+
Assert.True(jwks.RootElement.TryGetProperty("keys", out var keys));
80+
Assert.Equal(JsonValueKind.Array, keys.ValueKind);
81+
82+
var jwksKids = keys.EnumerateArray()
83+
.Where(key => key.TryGetProperty("kid", out _))
84+
.Select(key => key.GetProperty("kid").GetString())
85+
.Where(key => !string.IsNullOrWhiteSpace(key))
86+
.Cast<string>()
87+
.ToArray();
88+
89+
Assert.Contains(kidAfter!, jwksKids);
90+
}
91+
92+
[Fact(DisplayName = "[e2e] - when rotating keys should keep old token valid while old key is published")]
93+
public async Task WhenRotateKeys_ShouldKeepOldTokenValidWhileOldKeyIsPublished()
94+
{
95+
var httpClient = factory.HttpClient.WithRealmHeader("master");
96+
97+
var rotationService = factory.Services.GetRequiredService<ISecretRotationService>();
98+
var realmCollection = factory.Services.GetRequiredService<IRealmCollection>();
99+
100+
var authBefore = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", new AuthenticationCredentials
101+
{
102+
Username = "federation.testing.user",
103+
Password = "federation.testing.password"
104+
});
105+
106+
var authentication = await authBefore.Content.ReadFromJsonAsync<AuthenticationResult>();
107+
108+
Assert.NotNull(authentication);
109+
Assert.NotEmpty(authentication.AccessToken);
110+
111+
var oldToken = authentication.AccessToken;
112+
var oldJwt = new JwtSecurityTokenHandler().ReadJwtToken(oldToken);
113+
var oldKid = oldJwt.Header.Kid;
114+
115+
Assert.False(string.IsNullOrWhiteSpace(oldKid));
116+
117+
var realmFilters = RealmFilters.WithSpecifications()
118+
.WithName("master")
119+
.Build();
120+
121+
var realms = await realmCollection.GetRealmsAsync(realmFilters, CancellationToken.None);
122+
var realm = realms.FirstOrDefault();
123+
124+
Assert.NotNull(realm);
125+
126+
await rotationService.RotateSecretAsync(realm, CancellationToken.None);
127+
128+
string? newKid = null;
129+
var started = DateTimeOffset.UtcNow;
130+
131+
while (DateTimeOffset.UtcNow - started < TimeSpan.FromSeconds(10))
132+
{
133+
var authAfter = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", new AuthenticationCredentials
134+
{
135+
Username = "federation.testing.user",
136+
Password = "federation.testing.password"
137+
});
138+
139+
var authPayloadAfter = await authAfter.Content.ReadFromJsonAsync<AuthenticationResult>();
140+
141+
Assert.NotNull(authPayloadAfter);
142+
143+
var jwtAfter = new JwtSecurityTokenHandler().ReadJwtToken(authPayloadAfter.AccessToken);
144+
145+
newKid = jwtAfter.Header.Kid;
146+
147+
if (!string.Equals(oldKid, newKid, StringComparison.Ordinal))
148+
break;
149+
150+
await Task.Delay(300);
151+
}
152+
153+
Assert.False(string.IsNullOrWhiteSpace(newKid));
154+
155+
var jwksResponse = await httpClient.GetAsync("master/.well-known/jwks.json");
156+
var jwksRaw = await jwksResponse.Content.ReadAsStringAsync();
157+
158+
using var jwks = JsonDocument.Parse(jwksRaw);
159+
160+
Assert.True(jwks.RootElement.TryGetProperty("keys", out var keys));
161+
162+
var signingKeys = keys.EnumerateArray()
163+
.Select(key => new JsonWebKey(key.GetRawText()) as SecurityKey)
164+
.ToArray();
165+
166+
var validationParameters = new TokenValidationParameters
167+
{
168+
ValidateIssuerSigningKey = true,
169+
IssuerSigningKeys = signingKeys,
170+
ValidateIssuer = false,
171+
ValidateAudience = false,
172+
ValidateLifetime = false,
173+
RequireSignedTokens = true
174+
};
175+
176+
var tokenHandler = new JwtSecurityTokenHandler();
177+
var validationSucceeded = true;
178+
179+
try
180+
{
181+
tokenHandler.ValidateToken(oldToken, validationParameters, out _);
182+
}
183+
catch
184+
{
185+
validationSucceeded = false;
186+
}
187+
188+
Assert.True(validationSucceeded);
189+
}
190+
}

0 commit comments

Comments
 (0)