From 375322926736e0bd8a316b746a1ec8f6d75dcb54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 5 Jan 2024 17:11:27 +0100 Subject: [PATCH] Update the client and server stacks to automatically restore the authentication properties and attach them to the authentication context --- .../OpenIddictClientAspNetCoreHandler.cs | 44 +++++---------- .../OpenIddictClientOwinHandler.cs | 44 +++++---------- ...enIddictClientSystemIntegrationHandlers.cs | 48 +++++++++++++++++ .../OpenIddictClientWebIntegrationHandlers.cs | 22 +++----- .../OpenIddictClientHandlers.cs | 45 +++++++++++++++- .../OpenIddictServerAspNetCoreHandler.cs | 38 ++++--------- .../OpenIddictServerOwinHandler.cs | 38 ++++--------- .../OpenIddictServerEvents.cs | 5 ++ .../OpenIddictServerHandlers.cs | 54 +++++++++++++++++++ 9 files changed, 205 insertions(+), 133 deletions(-) diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs index 5614bcef0..b55a07dda 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs @@ -8,7 +8,6 @@ using System.Globalization; using System.Security.Claims; using System.Text.Encodings.Web; -using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; @@ -169,14 +168,20 @@ protected override async Task HandleAuthenticateAsync() else { - // Restore or create a new authentication properties collection and populate it. - var properties = CreateProperties(context.StateTokenPrincipal); - properties.ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate(); - properties.IssuedUtc = context.StateTokenPrincipal?.GetCreationDate(); + var properties = new AuthenticationProperties + { + ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate(), + IssuedUtc = context.StateTokenPrincipal?.GetCreationDate(), + + // Restore the target link URI that was stored in the state + // token when the challenge operation started, if available. + RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri) + }; - // Restore the target link URI that was stored in the state - // token when the challenge operation started, if available. - properties.RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri); + foreach (var property in context.Properties) + { + properties.Items[property.Key] = property.Value; + } List? tokens = null; @@ -334,29 +339,6 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Success(new AuthenticationTicket( context.MergedPrincipal ?? new ClaimsPrincipal(new ClaimsIdentity()), properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme)); - - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is not null) - { - var value = principal.GetClaim(Claims.Private.HostProperties); - if (!string.IsNullOrEmpty(value)) - { - var dictionary = new Dictionary(comparer: StringComparer.Ordinal); - using var document = JsonDocument.Parse(value); - - foreach (var property in document.RootElement.EnumerateObject()) - { - dictionary[property.Name] = property.Value.GetString(); - } - - return new AuthenticationProperties(dictionary); - } - } - - return new AuthenticationProperties(); - } } } diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs index eaf0dd20e..fecb5fae7 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs @@ -10,7 +10,6 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Claims; -using System.Text.Json; using Microsoft.Owin.Security.Infrastructure; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; using Properties = OpenIddict.Client.Owin.OpenIddictClientOwinConstants.Properties; @@ -168,14 +167,20 @@ public override async Task InvokeAsync() else { - // Restore or create a new authentication properties collection and populate it. - var properties = CreateProperties(context.StateTokenPrincipal); - properties.ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate(); - properties.IssuedUtc = context.StateTokenPrincipal?.GetCreationDate(); + var properties = new AuthenticationProperties + { + ExpiresUtc = context.StateTokenPrincipal?.GetExpirationDate(), + IssuedUtc = context.StateTokenPrincipal?.GetCreationDate(), + + // Restore the target link URI that was stored in the state + // token when the challenge operation started, if available. + RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri) + }; - // Restore the target link URI that was stored in the state - // token when the challenge operation started, if available. - properties.RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri); + foreach (var property in context.Properties) + { + properties.Dictionary[property.Key] = property.Value; + } // Attach the tokens to allow any OWIN component (e.g a controller) // to retrieve them (e.g to make an API request to another application). @@ -236,29 +241,6 @@ public override async Task InvokeAsync() } return new AuthenticationTicket(context.MergedPrincipal?.Identity as ClaimsIdentity, properties); - - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is not null) - { - var value = principal.GetClaim(Claims.Private.HostProperties); - if (!string.IsNullOrEmpty(value)) - { - var dictionary = new Dictionary(comparer: StringComparer.Ordinal); - using var document = JsonDocument.Parse(value); - - foreach (var property in document.RootElement.EnumerateObject()) - { - dictionary[property.Name] = property.Value.GetString(); - } - - return new AuthenticationProperties(dictionary); - } - } - - return new AuthenticationProperties(); - } } } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs index a70e3e93c..2ff231b32 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs @@ -48,6 +48,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers RestoreStateTokenFromMarshalledAuthentication.Descriptor, RestoreStateTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreHostAuthenticationPropertiesFromMarshalledAuthentication.Descriptor, RestoreClientRegistrationFromMarshalledContext.Descriptor, RedirectProtocolActivation.Descriptor, @@ -699,6 +700,53 @@ OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, o } } + /// + /// Contains the logic responsible for restoring the host authentication + /// properties from the marshalled authentication context, if applicable. + /// + public sealed class RestoreHostAuthenticationPropertiesFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreHostAuthenticationPropertiesFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveHostAuthenticationPropertiesFromStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + // When the authentication context is marshalled, restore the + // host authentication properties from the other instance. + if (context.EndpointType is OpenIddictClientEndpointType.Unknown && + _marshal.TryGetResult(context.Nonce, out var notification)) + { + foreach (var property in notification.Properties) + { + context.Properties[property.Key] = property.Value; + } + } + + return default; + } + } + /// /// Contains the logic responsible for restoring the client registration and /// configuration from the marshalled authentication context, if applicable. diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs index 0b147af0a..3cb6b3ceb 100644 --- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs +++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs @@ -293,10 +293,9 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) return default; } - // Resolve the shop name from the authentication properties stored in the state token principal. - if (context.StateTokenPrincipal.FindFirst(Claims.Private.HostProperties)?.Value is not string value || - JsonSerializer.Deserialize(value) is not { ValueKind: JsonValueKind.Object } properties || - !properties.TryGetProperty(Shopify.Properties.ShopName, out JsonElement name)) + // Resolve the shop name from the authentication properties. + if (!context.Properties.TryGetValue(Shopify.Properties.ShopName, out string? name) || + string.IsNullOrEmpty(name)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0412)); } @@ -352,13 +351,9 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context) // // For more information, see // https://shopify.dev/docs/apps/auth/oauth/getting-started#step-5-get-an-access-token. - ProviderTypes.Shopify when context.GrantType is GrantTypes.AuthorizationCode => - context.StateTokenPrincipal is ClaimsPrincipal principal && - principal.FindFirst(Claims.Private.HostProperties)?.Value is string value && - JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Object } properties && - properties.TryGetProperty(Shopify.Properties.ShopName, out JsonElement name) ? - new Uri($"https://{name}.myshopify.com/admin/oauth/access_token", UriKind.Absolute) : - throw new InvalidOperationException(SR.GetResourceString(SR.ID0412)), + ProviderTypes.Shopify when context.GrantType is GrantTypes.AuthorizationCode && + context.Properties[Shopify.Properties.ShopName] is var name => + new Uri($"https://{name}.myshopify.com/admin/oauth/access_token", UriKind.Absolute), // Trovo uses a different token endpoint for the refresh token grant. // @@ -1233,9 +1228,8 @@ public ValueTask HandleAsync(ProcessChallengeContext context) // // For more information, see // https://shopify.dev/docs/apps/auth/oauth/getting-started#step-3-ask-for-permission. - ProviderTypes.Shopify => context.Properties.TryGetValue(Shopify.Properties.ShopName, out string? name) ? - new Uri($"https://{name}.myshopify.com/admin/oauth/authorize", UriKind.Absolute) : - throw new InvalidOperationException(SR.GetResourceString(SR.ID0412)), + ProviderTypes.Shopify when context.Properties[Shopify.Properties.ShopName] is var name => + new Uri($"https://{name}.myshopify.com/admin/oauth/authorize", UriKind.Absolute), // Stripe uses a different authorization endpoint for express accounts. // diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 588f5c804..1ebecd946 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; using System.Security.Claims; using System.Text; +using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; @@ -36,6 +37,7 @@ public static partial class OpenIddictClientHandlers ResolveValidatedStateToken.Descriptor, ValidateRequiredStateToken.Descriptor, ValidateStateToken.Descriptor, + ResolveHostAuthenticationPropertiesFromStateToken.Descriptor, ResolveNonceFromStateToken.Descriptor, RedeemStateTokenEntry.Descriptor, ValidateStateTokenEndpointType.Descriptor, @@ -658,6 +660,47 @@ public async ValueTask HandleAsync(ProcessAuthenticationContext context) } } + /// + /// Contains the logic responsible for resolving the host authentication properties from the state token principal. + /// + public sealed class ResolveHostAuthenticationPropertiesFromStateToken : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateStateToken.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var properties = context.StateTokenPrincipal.GetClaim(Claims.Private.HostProperties); + if (!string.IsNullOrEmpty(properties)) + { + using var document = JsonDocument.Parse(properties); + + foreach (var property in document.RootElement.EnumerateObject()) + { + context.Properties[property.Name] = property.Value.GetString(); + } + } + + return default; + } + } + /// /// Contains the logic responsible for resolving the nonce identifying /// the authentication operation from the state token principal. @@ -672,7 +715,7 @@ public sealed class ResolveNonceFromStateToken : IOpenIddictClientHandler() .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateStateToken.Descriptor.Order + 1_000) + .SetOrder(ResolveHostAuthenticationPropertiesFromStateToken.Descriptor.Order + 1_000) .Build(); /// diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index d6fae3627..7c34c6b8e 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -7,7 +7,6 @@ using System.ComponentModel; using System.Security.Claims; using System.Text.Encodings.Web; -using System.Text.Json; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; @@ -200,10 +199,16 @@ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType( _ => null }; - // Restore or create a new authentication properties collection and populate it. - var properties = CreateProperties(principal); - properties.ExpiresUtc = principal?.GetExpirationDate(); - properties.IssuedUtc = principal?.GetCreationDate(); + var properties = new AuthenticationProperties + { + ExpiresUtc = principal?.GetExpirationDate(), + IssuedUtc = principal?.GetCreationDate() + }; + + foreach (var property in context.Properties) + { + properties.Items[property.Key] = property.Value; + } List? tokens = null; @@ -324,29 +329,6 @@ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType( principal ?? new ClaimsPrincipal(new ClaimsIdentity()), properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); } - - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is not null) - { - var value = principal.GetClaim(Claims.Private.HostProperties); - if (!string.IsNullOrEmpty(value)) - { - var dictionary = new Dictionary(comparer: StringComparer.Ordinal); - using var document = JsonDocument.Parse(value); - - foreach (var property in document.RootElement.EnumerateObject()) - { - dictionary[property.Name] = property.Value.GetString(); - } - - return new AuthenticationProperties(dictionary); - } - } - - return new AuthenticationProperties(); - } } /// diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index 7c17632c7..c5197833d 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using System.Security.Claims; -using System.Text.Json; using Microsoft.Owin.Security.Infrastructure; using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants; using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties; @@ -192,10 +191,16 @@ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType( _ => null }; - // Restore or create a new authentication properties collection and populate it. - var properties = CreateProperties(principal); - properties.ExpiresUtc = principal?.GetExpirationDate(); - properties.IssuedUtc = principal?.GetCreationDate(); + var properties = new AuthenticationProperties + { + ExpiresUtc = principal?.GetExpirationDate(), + IssuedUtc = principal?.GetCreationDate() + }; + + foreach (var property in context.Properties) + { + properties.Dictionary[property.Key] = property.Value; + } // Attach the tokens to allow any OWIN component (e.g a controller) // to retrieve them (e.g to make an API request to another application). @@ -237,29 +242,6 @@ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType( return new AuthenticationTicket(principal?.Identity as ClaimsIdentity, properties); } - - static AuthenticationProperties CreateProperties(ClaimsPrincipal? principal) - { - // Note: the principal may be null if no value was extracted from the corresponding token. - if (principal is not null) - { - var value = principal.GetClaim(Claims.Private.HostProperties); - if (!string.IsNullOrEmpty(value)) - { - var dictionary = new Dictionary(comparer: StringComparer.Ordinal); - using var document = JsonDocument.Parse(value); - - foreach (var property in document.RootElement.EnumerateObject()) - { - dictionary[property.Name] = property.Value.GetString(); - } - - return new AuthenticationProperties(dictionary); - } - } - - return new AuthenticationProperties(); - } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index c17dbc954..eb351ba6b 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -313,6 +313,11 @@ public OpenIddictRequest Request set => Transaction.Request = value; } + /// + /// Gets the user-defined authentication properties, if available. + /// + public Dictionary Properties { get; } = new(StringComparer.Ordinal); + /// /// Gets or sets a boolean indicating whether an access /// token should be extracted from the current context. diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 5b60c2d6d..d8afa1491 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.Security.Claims; using System.Text; +using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -48,6 +49,7 @@ public static partial class OpenIddictServerHandlers ValidateIdentityToken.Descriptor, ValidateRefreshToken.Descriptor, ValidateUserCode.Descriptor, + ResolveHostAuthenticationProperties.Descriptor, /* * Challenge processing: @@ -1773,6 +1775,58 @@ public async ValueTask HandleAsync(ProcessAuthenticationContext context) } } + /// + /// Contains the logic responsible for resolving the host authentication properties from the principal, if applicable. + /// + public sealed class ResolveHostAuthenticationProperties : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateUserCode.Descriptor.Order + 1_000) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var principal = context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => context.AuthorizationCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => context.DeviceCodePrincipal, + + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => context.RefreshTokenPrincipal, + + OpenIddictServerEndpointType.Verification => context.UserCodePrincipal, + + _ => null + }; + + if (principal?.GetClaim(Claims.Private.HostProperties) is string value && !string.IsNullOrEmpty(value)) + { + using var document = JsonDocument.Parse(value); + + foreach (var property in document.RootElement.EnumerateObject()) + { + context.Properties[property.Name] = property.Value.GetString(); + } + } + + return default; + } + } + /// /// Contains the logic responsible for rejecting challenge demands made from unsupported endpoints. ///