From 6e1c123dd83af890018db62b5d4e400953112a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 19 Feb 2024 11:17:03 +0100 Subject: [PATCH] Normalize introspection handling in the client and validation stacks --- .../InteractiveService.cs | 6 +- .../OpenIddictResources.resx | 3 + .../OpenIddictClientHandlers.Introspection.cs | 123 ++++++++++++++++- .../OpenIddictClientHandlers.cs | 33 ++--- ...nIddictServerAspNetCoreHandlers.Session.cs | 2 +- .../OpenIddictServerOwinHandlers.Session.cs | 2 +- .../OpenIddictServerHandlers.cs | 8 -- ...nIddictValidationHandlers.Introspection.cs | 124 +++++++++++++++++- ...OpenIddictValidationHandlers.Protection.cs | 5 - .../OpenIddictValidationHandlers.cs | 85 +++++++++++- .../OpenIddictValidationService.cs | 18 +-- 11 files changed, 347 insertions(+), 62 deletions(-) diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index ea14d1c8..58bdf13d 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/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; diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 14f8cb3a..6ec10518 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -2154,6 +2154,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The revocation request was rejected by the remote server. + + The introspection response indicates the token is no longer valid. + The '{0}' parameter shouldn't be null or empty at this point. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs index 633c4169..1728c972 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs +++ b/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 ]; /// @@ -217,7 +220,7 @@ public static partial class OpenIddictClientHandlers } /// - /// 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. /// public sealed class ValidateIssuer : IOpenIddictClientHandler { @@ -270,6 +273,53 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for extracting and validating the expiration date from the introspection response. + /// + public sealed class ValidateExpirationDate : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuer.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + /// /// Contains the logic responsible for extracting and validating the token usage from the introspection response. /// @@ -281,7 +331,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .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; } } + + /// + /// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent. + /// + public sealed class MapInternalClaims : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(PopulateClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 18c4477e..80ea70a6 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/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); } } /// - /// 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. /// - public sealed class MapIntrospectionParametersToWebServicesFederationClaims : IOpenIddictClientHandler + public sealed class MapIntrospectionClaimsToWebServicesFederationClaims : IOpenIddictClientHandler { /// /// 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() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .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)); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs index c442e794..1839ed9b 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs +++ b/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. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs index 16897ea1..d08229d1 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs +++ b/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; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 9c589d02..e9f092ba 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/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 } }; diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index e13cd398..f70121e9 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/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 ]; /// @@ -216,7 +220,7 @@ public static partial class OpenIddictValidationHandlers } /// - /// 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. /// public sealed class ValidateIssuer : IOpenIddictValidationHandler { @@ -269,6 +273,53 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for extracting and validating the expiration date from the introspection response. + /// + public sealed class ValidateExpirationDate : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIssuer.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + /// /// Contains the logic responsible for extracting and validating the token usage from the introspection response. /// @@ -280,7 +331,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .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; } } + + /// + /// Contains the logic responsible for mapping the standard claims to their internal/OpenIddict-specific equivalent. + /// + public sealed class MapInternalClaims : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(PopulateClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 59548754..ad6df128 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -149,7 +149,6 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() .AddFilter() .UseScopedHandler() .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) @@ -219,7 +218,6 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() .UseSingletonHandler() .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) @@ -470,7 +468,6 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() .AddFilter() .UseScopedHandler() .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) @@ -699,7 +696,6 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() .AddFilter() .AddFilter() .UseScopedHandler() @@ -754,7 +750,6 @@ public static partial class OpenIddictValidationHandlers /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() .AddFilter() .AddFilter() .UseScopedHandler() diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 77d91a43..40677ec3 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/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() .UseSingletonHandler() .SetOrder(EvaluateIntrospectionRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// @@ -467,6 +473,7 @@ public static partial class OpenIddictValidationHandlers .AddFilter() .UseSingletonHandler() .SetOrder(GenerateClientAssertion.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// @@ -520,6 +527,7 @@ public static partial class OpenIddictValidationHandlers .AddFilter() .UseSingletonHandler() .SetOrder(AttachIntrospectionRequestClientCredentials.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// @@ -576,6 +584,7 @@ public static partial class OpenIddictValidationHandlers .AddFilter() .UseSingletonHandler() .SetOrder(SendIntrospectionRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); /// @@ -612,6 +621,75 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for validating the audiences of the introspected token returned by the server, if applicable. + /// + public sealed class ValidateIntrospectedTokenAudiences : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateIntrospectedTokenUsage.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + /// /// Contains the logic responsible for ensuring a token was correctly resolved from the context. /// @@ -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 } }; diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index c3f415df..31fed388 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/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>(); - var configuration = await options.CurrentValue.ConfigurationManager - .GetConfigurationAsync(cancellationToken) - .WaitAsync(cancellationToken) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); 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