Browse Source

Implement new audience and presenter validation logic as part of the ValidateToken event

pull/2334/head
Kévin Chalet 8 months ago
parent
commit
b33dad15f3
  1. 29
      shared/OpenIddict.Extensions/OpenIddictHelpers.cs
  2. 24
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 22
      src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs
  4. 3
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  5. 51
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  6. 139
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  7. 22
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  8. 2
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  9. 22
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  10. 2
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  11. 34
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  12. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  13. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  14. 2
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  15. 144
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  16. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  17. 6
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  18. 77
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  19. 27
      src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs
  20. 3
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  21. 51
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  22. 104
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  23. 10
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  24. 2
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs
  25. 119
      test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

29
shared/OpenIddict.Extensions/OpenIddictHelpers.cs

@ -53,6 +53,35 @@ internal static class OpenIddictHelpers
} }
} }
/// <summary>
/// Determines whether the specified array contains at least one value present in the specified set.
/// </summary>
/// <typeparam name="T">The type of the elements.</typeparam>
/// <param name="array">The array.</param>
/// <param name="set">The set.</param>
/// <returns>
/// <see langword="true"/> if the specified array contains at least one
/// value present in the specified set, <see langword="false"/> otherwise.
/// </returns>
public static bool IncludesAnyFromSet<T>(IReadOnlyList<T> array, ISet<T> set)
{
if (set is null)
{
throw new ArgumentNullException(nameof(set));
}
for (var index = 0; index < array.Count; index++)
{
var value = array[index];
if (set.Contains(value))
{
return true;
}
}
return false;
}
#if !SUPPORTS_TASK_WAIT_ASYNC #if !SUPPORTS_TASK_WAIT_ASYNC
/// <summary> /// <summary>
/// Waits until the specified task returns a result or the cancellation token is signaled. /// Waits until the specified task returns a result or the cancellation token is signaled.

24
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -2029,7 +2029,7 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<value>The specified token doesn't contain any audience.</value> <value>The specified token doesn't contain any audience.</value>
</data> </data>
<data name="ID2094" xml:space="preserve"> <data name="ID2094" xml:space="preserve">
<value>The specified token cannot be used with this resource server.</value> <value>The specified token doesn't contain any valid audience, which may indicate that it was issued to be used with another application.</value>
</data> </data>
<data name="ID2095" xml:space="preserve"> <data name="ID2095" xml:space="preserve">
<value>The user represented by the token is not allowed to perform the requested action.</value> <value>The user represented by the token is not allowed to perform the requested action.</value>
@ -2295,6 +2295,12 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID2183" xml:space="preserve"> <data name="ID2183" xml:space="preserve">
<value>This client application is not allowed to use the pushed authorization request endpoint.</value> <value>This client application is not allowed to use the pushed authorization request endpoint.</value>
</data> </data>
<data name="ID2184" xml:space="preserve">
<value>The specified token doesn't contain any presenter.</value>
</data>
<data name="ID2185" xml:space="preserve">
<value>The specified token doesn't contain any valid presenter, which may indicate that it was issued to a different client.</value>
</data>
<data name="ID4000" xml:space="preserve"> <data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value> <value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data> </data>
@ -2736,10 +2742,10 @@ The principal used to create the token contained the following claims: {Claims}.
<value>The authentication demand was rejected because the token was expired.</value> <value>The authentication demand was rejected because the token was expired.</value>
</data> </data>
<data name="ID6157" xml:space="preserve"> <data name="ID6157" xml:space="preserve">
<value>The authentication demand was rejected because the token had no audience attached.</value> <value>The authentication demand was rejected because the token validated via introspection had no audience attached.</value>
</data> </data>
<data name="ID6158" xml:space="preserve"> <data name="ID6158" xml:space="preserve">
<value>The authentication demand was rejected because the token had no valid audience.</value> <value>The authentication demand was rejected because the token validated via introspection had no valid audience.</value>
</data> </data>
<data name="ID6159" xml:space="preserve"> <data name="ID6159" xml:space="preserve">
<value>Client authentication cannot be enforced for public applications.</value> <value>Client authentication cannot be enforced for public applications.</value>
@ -3048,6 +3054,18 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6263" xml:space="preserve"> <data name="ID6263" xml:space="preserve">
<value>The pushed authorization request was rejected because the identity token used as a hint was issued to a different client.</value> <value>The pushed authorization request was rejected because the identity token used as a hint was issued to a different client.</value>
</data> </data>
<data name="ID6264" xml:space="preserve">
<value>The token was rejected because it had no presenter attached and at least one explicit presenter was expected.</value>
</data>
<data name="ID6265" xml:space="preserve">
<value>The token was rejected because it had no valid presenter.</value>
</data>
<data name="ID6266" xml:space="preserve">
<value>The token was rejected because it had no audience attached and at least one explicit audience was expected.</value>
</data>
<data name="ID6267" xml:space="preserve">
<value>The token was rejected because it had no valid audience.</value>
</data>
<data name="ID8000" xml:space="preserve"> <data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value> <value>https://documentation.openiddict.com/errors/{0}</value>
</data> </data>

22
src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs

@ -123,11 +123,21 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value; set => Transaction.Request = value;
} }
/// <summary>
/// Gets or sets a boolean indicating whether audience validation is disabled.
/// </summary>
public bool DisableAudienceValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether lifetime validation is disabled. /// Gets or sets a boolean indicating whether lifetime validation is disabled.
/// </summary> /// </summary>
public bool DisableLifetimeValidation { get; set; } public bool DisableLifetimeValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether presenter validation is disabled.
/// </summary>
public bool DisablePresenterValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets the security token handler used to validate the token. /// Gets or sets the security token handler used to validate the token.
/// </summary> /// </summary>
@ -173,6 +183,18 @@ public static partial class OpenIddictClientEvents
/// </remarks> /// </remarks>
public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal); public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the audiences that are considered valid. If no value
/// is explicitly specified, all audiences are considered valid.
/// </summary>
public HashSet<string> ValidAudiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the presenters that are considered valid. If no value
/// is explicitly specified, all presenters are considered valid.
/// </summary>
public HashSet<string> ValidPresenters { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets the token types that are considered valid. If no value is /// Gets the token types that are considered valid. If no value is
/// explicitly specified, all supported tokens are considered valid. /// explicitly specified, all supported tokens are considered valid.

3
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -63,9 +63,12 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireRevocationRequest>(); builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireStateTokenPrincipal>(); builder.Services.TryAddSingleton<RequireStateTokenPrincipal>();
builder.Services.TryAddSingleton<RequireStateTokenValidated>(); builder.Services.TryAddSingleton<RequireStateTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>(); builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>(); builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>(); builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenRequest>(); builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>(); builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
builder.Services.TryAddSingleton<RequireUserInfoRequest>(); builder.Services.TryAddSingleton<RequireUserInfoRequest>();

51
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -490,6 +490,23 @@ public static class OpenIddictClientHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token audience validation was disabled.
/// </summary>
public sealed class RequireTokenAudienceValidationEnabled : IOpenIddictClientHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisableAudienceValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if no token entry is created in the database. /// Represents a filter that excludes the associated handlers if no token entry is created in the database.
/// </summary> /// </summary>
@ -524,6 +541,23 @@ public static class OpenIddictClientHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token lifetime validation was disabled.
/// </summary>
public sealed class RequireTokenLifetimeValidationEnabled : IOpenIddictClientHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisableLifetimeValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database. /// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database.
/// </summary> /// </summary>
@ -541,6 +575,23 @@ public static class OpenIddictClientHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token presenter validation was disabled.
/// </summary>
public sealed class RequireTokenPresenterValidationEnabled : IOpenIddictClientHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisablePresenterValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if no token request is expected to be sent. /// Represents a filter that excludes the associated handlers if no token request is expected to be sent.
/// </summary> /// </summary>

139
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -32,6 +32,8 @@ public static partial class OpenIddictClientHandlers
RestoreTokenEntryProperties.Descriptor, RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor, ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor, ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
/* /*
@ -606,7 +608,7 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands for which no valid principal was resolved. /// Contains the logic responsible for rejecting tokens for which no valid principal could be resolved.
/// </summary> /// </summary>
public sealed class ValidatePrincipal : IOpenIddictClientHandler<ValidateTokenContext> public sealed class ValidatePrincipal : IOpenIddictClientHandler<ValidateTokenContext>
{ {
@ -648,7 +650,7 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0004));
} }
if (context.ValidTokenTypes.Count > 0 && !context.ValidTokenTypes.Contains(type)) if (context.ValidTokenTypes.Count is > 0 && !context.ValidTokenTypes.Contains(type))
{ {
throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes)));
} }
@ -658,7 +660,7 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands that use an expired token. /// Contains the logic responsible for rejecting expired tokens.
/// </summary> /// </summary>
public sealed class ValidateExpirationDate : IOpenIddictClientHandler<ValidateTokenContext> public sealed class ValidateExpirationDate : IOpenIddictClientHandler<ValidateTokenContext>
{ {
@ -667,6 +669,7 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenLifetimeValidationEnabled>()
.UseSingletonHandler<ValidateExpirationDate>() .UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
@ -698,7 +701,133 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for authentication demands a token whose /// Contains the logic responsible for rejecting tokens that can't be used by the caller.
/// </summary>
public sealed class ValidatePresenters : IOpenIddictClientHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenPresenterValidationEnabled>()
.UseSingletonHandler<ValidatePresenters>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no specific value is expected, skip the default presenter validation.
if (context.ValidPresenters.Count is 0)
{
return default;
}
// If the token doesn't have any presenter attached, return an error.
var presenters = context.Principal.GetPresenters();
if (presenters.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6264, SR.GetResourceString(SR.ID6264));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2184),
uri: SR.FormatID8000(SR.ID2184));
return default;
}
// If the token doesn't include any registered presenter, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(presenters, context.ValidPresenters))
{
context.Logger.LogInformation(6265, SR.GetResourceString(SR.ID6265));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2185),
uri: SR.FormatID8000(SR.ID2185));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens issued for different recipients.
/// </summary>
public sealed class ValidateAudiences : IOpenIddictClientHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenAudienceValidationEnabled>()
.UseSingletonHandler<ValidateAudiences>()
.SetOrder(ValidatePresenters.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no specific value is expected, skip the default audience validation.
if (context.ValidAudiences.Count is 0)
{
return default;
}
// If the token doesn't have any audience attached, return an error.
var audiences = context.Principal.GetAudiences();
if (audiences.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6266, SR.GetResourceString(SR.ID6266));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2093),
uri: SR.FormatID8000(SR.ID2093));
return default;
}
// If the token doesn't include any registered audience, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.ValidAudiences))
{
context.Logger.LogInformation(6267, SR.GetResourceString(SR.ID6267));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2094),
uri: SR.FormatID8000(SR.ID2094));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated token entry is no longer valid (e.g was revoked). /// associated token entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when token storage is disabled. /// Note: this handler is not used when token storage is disabled.
/// </summary> /// </summary>
@ -719,7 +848,7 @@ public static partial class OpenIddictClientHandlers
.AddFilter<RequireTokenStorageEnabled>() .AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenIdResolved>() .AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>() .UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) .SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();

22
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -683,6 +683,8 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.StateToken, Token = context.StateToken,
ValidTokenTypes = { TokenTypeIdentifiers.Private.StateToken } ValidTokenTypes = { TokenTypeIdentifiers.Private.StateToken }
}; };
@ -1625,6 +1627,9 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Note: for identity tokens, audience validation is enforced by a specialized handler.
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.FrontchannelIdentityToken, Token = context.FrontchannelIdentityToken,
ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken } ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken }
}; };
@ -2458,7 +2463,7 @@ public static partial class OpenIddictClientHandlers
string value => value string value => value
}; };
if (context.Scopes.Count > 0 && if (context.Scopes.Count is > 0 &&
context.TokenRequest.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.DeviceCode)) context.TokenRequest.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.DeviceCode))
{ {
// Note: the final OAuth 2.0 specification requires using a space as the scope separator. // Note: the final OAuth 2.0 specification requires using a space as the scope separator.
@ -3061,6 +3066,9 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Note: for identity tokens, audience validation is enforced by a specialized handler.
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.BackchannelIdentityToken, Token = context.BackchannelIdentityToken,
ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken } ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken }
}; };
@ -5019,7 +5027,7 @@ public static partial class OpenIddictClientHandlers
} }
// If an explicit set of scopes was specified, don't overwrite it. // If an explicit set of scopes was specified, don't overwrite it.
if (context.Scopes.Count > 0) if (context.Scopes.Count is > 0)
{ {
return default; return default;
} }
@ -5497,7 +5505,7 @@ public static partial class OpenIddictClientHandlers
context.Request.ResponseType = context.ResponseType; context.Request.ResponseType = context.ResponseType;
context.Request.ResponseMode = context.ResponseMode; context.Request.ResponseMode = context.ResponseMode;
if (context.Scopes.Count > 0) if (context.Scopes.Count is > 0)
{ {
// Note: the final OAuth 2.0 specification requires using a space as the scope separator. // Note: the final OAuth 2.0 specification requires using a space as the scope separator.
// Clients that need to deal with older or non-compliant implementations can register // Clients that need to deal with older or non-compliant implementations can register
@ -5558,7 +5566,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -5734,7 +5742,7 @@ public static partial class OpenIddictClientHandlers
// Attach a new request instance if necessary. // Attach a new request instance if necessary.
context.DeviceAuthorizationRequest ??= new OpenIddictRequest(); context.DeviceAuthorizationRequest ??= new OpenIddictRequest();
if (context.Scopes.Count > 0) if (context.Scopes.Count is > 0)
{ {
// Note: the final OAuth 2.0 specification requires using a space as the scope separator. // Note: the final OAuth 2.0 specification requires using a space as the scope separator.
// Clients that need to deal with older or non-compliant implementations can register // Clients that need to deal with older or non-compliant implementations can register
@ -8586,7 +8594,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -8653,7 +8661,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {

2
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -118,7 +118,7 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
} }
// Ensure the device grant is allowed when the device authorization endpoint is enabled. // Ensure the device grant is allowed when the device authorization endpoint is enabled.
if (options.DeviceAuthorizationEndpointUris.Count > 0 && !options.GrantTypes.Contains(GrantTypes.DeviceCode)) if (options.DeviceAuthorizationEndpointUris.Count is > 0 && !options.GrantTypes.Contains(GrantTypes.DeviceCode))
{ {
throw new InvalidOperationException(SR.GetResourceString(SR.ID0084)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0084));
} }

22
src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs

@ -129,11 +129,21 @@ public static partial class OpenIddictServerEvents
set => Transaction.Request = value; set => Transaction.Request = value;
} }
/// <summary>
/// Gets or sets a boolean indicating whether audience validation is disabled.
/// </summary>
public bool DisableAudienceValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether lifetime validation is disabled. /// Gets or sets a boolean indicating whether lifetime validation is disabled.
/// </summary> /// </summary>
public bool DisableLifetimeValidation { get; set; } public bool DisableLifetimeValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether presenter validation is disabled.
/// </summary>
public bool DisablePresenterValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets the security token handler used to validate the token. /// Gets or sets the security token handler used to validate the token.
/// </summary> /// </summary>
@ -189,6 +199,18 @@ public static partial class OpenIddictServerEvents
/// </remarks> /// </remarks>
public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal); public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the audiences that are considered valid. If no value
/// is explicitly specified, all audiences are considered valid.
/// </summary>
public HashSet<string> ValidAudiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the presenters that are considered valid. If no value
/// is explicitly specified, all presenters are considered valid.
/// </summary>
public HashSet<string> ValidPresenters { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets the token types that are considered valid. /// Gets the token types that are considered valid.
/// </summary> /// </summary>

2
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -80,10 +80,12 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>(); builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>(); builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>(); builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>(); builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>(); builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>(); builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>(); builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenRequest>(); builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>(); builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
builder.Services.TryAddSingleton<RequireUserCodeGenerated>(); builder.Services.TryAddSingleton<RequireUserCodeGenerated>();

34
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -691,6 +691,23 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token audience validation was disabled.
/// </summary>
public sealed class RequireTokenAudienceValidationEnabled : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisableAudienceValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token. /// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </summary> /// </summary>
@ -759,6 +776,23 @@ public static class OpenIddictServerHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token presenter validation was disabled.
/// </summary>
public sealed class RequireTokenPresenterValidationEnabled : IOpenIddictServerHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisablePresenterValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a token request. /// Represents a filter that excludes the associated handlers if the request is not a token request.
/// </summary> /// </summary>

6
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -301,7 +301,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {
@ -342,7 +342,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {
@ -2398,7 +2398,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {

4
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -251,7 +251,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {
@ -961,7 +961,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {

2
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -252,7 +252,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {

144
src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs

@ -35,6 +35,8 @@ public static partial class OpenIddictServerHandlers
RestoreTokenEntryProperties.Descriptor, RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor, ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor, ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor, ValidateAuthorizationEntry.Descriptor,
@ -122,8 +124,8 @@ public static partial class OpenIddictServerHandlers
// Only provide a signing key resolver if the degraded mode was not enabled. // Only provide a signing key resolver if the degraded mode was not enabled.
// //
// Applications that opt for the degraded mode and need client assertions support // Applications that opt for the degraded mode and need client assertions support have
// need to implement a custom event handler thats a issuer signing key resolver. // to implement a custom event handler that attaches an issuer signing key resolver.
if (!context.Options.EnableDegradedMode) if (!context.Options.EnableDegradedMode)
{ {
if (_applicationManager is null) if (_applicationManager is null)
@ -813,7 +815,7 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands for which no valid principal was resolved. /// Contains the logic responsible for rejecting tokens for which no valid principal could be resolved.
/// </summary> /// </summary>
public sealed class ValidatePrincipal : IOpenIddictServerHandler<ValidateTokenContext> public sealed class ValidatePrincipal : IOpenIddictServerHandler<ValidateTokenContext>
{ {
@ -886,7 +888,7 @@ public static partial class OpenIddictServerHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0004));
} }
if (context.ValidTokenTypes.Count > 0 && !context.ValidTokenTypes.Contains(type)) if (context.ValidTokenTypes.Count is > 0 && !context.ValidTokenTypes.Contains(type))
{ {
throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes)));
} }
@ -896,7 +898,7 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands that use an expired token. /// Contains the logic responsible for rejecting expired tokens.
/// </summary> /// </summary>
public sealed class ValidateExpirationDate : IOpenIddictServerHandler<ValidateTokenContext> public sealed class ValidateExpirationDate : IOpenIddictServerHandler<ValidateTokenContext>
{ {
@ -956,8 +958,134 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands that /// Contains the logic responsible for rejecting tokens that can't be used by the caller.
/// use a token whose entry is no longer valid (e.g was revoked). /// </summary>
public sealed class ValidatePresenters : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenPresenterValidationEnabled>()
.UseSingletonHandler<ValidatePresenters>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no specific value is expected, skip the default presenter validation.
if (context.ValidPresenters.Count is 0)
{
return default;
}
// If the token doesn't have any presenter attached, return an error.
var presenters = context.Principal.GetPresenters();
if (presenters.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6264, SR.GetResourceString(SR.ID6264));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2184),
uri: SR.FormatID8000(SR.ID2184));
return default;
}
// If the token doesn't include any registered presenter, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(presenters, context.ValidPresenters))
{
context.Logger.LogInformation(6265, SR.GetResourceString(SR.ID6265));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2185),
uri: SR.FormatID8000(SR.ID2185));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens issued for different recipients.
/// </summary>
public sealed class ValidateAudiences : IOpenIddictServerHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenAudienceValidationEnabled>()
.UseSingletonHandler<ValidateAudiences>()
.SetOrder(ValidatePresenters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no specific value is expected, skip the default audience validation.
if (context.ValidAudiences.Count is 0)
{
return default;
}
// If the token doesn't have any audience attached, return an error.
var audiences = context.Principal.GetAudiences();
if (audiences.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6266, SR.GetResourceString(SR.ID6266));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2093),
uri: SR.FormatID8000(SR.ID2093));
return default;
}
// If the token doesn't include any registered audience, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.ValidAudiences))
{
context.Logger.LogInformation(6267, SR.GetResourceString(SR.ID6267));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2094),
uri: SR.FormatID8000(SR.ID2094));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens whose
/// associated token entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled. /// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public sealed class ValidateTokenEntry : IOpenIddictServerHandler<ValidateTokenContext> public sealed class ValidateTokenEntry : IOpenIddictServerHandler<ValidateTokenContext>
@ -1136,7 +1264,7 @@ public static partial class OpenIddictServerHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for authentication demands a token whose /// Contains the logic responsible for rejecting tokens whose
/// associated authorization entry is no longer valid (e.g was revoked). /// associated authorization entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled. /// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>

4
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -239,7 +239,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {
@ -280,7 +280,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse() Response = new OpenIddictResponse()
}; };
if (notification.Parameters.Count > 0) if (notification.Parameters.Count is > 0)
{ {
foreach (var parameter in notification.Parameters) foreach (var parameter in notification.Parameters)
{ {

6
src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs

@ -18,7 +18,7 @@ public static partial class OpenIddictServerHandlers
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } =
[ [
/* /*
* UserInfo request top-level processing: * Userinfo request top-level processing:
*/ */
ExtractUserInfoRequest.Descriptor, ExtractUserInfoRequest.Descriptor,
ValidateUserInfoRequest.Descriptor, ValidateUserInfoRequest.Descriptor,
@ -28,13 +28,13 @@ public static partial class OpenIddictServerHandlers
ApplyUserInfoResponse<ProcessRequestContext>.Descriptor, ApplyUserInfoResponse<ProcessRequestContext>.Descriptor,
/* /*
* UserInfo request validation: * Userinfo request validation:
*/ */
ValidateAccessTokenParameter.Descriptor, ValidateAccessTokenParameter.Descriptor,
ValidateAuthentication.Descriptor, ValidateAuthentication.Descriptor,
/* /*
* UserInfo request handling: * Userinfo request handling:
*/ */
AttachPrincipal.Descriptor, AttachPrincipal.Descriptor,
AttachAudiences.Descriptor, AttachAudiences.Descriptor,

77
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -601,6 +601,9 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Note: for client authentication assertions, audience validation is enforced by a specialized handler.
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.ClientAssertion, Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch TokenFormat = context.ClientAssertionType switch
{ {
@ -1319,6 +1322,8 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.RequestToken, Token = context.RequestToken,
ValidTokenTypes = { TokenTypeIdentifiers.Private.RequestToken } ValidTokenTypes = { TokenTypeIdentifiers.Private.RequestToken }
}; };
@ -1443,6 +1448,10 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Audience validation is deliberately disabled for the userinfo endpoint to allow any access token to
// be used even if the authorization server isn't explicitly listed as a valid audience in the token.
DisableAudienceValidation = context.EndpointType is OpenIddictServerEndpointType.UserInfo,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.UserInfo,
Token = context.AccessToken, Token = context.AccessToken,
ValidTokenTypes = { TokenTypeIdentifiers.AccessToken } ValidTokenTypes = { TokenTypeIdentifiers.AccessToken }
}; };
@ -1515,10 +1524,19 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
Token = context.AuthorizationCode, Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeIdentifiers.Private.AuthorizationCode } ValidTokenTypes = { TokenTypeIdentifiers.Private.AuthorizationCode }
}; };
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification); await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled) if (notification.IsRequestHandled)
@ -1587,10 +1605,19 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
Token = context.DeviceCode, Token = context.DeviceCode,
ValidTokenTypes = { TokenTypeIdentifiers.Private.DeviceCode } ValidTokenTypes = { TokenTypeIdentifiers.Private.DeviceCode }
}; };
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification); await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled) if (notification.IsRequestHandled)
@ -1659,6 +1686,12 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Audience and presenter validation is disabled for the introspection and revocation endpoints
// as these endpoints implement specialized event handlers that use more complex rules.
DisableAudienceValidation = context.EndpointType is OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Introspection or
OpenIddictServerEndpointType.Revocation,
Token = context.GenericToken, Token = context.GenericToken,
TokenTypeHint = context.GenericTokenTypeHint, TokenTypeHint = context.GenericTokenTypeHint,
@ -1749,14 +1782,27 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
// Don't validate the lifetime of id_tokens used as id_token_hints. // Audience and presenter validation is disabled for the authorization and end session endpoints
// as these endpoints implement specialized event handlers that use more complex rules.
DisableAudienceValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization,
// Don't validate the lifetime of identity token used as hints.
DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization, OpenIddictServerEndpointType.PushedAuthorization,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization,
Token = context.IdentityToken, Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken } ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken }
}; };
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification); await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled) if (notification.IsRequestHandled)
@ -1825,10 +1871,19 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
Token = context.RefreshToken, Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeIdentifiers.RefreshToken } ValidTokenTypes = { TokenTypeIdentifiers.RefreshToken }
}; };
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification); await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled) if (notification.IsRequestHandled)
@ -1897,10 +1952,17 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction) var notification = new ValidateTokenContext(context.Transaction)
{ {
DisableAudienceValidation = true,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.EndUserVerification,
Token = context.UserCode, Token = context.UserCode,
ValidTokenTypes = { TokenTypeIdentifiers.Private.UserCode } ValidTokenTypes = { TokenTypeIdentifiers.Private.UserCode }
}; };
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
// Note: restrict the allowed characters to the user code charset set in the options. // Note: restrict the allowed characters to the user code charset set in the options.
notification.AllowedCharset.UnionWith(context.Options.UserCodeCharset); notification.AllowedCharset.UnionWith(context.Options.UserCodeCharset);
@ -2304,7 +2366,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -2801,14 +2863,13 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// When a "resources" property cannot be found in the ticket, infer it from the "audiences" property. // When a "resources" property cannot be found in the ticket, infer it from the "audiences" property.
if (context.Principal.HasClaim(Claims.Private.Audience) && if (context.Principal.HasClaim(Claims.Private.Audience) && !context.Principal.HasClaim(Claims.Private.Resource))
!context.Principal.HasClaim(Claims.Private.Resource))
{ {
context.Principal.SetResources(context.Principal.GetAudiences()); context.Principal.SetResources(context.Principal.GetAudiences());
} }
// Reset the audiences collection, as it's later set, based on the token type. // Reset the audiences collection, as it's later set, based on the token type.
context.Principal.SetAudiences(ImmutableArray<string>.Empty); context.Principal.SetAudiences([]);
return default; return default;
} }
@ -4879,7 +4940,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -5014,7 +5075,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -5081,7 +5142,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {

27
src/OpenIddict.Validation/OpenIddictValidationEvents.Protection.cs

@ -123,6 +123,21 @@ public static partial class OpenIddictValidationEvents
set => Transaction.Request = value; set => Transaction.Request = value;
} }
/// <summary>
/// Gets or sets a boolean indicating whether audience validation is disabled.
/// </summary>
public bool DisableAudienceValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether lifetime validation is disabled.
/// </summary>
public bool DisableLifetimeValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether presenter validation is disabled.
/// </summary>
public bool DisablePresenterValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets the security token handler used to validate the token. /// Gets or sets the security token handler used to validate the token.
/// </summary> /// </summary>
@ -173,6 +188,18 @@ public static partial class OpenIddictValidationEvents
/// </remarks> /// </remarks>
public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal); public HashSet<string> AllowedCharset { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the audiences that are considered valid. If no value
/// is explicitly specified, all audiences are considered valid.
/// </summary>
public HashSet<string> ValidAudiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the presenters that are considered valid. If no value
/// is explicitly specified, all presenters are considered valid.
/// </summary>
public HashSet<string> ValidPresenters { get; } = new(StringComparer.Ordinal);
/// <summary> /// <summary>
/// Gets the token types that are considered valid. If no value is /// Gets the token types that are considered valid. If no value is
/// explicitly specified, all supported tokens are considered valid. /// explicitly specified, all supported tokens are considered valid.

3
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -49,8 +49,11 @@ public static class OpenIddictValidationExtensions
builder.Services.TryAddSingleton<RequireIntrospectionRequest>(); builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>(); builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireLocalValidation>(); builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>(); builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>(); builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

51
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -147,6 +147,23 @@ public static class OpenIddictValidationHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token audience validation was disabled.
/// </summary>
public sealed class RequireTokenAudienceValidationEnabled : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisableAudienceValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token. /// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </summary> /// </summary>
@ -164,6 +181,23 @@ public static class OpenIddictValidationHandlerFilters
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token lifetime validation was disabled.
/// </summary>
public sealed class RequireTokenLifetimeValidationEnabled : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisableLifetimeValidation);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if token validation was not enabled. /// Represents a filter that excludes the associated handlers if token validation was not enabled.
/// </summary> /// </summary>
@ -180,4 +214,21 @@ public static class OpenIddictValidationHandlerFilters
return new(context.Options.EnableTokenEntryValidation); return new(context.Options.EnableTokenEntryValidation);
} }
} }
/// <summary>
/// Represents a filter that excludes the associated handlers if token presenter validation was disabled.
/// </summary>
public sealed class RequireTokenPresenterValidationEnabled : IOpenIddictValidationHandlerFilter<ValidateTokenContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!context.DisablePresenterValidation);
}
}
} }

104
src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs

@ -32,7 +32,8 @@ public static partial class OpenIddictValidationHandlers
RestoreTokenEntryProperties.Descriptor, RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor, ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor, ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor, ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.Descriptor, ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor, ValidateAuthorizationEntry.Descriptor,
@ -595,7 +596,7 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands for which no valid principal was resolved. /// Contains the logic responsible for rejecting tokens for which no valid principal could be resolved.
/// </summary> /// </summary>
public sealed class ValidatePrincipal : IOpenIddictValidationHandler<ValidateTokenContext> public sealed class ValidatePrincipal : IOpenIddictValidationHandler<ValidateTokenContext>
{ {
@ -637,7 +638,7 @@ public static partial class OpenIddictValidationHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0004)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0004));
} }
if (context.ValidTokenTypes.Count > 0 && !context.ValidTokenTypes.Contains(type)) if (context.ValidTokenTypes.Count is > 0 && !context.ValidTokenTypes.Contains(type))
{ {
throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes))); throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes)));
} }
@ -647,7 +648,7 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands containing expired access tokens. /// Contains the logic responsible for rejecting expired tokens.
/// </summary> /// </summary>
public sealed class ValidateExpirationDate : IOpenIddictValidationHandler<ValidateTokenContext> public sealed class ValidateExpirationDate : IOpenIddictValidationHandler<ValidateTokenContext>
{ {
@ -656,6 +657,7 @@ public static partial class OpenIddictValidationHandlers
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenLifetimeValidationEnabled>()
.UseSingletonHandler<ValidateExpirationDate>() .UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000) .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
@ -689,17 +691,17 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting authentication demands containing /// Contains the logic responsible for rejecting tokens that can't be used by the caller.
/// access tokens that were issued to be used by another audience/resource server.
/// </summary> /// </summary>
public sealed class ValidateAudience : IOpenIddictValidationHandler<ValidateTokenContext> public sealed class ValidatePresenters : IOpenIddictValidationHandler<ValidateTokenContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
/// </summary> /// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; } public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>() = OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidateAudience>() .AddFilter<RequireTokenPresenterValidationEnabled>()
.UseSingletonHandler<ValidatePresenters>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -714,18 +716,80 @@ public static partial class OpenIddictValidationHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no explicit audience has been configured, // If no specific value is expected, skip the default presenter validation.
// skip the default audience validation. if (context.ValidPresenters.Count is 0)
if (context.Options.Audiences.Count is 0)
{ {
return default; return default;
} }
// If the access token doesn't have any audience attached, return an error. // If the token doesn't have any presenter attached, return an error.
var presenters = context.Principal.GetPresenters();
if (presenters.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6264, SR.GetResourceString(SR.ID6264));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2184),
uri: SR.FormatID8000(SR.ID2184));
return default;
}
// If the token doesn't include any registered presenter, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(presenters, context.ValidPresenters))
{
context.Logger.LogInformation(6265, SR.GetResourceString(SR.ID6265));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2185),
uri: SR.FormatID8000(SR.ID2185));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting tokens issued for different recipients.
/// </summary>
public sealed class ValidateAudiences : IOpenIddictValidationHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenAudienceValidationEnabled>()
.UseSingletonHandler<ValidateAudiences>()
.SetOrder(ValidatePresenters.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no specific value is expected, skip the default audience validation.
if (context.ValidAudiences.Count is 0)
{
return default;
}
// If the token doesn't have any audience attached, return an error.
var audiences = context.Principal.GetAudiences(); var audiences = context.Principal.GetAudiences();
if (audiences.IsDefaultOrEmpty) if (audiences.IsDefaultOrEmpty)
{ {
context.Logger.LogInformation(6157, SR.GetResourceString(SR.ID6157)); context.Logger.LogInformation(6266, SR.GetResourceString(SR.ID6266));
context.Reject( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,
@ -735,10 +799,10 @@ public static partial class OpenIddictValidationHandlers
return default; return default;
} }
// If the access token doesn't include any registered audience, return an error. // If the token doesn't include any registered audience, return an error.
if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any()) if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.ValidAudiences))
{ {
context.Logger.LogInformation(6158, SR.GetResourceString(SR.ID6158)); context.Logger.LogInformation(6267, SR.GetResourceString(SR.ID6267));
context.Reject( context.Reject(
error: Errors.InvalidToken, error: Errors.InvalidToken,
@ -753,9 +817,8 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for authentication demands a token whose /// Contains the logic responsible for rejecting tokens whose
/// associated token entry is no longer valid (e.g was revoked). /// associated token entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public sealed class ValidateTokenEntry : IOpenIddictValidationHandler<ValidateTokenContext> public sealed class ValidateTokenEntry : IOpenIddictValidationHandler<ValidateTokenContext>
{ {
@ -774,7 +837,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireTokenEntryValidationEnabled>() .AddFilter<RequireTokenEntryValidationEnabled>()
.AddFilter<RequireTokenIdResolved>() .AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>() .UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateAudience.Descriptor.Order + 1_000) .SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn) .SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build(); .Build();
@ -807,9 +870,8 @@ public static partial class OpenIddictValidationHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible for authentication demands a token whose /// Contains the logic responsible for rejecting tokens whose
/// associated authorization entry is no longer valid (e.g was revoked). /// associated authorization entry is no longer valid (e.g was revoked).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary> /// </summary>
public sealed class ValidateAuthorizationEntry : IOpenIddictValidationHandler<ValidateTokenContext> public sealed class ValidateAuthorizationEntry : IOpenIddictValidationHandler<ValidateTokenContext>
{ {

10
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -742,7 +742,7 @@ public static partial class OpenIddictValidationHandlers
} }
// If the access token doesn't include any registered audience, return an error. // If the access token doesn't include any registered audience, return an error.
if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any()) if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.Options.Audiences))
{ {
context.Logger.LogInformation(6158, SR.GetResourceString(SR.ID6158)); context.Logger.LogInformation(6158, SR.GetResourceString(SR.ID6158));
@ -798,6 +798,10 @@ public static partial class OpenIddictValidationHandlers
ValidTokenTypes = { TokenTypeIdentifiers.AccessToken } ValidTokenTypes = { TokenTypeIdentifiers.AccessToken }
}; };
// Note: by default, access tokens are not constrainted to specific presenters but must contain
// at least one audience matching one of the values configured in the options, if applicable.
notification.ValidAudiences.UnionWith(context.Options.Audiences);
await _dispatcher.DispatchAsync(notification); await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled) if (notification.IsRequestHandled)
@ -895,7 +899,7 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {
@ -962,7 +966,7 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
if (context.Parameters.Count > 0) if (context.Parameters.Count is > 0)
{ {
foreach (var parameter in context.Parameters) foreach (var parameter in context.Parameters)
{ {

2
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Userinfo.cs

@ -415,7 +415,7 @@ public abstract partial class OpenIddictServerIntegrationTests
context.Principal = new ClaimsPrincipal(identity) context.Principal = new ClaimsPrincipal(identity)
.SetTokenType(TokenTypeIdentifiers.AccessToken) .SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetPresenters("Fabrikam") .SetPresenters("Fabrikam")
.SetScopes(ImmutableArray<string>.Empty); .SetScopes([]);
return default; return default;
}); });

119
test/OpenIddict.Validation.IntegrationTests/OpenIddictValidationIntegrationTests.cs

@ -177,6 +177,125 @@ public abstract partial class OpenIddictValidationIntegrationTests
Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]); Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]);
} }
[Fact]
public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenAudienceIsMissing()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.AddAudiences("Fabrikam");
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("access_token", context.Token);
Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetAudiences([])
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/authenticate", new OpenIddictRequest
{
AccessToken = "access_token"
});
// Assert
Assert.Equal(Errors.InvalidToken, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2093), response.ErrorDescription);
}
[Fact]
public async Task ProcessAuthentication_RejectsDemandWhenAccessTokenAudienceIsInvalid()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.AddAudiences("Fabrikam");
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("access_token", context.Token);
Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetAudiences("Contoso")
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/authenticate", new OpenIddictRequest
{
AccessToken = "access_token"
});
// Assert
Assert.Equal(Errors.InvalidToken, response.Error);
Assert.Equal(SR.GetResourceString(SR.ID2094), response.ErrorDescription);
}
[Fact]
public async Task ProcessAuthentication_ReturnsExpectedIdentityWhenAccessTokenAudienceIsValid()
{
// Arrange
await using var server = await CreateServerAsync(options =>
{
options.AddAudiences("Fabrikam");
options.AddEventHandler<ValidateTokenContext>(builder =>
{
builder.UseInlineHandler(context =>
{
Assert.Equal("access_token", context.Token);
Assert.Equal([TokenTypeIdentifiers.AccessToken], context.ValidTokenTypes);
context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer"))
.SetAudiences("Fabrikam")
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetClaim(Claims.Subject, "Bob le Magnifique");
return default;
});
builder.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500);
});
});
await using var client = await server.CreateClientAsync();
// Act
var response = await client.GetAsync("/authenticate", new OpenIddictRequest
{
AccessToken = "access_token"
});
// Assert
Assert.Equal("Bob le Magnifique", (string?) response[Claims.Subject]);
}
[Fact] [Fact]
public async Task ProcessChallenge_ReturnsDefaultErrorWhenNoneIsSpecified() public async Task ProcessChallenge_ReturnsDefaultErrorWhenNoneIsSpecified()
{ {

Loading…
Cancel
Save