Skip to content

Commit e1f729b

Browse files
committed
Validate prompt values specified in authorization requests and update the configuration endpoint to return "prompt_values_supported"
1 parent fcc0ddd commit e1f729b

File tree

14 files changed

+208
-42
lines changed

14 files changed

+208
-42
lines changed

.editorconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ csharp_using_directive_placement = outside_namespace
9797
dotnet_code_quality_unused_parameters = all
9898
dotnet_diagnostic.CA1510.severity = none
9999
dotnet_diagnostic.CA2254.severity = none
100+
dotnet_diagnostic.IDE0002.severity = none
100101
dotnet_diagnostic.IDE0305.severity = none
101102
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
102103
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i

sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public async Task<ActionResult> Authorize()
139139
// return an authorization response without displaying the consent form.
140140
case ConsentTypes.Implicit:
141141
case ConsentTypes.External when authorizations.Count is not 0:
142-
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(Prompts.Consent):
142+
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(PromptValues.Consent):
143143
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
144144
var identity = new ClaimsIdentity(
145145
authenticationType: OpenIddictServerOwinDefaults.AuthenticationType,
@@ -178,8 +178,8 @@ public async Task<ActionResult> Authorize()
178178

179179
// At this point, no authorization was found in the database and an error must be returned
180180
// if the client application specified prompt=none in the authorization request.
181-
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
182-
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
181+
case ConsentTypes.Explicit when request.HasPrompt(PromptValues.None):
182+
case ConsentTypes.Systematic when request.HasPrompt(PromptValues.None):
183183
context.Authentication.Challenge(
184184
authenticationTypes: OpenIddictServerOwinDefaults.AuthenticationType,
185185
properties: new AuthenticationProperties(new Dictionary<string, string>

sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@ public async Task<IActionResult> Authorize()
7171
// For scenarios where the default authentication handler configured in the ASP.NET Core
7272
// authentication options shouldn't be used, a specific scheme can be specified here.
7373
var result = await HttpContext.AuthenticateAsync();
74-
if (result == null || !result.Succeeded || request.HasPrompt(Prompts.Login) ||
74+
if (result == null || !result.Succeeded || request.HasPrompt(PromptValues.Login) ||
7575
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
7676
DateTimeOffset.UtcNow - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value)))
7777
{
7878
// If the client application requested promptless authentication,
7979
// return an error indicating that the user is not logged in.
80-
if (request.HasPrompt(Prompts.None))
80+
if (request.HasPrompt(PromptValues.None))
8181
{
8282
return Forbid(
8383
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
@@ -90,7 +90,7 @@ public async Task<IActionResult> Authorize()
9090

9191
// To avoid endless login -> authorization redirects, the prompt=login flag
9292
// is removed from the authorization request payload before redirecting the user.
93-
var prompt = string.Join(" ", request.GetPrompts().Remove(Prompts.Login));
93+
var prompt = string.Join(" ", request.GetPrompts().Remove(PromptValues.Login));
9494

9595
var parameters = Request.HasFormContentType ?
9696
Request.Form.Where(parameter => parameter.Key != Parameters.Prompt).ToList() :
@@ -173,7 +173,7 @@ public async Task<IActionResult> Authorize()
173173
// return an authorization response without displaying the consent form.
174174
case ConsentTypes.Implicit:
175175
case ConsentTypes.External when authorizations.Count is not 0:
176-
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(Prompts.Consent):
176+
case ConsentTypes.Explicit when authorizations.Count is not 0 && !request.HasPrompt(PromptValues.Consent):
177177
// Create the claims-based identity that will be used by OpenIddict to generate tokens.
178178
var identity = new ClaimsIdentity(
179179
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
@@ -210,8 +210,8 @@ public async Task<IActionResult> Authorize()
210210

211211
// At this point, no authorization was found in the database and an error must be returned
212212
// if the client application specified prompt=none in the authorization request.
213-
case ConsentTypes.Explicit when request.HasPrompt(Prompts.None):
214-
case ConsentTypes.Systematic when request.HasPrompt(Prompts.None):
213+
case ConsentTypes.Explicit when request.HasPrompt(PromptValues.None):
214+
case ConsentTypes.Systematic when request.HasPrompt(PromptValues.None):
215215
return Forbid(
216216
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
217217
properties: new AuthenticationProperties(new Dictionary<string, string>

src/OpenIddict.Abstractions/OpenIddictConstants.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ public static class Metadata
295295
public const string MtlsEndpointAliases = "mtls_endpoint_aliases";
296296
public const string OpPolicyUri = "op_policy_uri";
297297
public const string OpTosUri = "op_tos_uri";
298+
public const string PromptValuesSupported = "prompt_values_supported";
298299
public const string RequestObjectEncryptionAlgValuesSupported = "request_object_encryption_alg_values_supported";
299300
public const string RequestObjectEncryptionEncValuesSupported = "request_object_encryption_enc_values_supported";
300301
public const string RequestObjectSigningAlgValuesSupported = "request_object_signing_alg_values_supported";
@@ -430,9 +431,10 @@ public static class Scopes
430431
}
431432
}
432433

433-
public static class Prompts
434+
public static class PromptValues
434435
{
435436
public const string Consent = "consent";
437+
public const string Create = "create";
436438
public const string Login = "login";
437439
public const string None = "none";
438440
public const string SelectAccount = "select_account";

src/OpenIddict.Abstractions/OpenIddictResources.resx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,6 @@ Consider using 'options.AddSigningCredentials(SigningCredentials)' instead.</val
365365
<data name="ID0072" xml:space="preserve">
366366
<value>Endpoint URIs must be valid URIs.</value>
367367
</data>
368-
<data name="ID0073" xml:space="preserve">
369-
<value>Claims cannot be null or empty.</value>
370-
</data>
371-
<data name="ID0074" xml:space="preserve">
372-
<value>Scopes cannot be null or empty.</value>
373-
</data>
374368
<data name="ID0075" xml:space="preserve">
375369
<value>The security token handler cannot be null.</value>
376370
</data>
@@ -1704,6 +1698,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
17041698
<data name="ID0456" xml:space="preserve">
17051699
<value>The specified client authentication method/token binding methods combination is not valid.</value>
17061700
</data>
1701+
<data name="ID0457" xml:space="preserve">
1702+
<value>The '{0}' parameter cannot contain null or empty values.</value>
1703+
</data>
17071704
<data name="ID2000" xml:space="preserve">
17081705
<value>The security token is missing.</value>
17091706
</data>
@@ -2379,7 +2376,7 @@ The principal used to create the token contained the following claims: {Claims}.
23792376
<value>The authorization request was rejected because the '{Scope}' scope was missing.</value>
23802377
</data>
23812378
<data name="ID6040" xml:space="preserve">
2382-
<value>The authorization request was rejected because an invalid prompt parameter was specified.</value>
2379+
<value>The authorization request was rejected because an invalid prompt combination was specified.</value>
23832380
</data>
23842381
<data name="ID6041" xml:space="preserve">
23852382
<value>The authorization request was rejected because the specified code challenge method was not supported.</value>
@@ -2892,6 +2889,9 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
28922889
<data name="ID6232" xml:space="preserve">
28932890
<value>An error was returned by ASWebAuthenticationSession while trying to start a sign-out operation.</value>
28942891
</data>
2892+
<data name="ID6233" xml:space="preserve">
2893+
<value>The authorization request was rejected because an unsupported prompt parameter was specified.</value>
2894+
</data>
28952895
<data name="ID8000" xml:space="preserve">
28962896
<value>https://documentation.openiddict.com/errors/{0}</value>
28972897
</data>

src/OpenIddict.Server/OpenIddictServerBuilder.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,12 +1634,33 @@ public OpenIddictServerBuilder RegisterClaims(params string[] claims)
16341634

16351635
if (Array.Exists(claims, string.IsNullOrEmpty))
16361636
{
1637-
throw new ArgumentException(SR.GetResourceString(SR.ID0073), nameof(claims));
1637+
throw new ArgumentException(SR.FormatID0457(nameof(claims)), nameof(claims));
16381638
}
16391639

16401640
return Configure(options => options.Claims.UnionWith(claims));
16411641
}
16421642

1643+
/// <summary>
1644+
/// Registers the specified prompt values as supported scopes so
1645+
/// they can be returned as part of the discovery document.
1646+
/// </summary>
1647+
/// <param name="values">The supported prompt values.</param>
1648+
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
1649+
public OpenIddictServerBuilder RegisterPromptValues(params string[] values)
1650+
{
1651+
if (values is null)
1652+
{
1653+
throw new ArgumentNullException(nameof(values));
1654+
}
1655+
1656+
if (Array.Exists(values, string.IsNullOrEmpty))
1657+
{
1658+
throw new ArgumentException(SR.FormatID0457(nameof(values)), nameof(values));
1659+
}
1660+
1661+
return Configure(options => options.PromptValues.UnionWith(values));
1662+
}
1663+
16431664
/// <summary>
16441665
/// Registers the specified scopes as supported scopes so
16451666
/// they can be returned as part of the discovery document.
@@ -1655,7 +1676,7 @@ public OpenIddictServerBuilder RegisterScopes(params string[] scopes)
16551676

16561677
if (Array.Exists(scopes, string.IsNullOrEmpty))
16571678
{
1658-
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
1679+
throw new ArgumentException(SR.FormatID0457(nameof(scopes)), nameof(scopes));
16591680
}
16601681

16611682
return Configure(options => options.Scopes.UnionWith(scopes));

src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ public OpenIddictRequest Request
166166
/// </summary>
167167
public HashSet<string> IntrospectionEndpointAuthenticationMethods { get; } = new(StringComparer.Ordinal);
168168

169+
/// <summary>
170+
/// Gets the list of prompt values supported by the authorization server.
171+
/// </summary>
172+
public HashSet<string> PromptValues { get; } = new(StringComparer.Ordinal);
173+
169174
/// <summary>
170175
/// Gets the list of response modes
171176
/// supported by the authorization server.

src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -875,10 +875,33 @@ public ValueTask HandleAsync(ValidateAuthorizationRequestContext context)
875875
throw new ArgumentNullException(nameof(context));
876876
}
877877

878+
if (string.IsNullOrEmpty(context.Request.Prompt))
879+
{
880+
return default;
881+
}
882+
883+
// Reject requests specifying an unsupported prompt value.
884+
// See https://openid.net/specs/openid-connect-prompt-create-1_0.html#section-4.1 for more information.
885+
foreach (var value in context.Request.GetPrompts().ToHashSet(StringComparer.Ordinal))
886+
{
887+
if (!context.Options.PromptValues.Contains(value))
888+
{
889+
context.Logger.LogInformation(SR.GetResourceString(SR.ID6233));
890+
891+
context.Reject(
892+
error: Errors.InvalidRequest,
893+
description: SR.FormatID2032(Parameters.Prompt),
894+
uri: SR.FormatID8000(SR.ID2032));
895+
896+
return default;
897+
}
898+
}
899+
878900
// Reject requests specifying prompt=none with consent/login or select_account.
879-
if (context.Request.HasPrompt(Prompts.None) && (context.Request.HasPrompt(Prompts.Consent) ||
880-
context.Request.HasPrompt(Prompts.Login) ||
881-
context.Request.HasPrompt(Prompts.SelectAccount)))
901+
// See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
902+
if (context.Request.HasPrompt(PromptValues.None) && (context.Request.HasPrompt(PromptValues.Consent) ||
903+
context.Request.HasPrompt(PromptValues.Login) ||
904+
context.Request.HasPrompt(PromptValues.SelectAccount)))
882905
{
883906
context.Logger.LogInformation(SR.GetResourceString(SR.ID6040));
884907

src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static class Discovery
4141
AttachScopes.Descriptor,
4242
AttachClaims.Descriptor,
4343
AttachSubjectTypes.Descriptor,
44+
AttachPromptValues.Descriptor,
4445
AttachSigningAlgorithms.Descriptor,
4546
AttachAdditionalMetadata.Descriptor,
4647

@@ -250,6 +251,7 @@ public async ValueTask HandleAsync(ProcessRequestContext context)
250251
[Metadata.IdTokenSigningAlgValuesSupported] = notification.IdTokenSigningAlgorithms.ToArray(),
251252
[Metadata.CodeChallengeMethodsSupported] = notification.CodeChallengeMethods.ToArray(),
252253
[Metadata.SubjectTypesSupported] = notification.SubjectTypes.ToArray(),
254+
[Metadata.PromptValuesSupported] = notification.PromptValues.ToArray(),
253255
[Metadata.TokenEndpointAuthMethodsSupported] = notification.TokenEndpointAuthenticationMethods.ToArray(),
254256
[Metadata.IntrospectionEndpointAuthMethodsSupported] = notification.IntrospectionEndpointAuthenticationMethods.ToArray(),
255257
[Metadata.RevocationEndpointAuthMethodsSupported] = notification.RevocationEndpointAuthenticationMethods.ToArray(),
@@ -673,6 +675,35 @@ public ValueTask HandleAsync(HandleConfigurationRequestContext context)
673675
}
674676
}
675677

678+
/// <summary>
679+
/// Contains the logic responsible for attaching the supported prompt values to the provider discovery document.
680+
/// </summary>
681+
public sealed class AttachPromptValues : IOpenIddictServerHandler<HandleConfigurationRequestContext>
682+
{
683+
/// <summary>
684+
/// Gets the default descriptor definition assigned to this handler.
685+
/// </summary>
686+
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
687+
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
688+
.UseSingletonHandler<AttachPromptValues>()
689+
.SetOrder(AttachSubjectTypes.Descriptor.Order + 1_000)
690+
.SetType(OpenIddictServerHandlerType.BuiltIn)
691+
.Build();
692+
693+
/// <inheritdoc/>
694+
public ValueTask HandleAsync(HandleConfigurationRequestContext context)
695+
{
696+
if (context is null)
697+
{
698+
throw new ArgumentNullException(nameof(context));
699+
}
700+
701+
context.PromptValues.UnionWith(context.Options.PromptValues);
702+
703+
return default;
704+
}
705+
}
706+
676707
/// <summary>
677708
/// Contains the logic responsible for attaching the supported signing algorithms to the provider discovery document.
678709
/// </summary>
@@ -684,7 +715,7 @@ public sealed class AttachSigningAlgorithms : IOpenIddictServerHandler<HandleCon
684715
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
685716
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>()
686717
.UseSingletonHandler<AttachSigningAlgorithms>()
687-
.SetOrder(AttachSubjectTypes.Descriptor.Order + 1_000)
718+
.SetOrder(AttachPromptValues.Descriptor.Order + 1_000)
688719
.SetType(OpenIddictServerHandlerType.BuiltIn)
689720
.Build();
690721

src/OpenIddict.Server/OpenIddictServerOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,19 @@ public sealed class OpenIddictServerOptions
375375
/// </summary>
376376
public HashSet<string> GrantTypes { get; } = new(StringComparer.Ordinal);
377377

378+
/// <summary>
379+
/// Gets the OpenID Connect prompt values enabled for this application.
380+
/// </summary>
381+
public HashSet<string> PromptValues { get; } = new(StringComparer.Ordinal)
382+
{
383+
// By default, only include the mandatory values defined in the core OpenID Connect specification.
384+
// See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information.
385+
OpenIddictConstants.PromptValues.Consent,
386+
OpenIddictConstants.PromptValues.Login,
387+
OpenIddictConstants.PromptValues.None,
388+
OpenIddictConstants.PromptValues.SelectAccount
389+
};
390+
378391
/// <summary>
379392
/// Gets or sets a boolean indicating whether PKCE must be used by client applications
380393
/// when requesting an authorization code (e.g when using the code or hybrid flows).

0 commit comments

Comments
 (0)