diff --git a/src/Security/Authentication/OAuth/src/OAuthHandler.cs b/src/Security/Authentication/OAuth/src/OAuthHandler.cs index 1a6937731873..e75b09358295 100644 --- a/src/Security/Authentication/OAuth/src/OAuthHandler.cs +++ b/src/Security/Authentication/OAuth/src/OAuthHandler.cs @@ -303,12 +303,12 @@ protected virtual string BuildChallengeUrl(AuthenticationProperties properties, var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope(); var parameters = new Dictionary<string, string> - { - { "client_id", Options.ClientId }, - { "scope", scope }, - { "response_type", "code" }, - { "redirect_uri", redirectUri }, - }; + { + { "client_id", Options.ClientId }, + { "scope", scope }, + { "response_type", "code" }, + { "redirect_uri", redirectUri }, + }; if (Options.UsePkce) { @@ -328,6 +328,11 @@ protected virtual string BuildChallengeUrl(AuthenticationProperties properties, parameters["state"] = Options.StateDataFormat.Protect(properties); + foreach (var additionalParameter in Options.AdditionalAuthorizationParameters) + { + parameters.Add(additionalParameter.Key, additionalParameter.Value); + } + return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters!); } diff --git a/src/Security/Authentication/OAuth/src/OAuthOptions.cs b/src/Security/Authentication/OAuth/src/OAuthOptions.cs index c0e30d9c4e51..95a100e32de4 100644 --- a/src/Security/Authentication/OAuth/src/OAuthOptions.cs +++ b/src/Security/Authentication/OAuth/src/OAuthOptions.cs @@ -83,6 +83,16 @@ public override void Validate() /// </summary> public ICollection<string> Scope { get; } = new HashSet<string>(); + /// <summary> + /// Gets the additional parameters that will be included in the authorization request. + /// </summary> + /// <remarks> + /// The additional parameters can be used to customize the authorization request, + /// providing extra information or fulfilling specific requirements of the OAuth provider. + /// These parameters are typically, but not always, appended to the query string. + /// </remarks> + public IDictionary<string, string> AdditionalAuthorizationParameters { get; } = new Dictionary<string, string>(); + /// <summary> /// Gets or sets the type used to secure data handled by the middleware. /// </summary> diff --git a/src/Security/Authentication/OAuth/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OAuth/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..bc2cd3195c2c 100644 --- a/src/Security/Authentication/OAuth/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/OAuth/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Authentication.OAuth.OAuthOptions.AdditionalAuthorizationParameters.get -> System.Collections.Generic.IDictionary<string!, string!>! diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs index 4b074f5621e3..f0b93c306727 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs @@ -450,6 +450,11 @@ private async Task HandleChallengeAsyncInternal(AuthenticationProperties propert GenerateCorrelationId(properties); + foreach (var additionalParameter in Options.AdditionalAuthorizationParameters) + { + message.Parameters.Add(additionalParameter.Key, additionalParameter.Value); + } + var redirectContext = new RedirectContext(Context, Scheme, Options, properties) { ProtocolMessage = message diff --git a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs index 7b72ad5c48c1..f25095862518 100644 --- a/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs +++ b/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs @@ -236,6 +236,16 @@ public override void Validate() /// </summary> public ICollection<string> Scope { get; } = new HashSet<string>(); + /// <summary> + /// Gets the additional parameters that will be included in the authorization request. + /// </summary> + /// <remarks> + /// The additional parameters can be used to customize the authorization request, + /// providing extra information or fulfilling specific requirements of the OpenIdConnect provider. + /// These parameters are typically, but not always, appended to the query string. + /// </remarks> + public IDictionary<string, string> AdditionalAuthorizationParameters { get; } = new Dictionary<string, string>(); + /// <summary> /// Requests received on this path will cause the handler to invoke SignOut using the SignOutScheme. /// </summary> diff --git a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..49118745ed9c 100644 --- a/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/OpenIdConnect/src/PublicAPI.Unshipped.txt @@ -1 +1,2 @@ #nullable enable +Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.AdditionalAuthorizationParameters.get -> System.Collections.Generic.IDictionary<string!, string!>! diff --git a/src/Security/Authentication/test/OAuthTests.cs b/src/Security/Authentication/test/OAuthTests.cs index 3d49bc3bc8eb..43463f2e15bf 100644 --- a/src/Security/Authentication/test/OAuthTests.cs +++ b/src/Security/Authentication/test/OAuthTests.cs @@ -193,6 +193,32 @@ public async Task RedirectToAuthorizeEndpoint_HasScopeAsConfigured() Assert.Contains("scope=foo%20bar", res.Headers.Location.Query); } + [Fact] + public async Task RedirectToAuthorizeEndpoint_HasAdditionalAuthorizationParametersAsConfigured() + { + using var host = await CreateHost( + s => s.AddAuthentication(o => o.DisableAutoDefaultScheme = true).AddOAuth( + "Weblie", + opt => + { + ConfigureDefaults(opt); + opt.AdditionalAuthorizationParameters.Add("prompt", "login"); + opt.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com"); + }), + async ctx => + { + await ctx.ChallengeAsync("Weblie"); + return true; + }); + + using var server = host.GetTestServer(); + var transaction = await server.SendAsync("https://www.example.com/challenge"); + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.Contains("prompt=login&audience=https%3A%2F%2Fapi.example.com", res.Headers.Location.Query); + } + [Fact] public async Task RedirectToAuthorizeEndpoint_HasScopeAsOverwritten() { diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs index 1870cc7f75d4..f8bfc8a9fb4f 100644 --- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs +++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -670,4 +670,25 @@ public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationProper settings.ValidateChallengeRedirect(res.Headers.Location); Assert.Contains("max_age=1234", res.Headers.Location.Query); } + + [Fact] + public async Task Challenge_WithAdditionalAuthorizationParameters() + { + var settings = new TestSettings(opt => + { + opt.ClientId = "Test Id"; + opt.Authority = TestServerBuilder.DefaultAuthority; + opt.AdditionalAuthorizationParameters.Add("prompt", "login"); + opt.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com"); + }); + + var server = settings.CreateTestServer(); + var transaction = await server.SendAsync(TestServerBuilder.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + settings.ValidateChallengeRedirect(res.Headers.Location); + Assert.Contains("prompt=login&audience=https%3A%2F%2Fapi.example.com", res.Headers.Location.Query); + } }