Skip to content

Commit bc3f423

Browse files
committed
Add a functional test for PAR-enabled authorization flows
1 parent 1cca51c commit bc3f423

File tree

1 file changed

+182
-8
lines changed

1 file changed

+182
-8
lines changed

test/OrchardCore.Tests/Modules/OrchardCore.OpenId/OpenIdAuthenticationTests.cs

Lines changed: 182 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,15 @@ await context.UsingTenantScopeAsync(async scope =>
117117
Assert.Contains("orchauth_" + shellSettings.Name, cookies.Keys);
118118

119119
var codeVerifier = GenerateCodeVerifier();
120-
var challengeCode = GenerateCodeChallenge(codeVerifier);
120+
var codeChallenge = GenerateCodeChallenge(codeVerifier);
121121
var requestData = new Dictionary<string, string>
122122
{
123123
{ "client_id", clientId },
124124
{ "response_type", "code" },
125125
{ "redirect_uri", redirectUri },
126126
{ "scope", "openid offline_access" },
127127
{ "code_challenge_method", "S256" },
128-
{ "code_verifier", codeVerifier },
129-
{ "code_challenge", challengeCode },
128+
{ "code_challenge", codeChallenge },
130129
};
131130

132131
var authorizeRequestMessage = HttpRequestHelper.CreatePostMessage("connect/authorize", requestData);
@@ -163,17 +162,163 @@ await context.UsingTenantScopeAsync(async scope =>
163162
var tokens = new ConcurrentBag<string>();
164163

165164
// One one task should succeed since OpenId will only allow one access_token exchange for every authorization_code.
166-
var taskOne = ExchangeCodeForTokenAsync(httpClient, cookies, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
167-
var taskTwo = ExchangeCodeForTokenAsync(httpClient, cookies, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
168-
var taskThree = ExchangeCodeForTokenAsync(httpClient, cookies, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
165+
var taskOne = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
166+
var taskTwo = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
167+
var taskThree = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
169168

170169
await Task.WhenAll(taskOne, taskTwo, taskThree);
171170

172171
Assert.Single(tokens);
173172
});
174173
}
175174

176-
private static async Task ExchangeCodeForTokenAsync(HttpClient httpClient, IDictionary<string, string> cookies, string authorizationCode, string clientId, string redirectUri, string codeVerifier, ConcurrentBag<string> tokens)
175+
[Fact]
176+
public async Task OpenId_CodeFlowWithPushedAuthorizationRequests_CanExchangeAuthorizationCodeForAccessTokenOnlyOnce()
177+
{
178+
var context = new SiteContext();
179+
180+
await context.InitializeAsync();
181+
182+
var redirectUri = context.Client.BaseAddress.ToString() + "signin-oidc";
183+
184+
var clientId = "test_id";
185+
186+
var recipeSteps = new JsonArray
187+
{
188+
new JsonObject
189+
{
190+
{"name", "Feature"},
191+
{"enable", new JsonArray(
192+
"OrchardCore.Users",
193+
"OrchardCore.OpenId.Server",
194+
"OrchardCore.OpenId.Validation",
195+
"OrchardCore.OpenId")
196+
},
197+
},
198+
new JsonObject
199+
{
200+
{"name", "OpenIdApplication"},
201+
{"ClientId", clientId},
202+
{"DisplayName", "Test Application"},
203+
{"Type", "public"},
204+
{"ConsentType", "implicit"},
205+
{"AllowAuthorizationCodeFlow", true},
206+
{"RequireProofKeyForCodeExchange", true},
207+
{"RequirePushedAuthorizationRequests", true},
208+
{"AllowRefreshTokenFlow", true},
209+
{"RedirectUris", redirectUri},
210+
},
211+
};
212+
213+
var recipe = new JsonObject
214+
{
215+
{"steps", recipeSteps},
216+
};
217+
218+
await RecipeHelpers.RunRecipeAsync(context, recipe);
219+
220+
await context.UsingTenantScopeAsync(async scope =>
221+
{
222+
var featureManager = scope.ServiceProvider.GetService<IShellFeaturesManager>();
223+
224+
Assert.True(await featureManager.IsFeatureEnabledAsync("OrchardCore.Users"));
225+
Assert.True(await featureManager.IsFeatureEnabledAsync("OrchardCore.OpenId.Server"));
226+
Assert.True(await featureManager.IsFeatureEnabledAsync("OrchardCore.OpenId.Validation"));
227+
Assert.True(await featureManager.IsFeatureEnabledAsync("OrchardCore.OpenId"));
228+
229+
var httpClient = context.Client;
230+
231+
var session = scope.ServiceProvider.GetRequiredService<YesSql.ISession>();
232+
233+
var applications = await session.Query<OpenIdApplication, OpenIdApplicationIndex>(OpenIdApplication.OpenIdCollection).ListAsync();
234+
235+
Assert.Single(applications);
236+
237+
var application = applications.First();
238+
Assert.True(application.ClientId == clientId);
239+
Assert.Contains(redirectUri, application.RedirectUris);
240+
Assert.Equal("implicit", application.ConsentType);
241+
Assert.Contains(OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, application.Permissions);
242+
Assert.Contains(OpenIddictConstants.Permissions.GrantTypes.RefreshToken, application.Permissions);
243+
Assert.Contains(OpenIddictConstants.Permissions.Endpoints.Authorization, application.Permissions);
244+
Assert.Contains(OpenIddictConstants.Permissions.Endpoints.PushedAuthorization, application.Permissions);
245+
Assert.Contains(OpenIddictConstants.Permissions.Endpoints.Token, application.Permissions);
246+
Assert.Contains(OpenIddictConstants.Permissions.ResponseTypes.Code, application.Permissions);
247+
248+
Assert.Contains(OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange, application.Requirements);
249+
Assert.Contains(OpenIddictConstants.Requirements.Features.PushedAuthorizationRequests, application.Requirements);
250+
251+
// Visit the login page to get the AntiForgery token.
252+
var loginGetRequest = await httpClient.GetAsync("Login", CancellationToken.None);
253+
254+
var loginFormData = new Dictionary<string, string>
255+
{
256+
{"__RequestVerificationToken", await AntiForgeryHelper.ExtractAntiForgeryToken(loginGetRequest) },
257+
{$"{nameof(LoginForm)}.{nameof(LoginViewModel.UserName)}", "admin"},
258+
{$"{nameof(LoginForm)}.{nameof(LoginViewModel.Password)}", "Password01_"},
259+
};
260+
261+
var shellSettings = scope.ServiceProvider.GetService<ShellSettings>();
262+
263+
var loginPostRequest = HttpRequestHelper.CreatePostMessageWithCookies($"Login?ReturnUrl=/{shellSettings.RequestUrlPrefix}?loggedIn=true", loginFormData, loginGetRequest);
264+
265+
// Login
266+
var loginPostResponse = await httpClient.SendAsync(loginPostRequest, CancellationToken.None);
267+
268+
Assert.Equal(HttpStatusCode.Redirect, loginPostResponse.StatusCode);
269+
270+
var loginRequestRedirectToLocation = loginPostResponse.Headers.Location?.ToString();
271+
272+
Assert.NotEmpty(loginRequestRedirectToLocation);
273+
Assert.Contains("loggedIn=true", loginRequestRedirectToLocation);
274+
275+
var cookies = CookiesHelper.ExtractCookies(loginPostResponse);
276+
277+
Assert.Contains("orchauth_" + shellSettings.Name, cookies.Keys);
278+
279+
var codeVerifier = GenerateCodeVerifier();
280+
var codeChallenge = GenerateCodeChallenge(codeVerifier);
281+
282+
var requestData = new Dictionary<string, string>
283+
{
284+
{ "client_id", clientId },
285+
{ "request_uri", await GetRequestUriAsync(httpClient, clientId, redirectUri, codeChallenge) }
286+
};
287+
288+
var authorizeRequestMessage = HttpRequestHelper.CreatePostMessage("connect/authorize", requestData);
289+
CookiesHelper.AddCookiesToRequest(authorizeRequestMessage, cookies);
290+
291+
Assert.True(authorizeRequestMessage.Headers.Contains("Cookie"), "Cookie header is missing from request.");
292+
293+
var authorizeResponse = await httpClient.SendAsync(authorizeRequestMessage, CancellationToken.None);
294+
295+
Assert.Equal(HttpStatusCode.Redirect, authorizeResponse.StatusCode);
296+
297+
var finalRedirect = authorizeResponse.Headers.Location?.ToString();
298+
299+
Assert.NotEmpty(finalRedirect);
300+
Assert.StartsWith(redirectUri, finalRedirect);
301+
302+
// Extract the authorization code from the query string.
303+
var queryParameters = HttpUtility.ParseQueryString(new Uri(finalRedirect).Query);
304+
var authorizationCode = queryParameters["code"];
305+
306+
Assert.NotEmpty(authorizationCode);
307+
308+
var tokens = new ConcurrentBag<string>();
309+
310+
// One one task should succeed since OpenId will only allow one access_token exchange for every authorization_code.
311+
var taskOne = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
312+
var taskTwo = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
313+
var taskThree = ExchangeCodeForTokenAsync(httpClient, authorizationCode, clientId, redirectUri, codeVerifier, tokens);
314+
315+
await Task.WhenAll(taskOne, taskTwo, taskThree);
316+
317+
Assert.Single(tokens);
318+
});
319+
}
320+
321+
private static async Task ExchangeCodeForTokenAsync(HttpClient httpClient, string authorizationCode, string clientId, string redirectUri, string codeVerifier, ConcurrentBag<string> tokens)
177322
{
178323
var data = new Dictionary<string, string>()
179324
{
@@ -185,7 +330,6 @@ private static async Task ExchangeCodeForTokenAsync(HttpClient httpClient, IDict
185330
};
186331

187332
var request = HttpRequestHelper.CreatePostMessage("connect/token", data);
188-
CookiesHelper.AddCookiesToRequest(request, cookies);
189333

190334
var tokenResponse = await httpClient.SendAsync(request, CancellationToken.None);
191335

@@ -201,6 +345,36 @@ private static async Task ExchangeCodeForTokenAsync(HttpClient httpClient, IDict
201345
}
202346
}
203347

348+
private static async Task<string> GetRequestUriAsync(HttpClient httpClient, string clientId, string redirectUri, string codeChallenge)
349+
{
350+
var data = new Dictionary<string, string>()
351+
{
352+
{ "client_id", clientId },
353+
{ "response_type", "code" },
354+
{ "redirect_uri", redirectUri },
355+
{ "scope", "openid offline_access" },
356+
{ "code_challenge_method", "S256" },
357+
{ "code_challenge", codeChallenge },
358+
};
359+
360+
var request = HttpRequestHelper.CreatePostMessage("connect/par", data);
361+
362+
var response = await httpClient.SendAsync(request, CancellationToken.None);
363+
if (response.IsSuccessStatusCode)
364+
{
365+
var result = await response.Content.ReadFromJsonAsync<JsonObject>();
366+
var value = result["request_uri"]?.ToString();
367+
368+
Assert.NotEmpty(value);
369+
370+
return value;
371+
}
372+
else
373+
{
374+
throw new InvalidOperationException("An error response was returned by the pushed authorization endpoint.");
375+
}
376+
}
377+
204378
private static string GenerateCodeVerifier()
205379
{
206380
var randomBytes = new byte[32];

0 commit comments

Comments
 (0)