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
/// <summary>
/// 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>
</data>
<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 name="ID2095" xml:space="preserve">
<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">
<value>This client application is not allowed to use the pushed authorization request endpoint.</value>
</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">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</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>
</data>
<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 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 name="ID6159" xml:space="preserve">
<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">
<value>The pushed authorization request was rejected because the identity token used as a hint was issued to a different client.</value>
</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">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

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

@ -123,11 +123,21 @@ public static partial class OpenIddictClientEvents
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>
/// Gets or sets the security token handler used to validate the token.
/// </summary>
@ -173,6 +183,18 @@ public static partial class OpenIddictClientEvents
/// </remarks>
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>
/// Gets the token types that are considered valid. If no value is
/// 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<RequireStateTokenPrincipal>();
builder.Services.TryAddSingleton<RequireStateTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
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>
/// Represents a filter that excludes the associated handlers if no token entry is created in the database.
/// </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>
/// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database.
/// </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>
/// Represents a filter that excludes the associated handlers if no token request is expected to be sent.
/// </summary>

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

@ -32,6 +32,8 @@ public static partial class OpenIddictClientHandlers
RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.Descriptor,
/*
@ -606,7 +608,7 @@ public static partial class OpenIddictClientHandlers
}
/// <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>
public sealed class ValidatePrincipal : IOpenIddictClientHandler<ValidateTokenContext>
{
@ -648,7 +650,7 @@ public static partial class OpenIddictClientHandlers
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)));
}
@ -658,7 +660,7 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that use an expired token.
/// Contains the logic responsible for rejecting expired tokens.
/// </summary>
public sealed class ValidateExpirationDate : IOpenIddictClientHandler<ValidateTokenContext>
{
@ -667,6 +669,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenLifetimeValidationEnabled>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -698,7 +701,133 @@ public static partial class OpenIddictClientHandlers
}
/// <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).
/// Note: this handler is not used when token storage is disabled.
/// </summary>
@ -719,7 +848,7 @@ public static partial class OpenIddictClientHandlers
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

22
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -683,6 +683,8 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.StateToken,
ValidTokenTypes = { TokenTypeIdentifiers.Private.StateToken }
};
@ -1625,6 +1627,9 @@ public static partial class OpenIddictClientHandlers
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,
ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken }
};
@ -2458,7 +2463,7 @@ public static partial class OpenIddictClientHandlers
string value => value
};
if (context.Scopes.Count > 0 &&
if (context.Scopes.Count is > 0 &&
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.
@ -3061,6 +3066,9 @@ public static partial class OpenIddictClientHandlers
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,
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 (context.Scopes.Count > 0)
if (context.Scopes.Count is > 0)
{
return default;
}
@ -5497,7 +5505,7 @@ public static partial class OpenIddictClientHandlers
context.Request.ResponseType = context.ResponseType;
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.
// 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));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
foreach (var parameter in context.Parameters)
{
@ -5734,7 +5742,7 @@ public static partial class OpenIddictClientHandlers
// Attach a new request instance if necessary.
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.
// 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));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
foreach (var parameter in context.Parameters)
{
@ -8653,7 +8661,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
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.
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));
}

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

@ -129,11 +129,21 @@ public static partial class OpenIddictServerEvents
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>
/// Gets or sets the security token handler used to validate the token.
/// </summary>
@ -189,6 +199,18 @@ public static partial class OpenIddictServerEvents
/// </remarks>
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>
/// Gets the token types that are considered valid.
/// </summary>

2
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -80,10 +80,12 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();
builder.Services.TryAddSingleton<RequireTokenLifetimeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenPayloadPersisted>();
builder.Services.TryAddSingleton<RequireTokenPresenterValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
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>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </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>
/// Represents a filter that excludes the associated handlers if the request is not a token request.
/// </summary>

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

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

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

@ -35,6 +35,8 @@ public static partial class OpenIddictServerHandlers
RestoreTokenEntryProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.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.
//
// Applications that opt for the degraded mode and need client assertions support
// need to implement a custom event handler thats a issuer signing key resolver.
// Applications that opt for the degraded mode and need client assertions support have
// to implement a custom event handler that attaches an issuer signing key resolver.
if (!context.Options.EnableDegradedMode)
{
if (_applicationManager is null)
@ -813,7 +815,7 @@ public static partial class OpenIddictServerHandlers
}
/// <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>
public sealed class ValidatePrincipal : IOpenIddictServerHandler<ValidateTokenContext>
{
@ -886,7 +888,7 @@ public static partial class OpenIddictServerHandlers
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)));
}
@ -896,7 +898,7 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that use an expired token.
/// Contains the logic responsible for rejecting expired tokens.
/// </summary>
public sealed class ValidateExpirationDate : IOpenIddictServerHandler<ValidateTokenContext>
{
@ -956,8 +958,134 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands that
/// use a token whose entry is no longer valid (e.g was revoked).
/// Contains the logic responsible for rejecting tokens that can't be used by the caller.
/// </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.
/// </summary>
public sealed class ValidateTokenEntry : IOpenIddictServerHandler<ValidateTokenContext>
@ -1136,7 +1264,7 @@ public static partial class OpenIddictServerHandlers
}
/// <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).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>

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

@ -239,7 +239,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse()
};
if (notification.Parameters.Count > 0)
if (notification.Parameters.Count is > 0)
{
foreach (var parameter in notification.Parameters)
{
@ -280,7 +280,7 @@ public static partial class OpenIddictServerHandlers
Response = new OpenIddictResponse()
};
if (notification.Parameters.Count > 0)
if (notification.Parameters.Count is > 0)
{
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; } =
[
/*
* UserInfo request top-level processing:
* Userinfo request top-level processing:
*/
ExtractUserInfoRequest.Descriptor,
ValidateUserInfoRequest.Descriptor,
@ -28,13 +28,13 @@ public static partial class OpenIddictServerHandlers
ApplyUserInfoResponse<ProcessRequestContext>.Descriptor,
/*
* UserInfo request validation:
* Userinfo request validation:
*/
ValidateAccessTokenParameter.Descriptor,
ValidateAuthentication.Descriptor,
/*
* UserInfo request handling:
* Userinfo request handling:
*/
AttachPrincipal.Descriptor,
AttachAudiences.Descriptor,

77
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -601,6 +601,9 @@ public static partial class OpenIddictServerHandlers
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,
TokenFormat = context.ClientAssertionType switch
{
@ -1319,6 +1322,8 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
DisableAudienceValidation = true,
DisablePresenterValidation = true,
Token = context.RequestToken,
ValidTokenTypes = { TokenTypeIdentifiers.Private.RequestToken }
};
@ -1443,6 +1448,10 @@ public static partial class OpenIddictServerHandlers
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,
ValidTokenTypes = { TokenTypeIdentifiers.AccessToken }
};
@ -1515,10 +1524,19 @@ public static partial class OpenIddictServerHandlers
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,
ValidTokenTypes = { TokenTypeIdentifiers.Private.AuthorizationCode }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
@ -1587,10 +1605,19 @@ public static partial class OpenIddictServerHandlers
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,
ValidTokenTypes = { TokenTypeIdentifiers.Private.DeviceCode }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
@ -1659,6 +1686,12 @@ public static partial class OpenIddictServerHandlers
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,
TokenTypeHint = context.GenericTokenTypeHint,
@ -1749,14 +1782,27 @@ public static partial class OpenIddictServerHandlers
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
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.EndSession or
OpenIddictServerEndpointType.PushedAuthorization,
Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeIdentifiers.IdentityToken }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
@ -1825,10 +1871,19 @@ public static partial class OpenIddictServerHandlers
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,
ValidTokenTypes = { TokenTypeIdentifiers.RefreshToken }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
@ -1897,10 +1952,17 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
DisableAudienceValidation = true,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.EndUserVerification,
Token = context.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.
notification.AllowedCharset.UnionWith(context.Options.UserCodeCharset);
@ -2304,7 +2366,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
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));
// When a "resources" property cannot be found in the ticket, infer it from the "audiences" property.
if (context.Principal.HasClaim(Claims.Private.Audience) &&
!context.Principal.HasClaim(Claims.Private.Resource))
if (context.Principal.HasClaim(Claims.Private.Audience) && !context.Principal.HasClaim(Claims.Private.Resource))
{
context.Principal.SetResources(context.Principal.GetAudiences());
}
// 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;
}
@ -4879,7 +4940,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
foreach (var parameter in context.Parameters)
{
@ -5014,7 +5075,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
foreach (var parameter in context.Parameters)
{
@ -5081,7 +5142,7 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
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;
}
/// <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>
/// Gets or sets the security token handler used to validate the token.
/// </summary>
@ -173,6 +188,18 @@ public static partial class OpenIddictValidationEvents
/// </remarks>
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>
/// Gets the token types that are considered valid. If no value is
/// 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<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireLocalValidation>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryValidationEnabled>();
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.
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>
/// Represents a filter that excludes the associated handlers if no token identifier is resolved from the token.
/// </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>
/// Represents a filter that excludes the associated handlers if token validation was not enabled.
/// </summary>
@ -180,4 +214,21 @@ public static class OpenIddictValidationHandlerFilters
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,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor,
ValidatePresenters.Descriptor,
ValidateAudiences.Descriptor,
ValidateTokenEntry.Descriptor,
ValidateAuthorizationEntry.Descriptor,
@ -595,7 +596,7 @@ public static partial class OpenIddictValidationHandlers
}
/// <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>
public sealed class ValidatePrincipal : IOpenIddictValidationHandler<ValidateTokenContext>
{
@ -637,7 +638,7 @@ public static partial class OpenIddictValidationHandlers
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)));
}
@ -647,7 +648,7 @@ public static partial class OpenIddictValidationHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands containing expired access tokens.
/// Contains the logic responsible for rejecting expired tokens.
/// </summary>
public sealed class ValidateExpirationDate : IOpenIddictValidationHandler<ValidateTokenContext>
{
@ -656,6 +657,7 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireTokenLifetimeValidationEnabled>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
@ -689,17 +691,17 @@ public static partial class OpenIddictValidationHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting authentication demands containing
/// access tokens that were issued to be used by another audience/resource server.
/// Contains the logic responsible for rejecting tokens that can't be used by the caller.
/// </summary>
public sealed class ValidateAudience : IOpenIddictValidationHandler<ValidateTokenContext>
public sealed class ValidatePresenters : IOpenIddictValidationHandler<ValidateTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.UseSingletonHandler<ValidateAudience>()
.AddFilter<RequireTokenPresenterValidationEnabled>()
.UseSingletonHandler<ValidatePresenters>()
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -714,18 +716,80 @@ public static partial class OpenIddictValidationHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no explicit audience has been configured,
// skip the default audience validation.
if (context.Options.Audiences.Count is 0)
// If no specific value is expected, skip the default presenter validation.
if (context.ValidPresenters.Count is 0)
{
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();
if (audiences.IsDefaultOrEmpty)
{
context.Logger.LogInformation(6157, SR.GetResourceString(SR.ID6157));
context.Logger.LogInformation(6266, SR.GetResourceString(SR.ID6266));
context.Reject(
error: Errors.InvalidToken,
@ -735,10 +799,10 @@ public static partial class OpenIddictValidationHandlers
return default;
}
// If the access token doesn't include any registered audience, return an error.
if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any())
// If the token doesn't include any registered audience, return an error.
if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.ValidAudiences))
{
context.Logger.LogInformation(6158, SR.GetResourceString(SR.ID6158));
context.Logger.LogInformation(6267, SR.GetResourceString(SR.ID6267));
context.Reject(
error: Errors.InvalidToken,
@ -753,9 +817,8 @@ public static partial class OpenIddictValidationHandlers
}
/// <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).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public sealed class ValidateTokenEntry : IOpenIddictValidationHandler<ValidateTokenContext>
{
@ -774,7 +837,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireTokenEntryValidationEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
.SetOrder(ValidateAudience.Descriptor.Order + 1_000)
.SetOrder(ValidateAudiences.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -807,9 +870,8 @@ public static partial class OpenIddictValidationHandlers
}
/// <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).
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
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 (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any())
if (!OpenIddictHelpers.IncludesAnyFromSet(audiences, context.Options.Audiences))
{
context.Logger.LogInformation(6158, SR.GetResourceString(SR.ID6158));
@ -798,6 +798,10 @@ public static partial class OpenIddictValidationHandlers
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);
if (notification.IsRequestHandled)
@ -895,7 +899,7 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
foreach (var parameter in context.Parameters)
{
@ -962,7 +966,7 @@ public static partial class OpenIddictValidationHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Parameters.Count > 0)
if (context.Parameters.Count is > 0)
{
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)
.SetTokenType(TokenTypeIdentifiers.AccessToken)
.SetPresenters("Fabrikam")
.SetScopes(ImmutableArray<string>.Empty);
.SetScopes([]);
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]);
}
[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]
public async Task ProcessChallenge_ReturnsDefaultErrorWhenNoneIsSpecified()
{

Loading…
Cancel
Save