-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathConnectEndpointTests.cs
More file actions
388 lines (300 loc) · 16 KB
/
Copy pathConnectEndpointTests.cs
File metadata and controls
388 lines (300 loc) · 16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
namespace HttpsRichardy.Federation.TestSuite.Integration.Endpoints;
public sealed class ConnectEndpointTests(IntegrationEnvironmentFixture factory) :
IClassFixture<IntegrationEnvironmentFixture>
{
private readonly Fixture _fixture = new();
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with valid client credentials should return access token")]
public async Task WhenPostTokenWithValidClientCredentials_ShouldReturnAccessToken()
{
/* arrange: authenticate user and get access token */
var clientCollection = factory.Services.GetRequiredService<IClientCollection>();
var httpClient = factory.HttpClient.WithRealmHeader("master");
var userCredentials = new AuthenticationCredentials
{
Username = "federation.testing.user",
Password = "federation.testing.password"
};
var authenticationResponse = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", userCredentials);
var authentication = await authenticationResponse.Content.ReadFromJsonAsync<AuthenticationResult>();
Assert.NotNull(authentication);
Assert.NotEmpty(authentication.AccessToken);
httpClient.WithAuthorization(authentication.AccessToken);
/* arrange: create a client */
var payload = _fixture.Build<ClientCreationScheme>()
.With(client => client.Name, "root")
.With(client => client.Flows, [Grant.ClientCredentials])
.With(client => client.RedirectUris, [])
.Create();
var response = await httpClient.PostAsJsonAsync("api/v1/clients", payload);
Assert.NotNull(response);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var filters = ClientFilters.WithSpecifications()
.WithName(payload.Name)
.Build();
var clients = await clientCollection.GetClientsAsync(filters);
var client = clients.FirstOrDefault();
Assert.NotEmpty(clients);
Assert.NotNull(client);
/* arrange: prepare client credentials */
var credentials = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", client.ClientId },
{ "client_secret", client.Secret }
};
var content = new FormUrlEncodedContent(credentials);
var connectClient = factory.HttpClient;
/* act: send POST request to token endpoint */
var httpResponse = await connectClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
var grantedToken = await httpResponse.Content.ReadFromJsonAsync<ClientAuthenticationResult>();
/* assert: response should be 200 OK */
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(grantedToken);
Assert.False(string.IsNullOrWhiteSpace(grantedToken.AccessToken));
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with non-existent client should return 401 #ERROR-0AF50")]
public async Task WhenPostTokenWithNonExistentClient_ShouldReturnUnauthorized()
{
/* arrange: prepare credentials with non-existent client */
var httpClient = factory.HttpClient;
var credentials = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"non-existent-client-{Guid.NewGuid()}" },
{ "client_secret", "some-secret" }
};
var content = new FormUrlEncodedContent(credentials);
/* act: send POST request with non-existent client */
var response = await httpClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
var error = await response.Content.ReadFromJsonAsync<Error>();
/* assert: response should be 401 Unauthorized */
Assert.NotNull(error);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Assert.Equal(AuthenticationErrors.ClientNotFound, error);
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with invalid client secret should return 401 #ERROR-A7E7C")]
public async Task WhenPostTokenWithInvalidClientSecret_ShouldReturnUnauthorized()
{
/* arrange: authenticate user and get access token */
var clientCollection = factory.Services.GetRequiredService<IClientCollection>();
var httpClient = factory.HttpClient.WithRealmHeader("master");
var userCredentials = new AuthenticationCredentials
{
Username = "federation.testing.user",
Password = "federation.testing.password"
};
var authenticationResponse = await httpClient.PostAsJsonAsync("api/v1/identity/authenticate", userCredentials);
var authentication = await authenticationResponse.Content.ReadFromJsonAsync<AuthenticationResult>();
Assert.NotNull(authentication);
httpClient.WithAuthorization(authentication.AccessToken);
/* arrange: create a client */
var payload = _fixture.Build<ClientCreationScheme>()
.With(client => client.Name, "nexus")
.With(client => client.Flows, [Grant.ClientCredentials])
.With(client => client.RedirectUris, [])
.Create();
var response = await httpClient.PostAsJsonAsync("api/v1/clients", payload);
Assert.NotNull(response);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
var filters = ClientFilters.WithSpecifications()
.WithName(payload.Name)
.Build();
var clients = await clientCollection.GetClientsAsync(filters);
var client = clients.FirstOrDefault();
Assert.NotEmpty(clients);
Assert.NotNull(client);
/* arrange: prepare credentials with wrong secret */
var credentials = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", client.ClientId },
{ "client_secret", "wrong-secret" }
};
/* act: send POST request with invalid secret */
var content = new FormUrlEncodedContent(credentials);
var connectClient = factory.HttpClient;
var httpResponse = await connectClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
var error = await httpResponse.Content.ReadFromJsonAsync<Error>();
/* assert: response should be 401 Unauthorized */
Assert.NotNull(error);
Assert.Equal(HttpStatusCode.Unauthorized, httpResponse.StatusCode);
Assert.Equal(AuthenticationErrors.InvalidClientCredentials, error);
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with missing grant_type should return 400")]
public async Task WhenPostTokenWithMissingGrantType_ShouldReturnBadRequest()
{
/* arrange: prepare credentials without grant_type */
var httpClient = factory.HttpClient;
var credentials = new Dictionary<string, string>
{
{ "client_id", "test-client-id" },
{ "client_secret", "test-client-secret" }
};
/* act: send POST request without grant_type */
var content = new FormUrlEncodedContent(credentials);
var response = await httpClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
/* assert: response should be 400 Bad Request */
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with missing client_id should return 400")]
public async Task WhenPostTokenWithMissingClientId_ShouldReturnBadRequest()
{
/* arrange: prepare credentials without client_id */
var httpClient = factory.HttpClient;
var credentials = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_secret", "test-client-secret" }
};
/* act: send POST request without client_id */
var content = new FormUrlEncodedContent(credentials);
var response = await httpClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
/* assert: response should be 400 Bad Request */
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with missing client_secret should return 400")]
public async Task WhenPostTokenWithMissingClientSecret_ShouldReturnBadRequest()
{
/* arrange: prepare credentials without client_secret */
var httpClient = factory.HttpClient;
var credentials = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", "test-client-id" }
};
/* act: send POST request without client_secret */
var content = new FormUrlEncodedContent(credentials);
var response = await httpClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
/* assert: response should be 400 Bad Request */
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact(DisplayName = "[e2e] - when POST /openid/connect/token with valid authorization_code should return access token")]
public async Task WhenPostTokenWithValidAuthorizationCode_ShouldReturnAccessToken()
{
// arrange: resolve required dependencies
var tokenCollection = factory.Services.GetRequiredService<ITokenCollection>();
var userCollection = factory.Services.GetRequiredService<IUserCollection>();
var clientCollection = factory.Services.GetRequiredService<IClientCollection>();
// arrange: authenticate as master to create client and realm
var masterClient = factory.HttpClient.WithRealmHeader("master");
var masterCredentials = new AuthenticationCredentials
{
Username = "federation.testing.user",
Password = "federation.testing.password"
};
var authentication = await masterClient.PostAsJsonAsync("api/v1/identity/authenticate", masterCredentials);
var grantedToken = await authentication.Content.ReadFromJsonAsync<AuthenticationResult>();
Assert.NotNull(grantedToken);
Assert.NotEmpty(grantedToken.AccessToken);
masterClient.WithAuthorization(grantedToken.AccessToken);
// arrange: create realm
var realmPayload = _fixture.Build<RealmCreationScheme>()
.With(realm => realm.Name, $"test-realm-{Guid.NewGuid()}")
.With(realm => realm.Description, $"test-description-{Guid.NewGuid()}")
.Create();
var realmResponse = await masterClient.PostAsJsonAsync("api/v1/realms", realmPayload);
var realm = await realmResponse.Content.ReadFromJsonAsync<RealmDetailsScheme>();
Assert.NotNull(realm);
Assert.Equal(HttpStatusCode.Created, realmResponse.StatusCode);
// arrange: create client for authorization_code grant
var realmMasterClient = factory.HttpClient.WithRealmHeader(realm.Name);
var realmAuth = await realmMasterClient.PostAsJsonAsync("api/v1/identity/authenticate", masterCredentials);
var realmAdminClient = factory.HttpClient
.WithRealmHeader(realm.Name)
.WithAuthorization(grantedToken.AccessToken);
var payload = _fixture.Build<ClientCreationScheme>()
.With(client => client.Name, "root")
.With(client => client.Flows, [Grant.ClientCredentials])
.With(client => client.RedirectUris, [])
.Create();
var httpResponse = await realmAdminClient.PostAsJsonAsync("api/v1/clients", payload);
Assert.NotNull(httpResponse);
Assert.Equal(HttpStatusCode.Created, httpResponse.StatusCode);
var clientFilters = ClientFilters.WithSpecifications()
.WithName(payload.Name)
.Build();
var clients = await clientCollection.GetClientsAsync(clientFilters);
var client = clients.FirstOrDefault();
Assert.NotEmpty(clients);
Assert.NotNull(client);
// arrange: assign client audience
var assignAudience = new AssignClientAudienceScheme { Value = "backend.api" };
var assignAudienceResponse = await realmAdminClient.PostAsJsonAsync($"api/v1/clients/{client.Id}/audiences", assignAudience);
Assert.Equal(HttpStatusCode.OK, assignAudienceResponse.StatusCode);
// arrange: create user for realm
var credentials = new IdentityEnrollmentCredentials
{
Username = $"user.{Guid.NewGuid()}@email.com",
Password = "TestPassword123!"
};
var realmClient = factory.HttpClient.WithRealmHeader(realm.Name);
var enrollment = await realmClient.PostAsJsonAsync("api/v1/identity", credentials);
var identity = await enrollment.Content.ReadFromJsonAsync<UserDetailsScheme>();
Assert.NotNull(identity);
Assert.Equal(HttpStatusCode.Created, enrollment.StatusCode);
// arrange: authenticate new user
var authenticationCredentials = new AuthenticationCredentials
{
Username = credentials.Username,
Password = credentials.Password
};
var authenticationResponse = await realmClient.PostAsJsonAsync("api/v1/identity/authenticate", authenticationCredentials);
var authenticationResult = await authenticationResponse.Content.ReadFromJsonAsync<AuthenticationResult>();
Assert.NotNull(authenticationResult);
Assert.NotEmpty(authenticationResult.AccessToken);
realmClient.WithAuthorization(authenticationResult.AccessToken);
// arrange: generate PKCE
var codeVerifier = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
var codeChallenge = Application.Utilities.Base64UrlEncoder.Encode(SHA256.HashData(System.Text.Encoding.ASCII.GetBytes(codeVerifier)));
var codeChallengeMethod = "S256";
// arrange: get user from db
var filters = UserFilters.WithSpecifications()
.WithUsername(credentials.Username)
.Build();
var users = await userCollection.GetUsersAsync(filters);
var user = users.FirstOrDefault();
Assert.NotEmpty(users);
Assert.NotNull(user);
// arrange: create authorization code token
var authorizationCode = Guid.NewGuid().ToString("N");
var token = new Domain.Aggregates.SecurityToken
{
Value = authorizationCode,
UserId = user.Id,
RealmId = realm.Id,
Type = TokenType.AuthorizationCode,
ExpiresAt = DateTime.UtcNow.AddMinutes(5),
Metadata = new Dictionary<string, string>
{
["client.id"] = client.ClientId,
["code.challenge"] = codeChallenge,
["code.challenge.method"] = codeChallengeMethod
}
};
await tokenCollection.InsertAsync(token);
// arrange: prepare authorization_code grant request
var parameters = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "code", authorizationCode },
{ "client_id", client.ClientId },
{ "code_verifier", codeVerifier }
};
var content = new FormUrlEncodedContent(parameters);
var connectClient = factory.HttpClient.WithRealmHeader(realm.Name);
// act: send POST request to token endpoint
var response = await connectClient.PostAsync("api/v1/protocol/open-id/connect/token", content);
var grant = await response.Content.ReadFromJsonAsync<ClientAuthenticationResult>();
// assert: response should be 200 OK
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(grant);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.ReadJwtToken(grant.AccessToken);
var audiences = jwt.Claims
.Where(claim => claim.Type == JwtRegisteredClaimNames.Aud)
.Select(claim => claim.Value)
.ToList();
Assert.Contains("backend.api", audiences);
Assert.DoesNotContain(realm.Name, audiences);
}
}