Browse Source

Normalize introspection handling in the client and validation stacks

pull/1985/head
Kévin Chalet 2 years ago
parent
commit
6e1c123dd8
  1. 6
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  2. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 123
      src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
  4. 33
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  5. 2
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  6. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  7. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  8. 124
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
  9. 5
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  10. 85
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  11. 18
      src/OpenIddict.Validation/OpenIddictValidationService.cs

6
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -219,14 +219,16 @@ public class InteractiveService : BackgroundService
.LeftAligned()
.AddColumn("Claim type")
.AddColumn("Claim value type")
.AddColumn("Claim value");
.AddColumn("Claim value")
.AddColumn("Claim issuer");
foreach (var claim in principal.Claims)
{
table.AddRow(
claim.Type.EscapeMarkup(),
claim.ValueType.EscapeMarkup(),
claim.Value.EscapeMarkup());
claim.Value.EscapeMarkup(),
claim.Issuer.EscapeMarkup());
}
return table;

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -2154,6 +2154,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID2175" xml:space="preserve">
<value>The revocation request was rejected by the remote server.</value>
</data>
<data name="ID2176" xml:space="preserve">
<value>The introspection response indicates the token is no longer valid.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

123
src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs

@ -6,6 +6,7 @@
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@ -24,8 +25,10 @@ public static partial class OpenIddictClientHandlers
HandleErrorResponse.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateIssuer.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenUsage.Descriptor,
PopulateClaims.Descriptor
PopulateClaims.Descriptor,
MapInternalClaims.Descriptor
];
/// <summary>
@ -217,7 +220,7 @@ public static partial class OpenIddictClientHandlers
}
/// <summary>
/// Contains the logic responsible for extracting the issuer from the introspection response.
/// Contains the logic responsible for extracting and validating the issuer from the introspection response.
/// </summary>
public sealed class ValidateIssuer : IOpenIddictClientHandler<HandleIntrospectionResponseContext>
{
@ -270,6 +273,53 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting and validating the expiration date from the introspection response.
/// </summary>
public sealed class ValidateExpirationDate : IOpenIddictClientHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: in most cases, an expired token should lead to an errored or "active=false" response
// being returned by the authorization server. Unfortunately, some implementations are known not
// to check the expiration date of the introspected token before returning a positive response.
//
// To ensure expired tokens are rejected, a manual check is performed here if the
// expiration date was returned as a dedicated claim by the remote authorization server.
if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date &&
date.Add(context.Registration.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow)
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2176),
uri: SR.FormatID8000(SR.ID2176));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
/// </summary>
@ -281,7 +331,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateTokenUsage>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -402,5 +452,72 @@ public static partial class OpenIddictClientHandlers
return default;
}
}
/// <summary>
/// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent.
/// </summary>
public sealed class MapInternalClaims : IOpenIddictClientHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<MapInternalClaims>()
.SetOrder(PopulateClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Map the internal "oi_crt_dt" claim from the standard "iat" claim, if available.
context.Principal.SetCreationDate(context.Principal.GetClaim(Claims.IssuedAt) switch
{
string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
=> DateTimeOffset.FromUnixTimeSeconds(value),
_ => null
});
// Map the internal "oi_exp_dt" claim from the standard "exp" claim, if available.
context.Principal.SetExpirationDate(context.Principal.GetClaim(Claims.ExpiresAt) switch
{
string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
=> DateTimeOffset.FromUnixTimeSeconds(value),
_ => null
});
// Map the internal "oi_aud" claims from the standard "aud" claims, if available.
context.Principal.SetAudiences(context.Principal.GetClaims(Claims.Audience));
// Map the internal "oi_prst" claims from the standard "client_id" claim, if available.
context.Principal.SetPresenters(context.Principal.GetClaim(Claims.ClientId) switch
{
string identifier when !string.IsNullOrEmpty(identifier)
=> ImmutableArray.Create(identifier),
_ => []
});
// Map the internal "oi_scp" claims from the standard, space-separated "scope" claim, if available.
context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) switch
{
string scope => scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(),
_ => []
});
return default;
}
}
}
}

33
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -144,7 +144,7 @@ public static partial class OpenIddictClientHandlers
GenerateIntrospectionClientAssertion.Descriptor,
AttachIntrospectionRequestClientCredentials.Descriptor,
SendIntrospectionRequest.Descriptor,
MapIntrospectionParametersToWebServicesFederationClaims.Descriptor,
MapIntrospectionClaimsToWebServicesFederationClaims.Descriptor,
/*
* Revocation processing:
@ -656,7 +656,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.StateTokenPrincipal,
Token = context.StateToken,
ValidTokenTypes = { TokenTypeHints.StateToken }
};
@ -1587,7 +1586,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.FrontchannelIdentityTokenPrincipal,
Token = context.FrontchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -2102,7 +2100,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.FrontchannelAccessTokenPrincipal,
Token = context.FrontchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -2177,7 +2174,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@ -2917,7 +2913,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.BackchannelIdentityTokenPrincipal,
Token = context.BackchannelIdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -3396,7 +3391,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.BackchannelAccessTokenPrincipal,
Token = context.BackchannelAccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -3471,7 +3465,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@ -3823,7 +3816,6 @@ public static partial class OpenIddictClientHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.UserinfoTokenPrincipal,
Token = context.UserinfoToken,
ValidTokenTypes = { TokenTypeHints.UserinfoToken }
};
@ -4020,9 +4012,9 @@ public static partial class OpenIddictClientHandlers
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
context.MergedPrincipal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
context.MergedPrincipal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
return default;
@ -6359,6 +6351,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.IntrospectionRequest is not null, SR.GetResourceString(SR.ID4008));
// Ensure the introspection endpoint is present and is a valid absolute URI.
@ -6385,15 +6378,20 @@ public static partial class OpenIddictClientHandlers
return;
}
// Attach the registration identifier and identity of the authorization server to the returned principal.
context.Principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
context.Logger.LogTrace(SR.GetResourceString(SR.ID6154), context.Token, context.Principal.Claims);
}
}
/// <summary>
/// Contains the logic responsible for mapping the introspection parameters
/// to their WS-Federation claim equivalent, if applicable.
/// Contains the logic responsible for mapping the standard claims resolved from the
/// introspection response to their WS-Federation claim equivalent, if applicable.
/// </summary>
public sealed class MapIntrospectionParametersToWebServicesFederationClaims : IOpenIddictClientHandler<ProcessIntrospectionContext>
public sealed class MapIntrospectionClaimsToWebServicesFederationClaims : IOpenIddictClientHandler<ProcessIntrospectionContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
@ -6401,7 +6399,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessIntrospectionContext>()
.AddFilter<RequireWebServicesFederationClaimMappingEnabled>()
.UseSingletonHandler<MapIntrospectionParametersToWebServicesFederationClaims>()
.UseSingletonHandler<MapIntrospectionClaimsToWebServicesFederationClaims>()
.SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -6414,11 +6412,6 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
if (context.Options.DisableWebServicesFederationClaimMapping)
{
return default;
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));

2
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -340,7 +340,7 @@ public static partial class OpenIddictServerAspNetCoreHandlers
return default;
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, response);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, context.Response);
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -340,7 +340,7 @@ public static partial class OpenIddictServerOwinHandlers
return default;
}
context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, response);
context.Logger.LogInformation(SR.GetResourceString(SR.ID6151), context.PostLogoutRedirectUri, context.Response);
var location = context.PostLogoutRedirectUri;

8
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -554,7 +554,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.ClientAssertionPrincipal,
Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch
{
@ -1255,7 +1254,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
@ -1328,7 +1326,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.AuthorizationCodePrincipal,
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@ -1401,7 +1398,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.DeviceCodePrincipal,
Token = context.DeviceCode,
ValidTokenTypes = { TokenTypeHints.DeviceCode }
};
@ -1474,7 +1470,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.GenericTokenPrincipal,
Token = context.GenericToken,
TokenTypeHint = context.GenericTokenTypeHint,
@ -1568,7 +1563,6 @@ public static partial class OpenIddictServerHandlers
// Don't validate the lifetime of id_tokens used as id_token_hints.
DisableLifetimeValidation = context.EndpointType is OpenIddictServerEndpointType.Authorization or
OpenIddictServerEndpointType.Logout,
Principal = context.IdentityTokenPrincipal,
Token = context.IdentityToken,
ValidTokenTypes = { TokenTypeHints.IdToken }
};
@ -1641,7 +1635,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.RefreshTokenPrincipal,
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@ -1714,7 +1707,6 @@ public static partial class OpenIddictServerHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
Principal = context.UserCodePrincipal,
Token = context.UserCode,
ValidTokenTypes = { TokenTypeHints.UserCode }
};

124
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -5,6 +5,8 @@
*/
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
@ -23,8 +25,10 @@ public static partial class OpenIddictValidationHandlers
HandleErrorResponse.Descriptor,
HandleInactiveResponse.Descriptor,
ValidateIssuer.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateTokenUsage.Descriptor,
PopulateClaims.Descriptor
PopulateClaims.Descriptor,
MapInternalClaims.Descriptor
];
/// <summary>
@ -216,7 +220,7 @@ public static partial class OpenIddictValidationHandlers
}
/// <summary>
/// Contains the logic responsible for extracting the issuer from the introspection response.
/// Contains the logic responsible for extracting and validating the issuer from the introspection response.
/// </summary>
public sealed class ValidateIssuer : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
@ -269,6 +273,53 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for extracting and validating the expiration date from the introspection response.
/// </summary>
public sealed class ValidateExpirationDate : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateExpirationDate>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: in most cases, an expired token should lead to an errored or "active=false" response
// being returned by the authorization server. Unfortunately, some implementations are known not
// to check the expiration date of the introspected token before returning a positive response.
//
// To ensure expired tokens are rejected, a manual check is performed here if the
// expiration date was returned as a dedicated claim by the remote authorization server.
if (long.TryParse((string?) context.Response[Claims.ExpiresAt],
NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) &&
DateTimeOffset.FromUnixTimeSeconds(value) is DateTimeOffset date &&
date.Add(context.Options.TokenValidationParameters.ClockSkew) < DateTimeOffset.UtcNow)
{
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2176),
uri: SR.FormatID8000(SR.ID2176));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for extracting and validating the token usage from the introspection response.
/// </summary>
@ -280,7 +331,7 @@ public static partial class OpenIddictValidationHandlers
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<ValidateTokenUsage>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
@ -403,5 +454,72 @@ public static partial class OpenIddictValidationHandlers
return default;
}
}
/// <summary>
/// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent.
/// </summary>
public sealed class MapInternalClaims : IOpenIddictValidationHandler<HandleIntrospectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<HandleIntrospectionResponseContext>()
.UseSingletonHandler<MapInternalClaims>()
.SetOrder(PopulateClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleIntrospectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Map the internal "oi_crt_dt" claim from the standard "iat" claim, if available.
context.Principal.SetCreationDate(context.Principal.GetClaim(Claims.IssuedAt) switch
{
string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
=> DateTimeOffset.FromUnixTimeSeconds(value),
_ => null
});
// Map the internal "oi_exp_dt" claim from the standard "exp" claim, if available.
context.Principal.SetExpirationDate(context.Principal.GetClaim(Claims.ExpiresAt) switch
{
string date when long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
=> DateTimeOffset.FromUnixTimeSeconds(value),
_ => null
});
// Map the internal "oi_aud" claims from the standard "aud" claims, if available.
context.Principal.SetAudiences(context.Principal.GetClaims(Claims.Audience));
// Map the internal "oi_prst" claims from the standard "client_id" claim, if available.
context.Principal.SetPresenters(context.Principal.GetClaim(Claims.ClientId) switch
{
string identifier when !string.IsNullOrEmpty(identifier)
=> ImmutableArray.Create(identifier),
_ => []
});
// Map the internal "oi_scp" claims from the standard, space-separated "scope" claim, if available.
context.Principal.SetScopes(context.Principal.GetClaim(Claims.Scope) switch
{
string scope => scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray(),
_ => []
});
return default;
}
}
}
}

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

@ -149,7 +149,6 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<ValidateReferenceTokenIdentifier>()
.SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000)
@ -219,7 +218,6 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.UseSingletonHandler<ValidateIdentityModelToken>()
.SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
@ -470,7 +468,6 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.UseScopedHandler<RestoreTokenEntryProperties>()
.SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
@ -699,7 +696,6 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireTokenEntryValidationEnabled>()
.AddFilter<RequireTokenIdResolved>()
.UseScopedHandler<ValidateTokenEntry>()
@ -754,7 +750,6 @@ public static partial class OpenIddictValidationHandlers
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ValidateTokenContext>()
.AddFilter<RequireLocalValidation>()
.AddFilter<RequireAuthorizationEntryValidationEnabled>()
.AddFilter<RequireAuthorizationIdResolved>()
.UseScopedHandler<ValidateAuthorizationEntry>()

85
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -34,6 +34,7 @@ public static partial class OpenIddictValidationHandlers
AttachIntrospectionRequestClientCredentials.Descriptor,
SendIntrospectionRequest.Descriptor,
ValidateIntrospectedTokenUsage.Descriptor,
ValidateIntrospectedTokenAudiences.Descriptor,
ValidateAccessToken.Descriptor,
/*
@ -81,8 +82,12 @@ public static partial class OpenIddictValidationHandlers
context.ValidateAccessToken,
context.RejectAccessToken) = context.EndpointType switch
{
// The validation handler is responsible for validating access tokens for endpoints
// it doesn't manage (typically, API endpoints using token authentication).
// When introspection is used, ask the server to validate the token.
OpenIddictValidationEndpointType.Unknown
when context.Options.ValidationType is OpenIddictValidationType.Introspection
=> (true, true, false, true),
// Otherwise, always validate it locally.
OpenIddictValidationEndpointType.Unknown => (true, true, true, true),
_ => (false, false, false, false)
@ -261,6 +266,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionRequestParameters>()
.SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
@ -467,6 +473,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<AttachIntrospectionRequestClientCredentials>()
.SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
@ -520,6 +527,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<SendIntrospectionRequest>()
.SetOrder(AttachIntrospectionRequestClientCredentials.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
@ -576,6 +584,7 @@ public static partial class OpenIddictValidationHandlers
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<ValidateIntrospectedTokenUsage>()
.SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
@ -612,6 +621,75 @@ public static partial class OpenIddictValidationHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the audiences of the introspected token returned by the server, if applicable.
/// </summary>
public sealed class ValidateIntrospectedTokenAudiences : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIntrospectionRequest>()
.UseSingletonHandler<ValidateIntrospectedTokenAudiences>()
.SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000)
.SetType(OpenIddictValidationHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// In theory, authorization servers are expected to return an error (or an active=false response)
// when the caller is not allowed to introspect the token (e.g because it's not a valid audience
// or authorized party). Unfortunately, some servers are known to have a relaxed validation policy.
//
// To ensure the token can be used with this resource server, a second pass is manually performed here.
// If no explicit audience has been configured, skip the audience validation.
if (context.Options.Audiences.Count is 0)
{
return default;
}
// If the access token doesn't have any audience attached, return an error.
var audiences = context.AccessTokenPrincipal.GetAudiences();
if (audiences.IsDefaultOrEmpty)
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6157));
context.Reject(
error: Errors.InvalidToken,
description: SR.GetResourceString(SR.ID2093),
uri: SR.FormatID8000(SR.ID2093));
return default;
}
// If the access token doesn't include any registered audience, return an error.
if (!audiences.Intersect(context.Options.Audiences, StringComparer.Ordinal).Any())
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6158));
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 ensuring a token was correctly resolved from the context.
/// </summary>
@ -648,9 +726,6 @@ public static partial class OpenIddictValidationHandlers
var notification = new ValidateTokenContext(context.Transaction)
{
// When using introspection, the principal is already available as it is extracted
// from the introspection response returned by the authorization server.
Principal = context.AccessTokenPrincipal,
Token = context.AccessToken,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};

18
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -8,9 +8,7 @@ using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.Validation;
@ -53,21 +51,13 @@ public class OpenIddictValidationService
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
var configuration = await options.CurrentValue.ConfigurationManager
.GetConfigurationAsync(cancellationToken)
.WaitAsync(cancellationToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ValidateTokenContext(transaction)
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
Token = token,
ValidTokenTypes = { TokenTypeHints.AccessToken }
AccessToken = token
};
await dispatcher.DispatchAsync(context);
@ -79,9 +69,9 @@ public class OpenIddictValidationService
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
return context.Principal;
return context.AccessTokenPrincipal;
}
finally

Loading…
Cancel
Save