diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 0f7f2535..525ba8aa 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1668,6 +1668,12 @@ To register the server services, use 'services.AddOpenIddict().AddClient()'. The issuer should be a valid absolute URL at this point. + + The token endpoint should be a valid absolute URL at this point. + + + The userinfo endpoint should be a valid absolute URL at this point. + An error occurred while validating the token '{Token}'. diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index 29d45182..329d49da 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -294,6 +294,16 @@ public static partial class OpenIddictClientEvents /// public string? ResponseType { get; set; } + /// + /// Gets or sets the address of the token endpoint, if applicable. + /// + public Uri? TokenEndpoint { get; set; } + + /// + /// Gets or sets the address of the userinfo endpoint, if applicable. + /// + public Uri? UserinfoEndpoint { get; set; } + /// /// Gets or sets a boolean indicating whether an authorization /// code should be extracted from the current context. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index cea07e93..7599fa43 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -52,6 +52,7 @@ public static partial class OpenIddictClientHandlers EvaluateValidatedBackchannelTokens.Descriptor, + ResolveTokenEndpoint.Descriptor, AttachTokenRequestParameters.Descriptor, SendTokenRequest.Descriptor, ValidateTokenErrorParameters.Descriptor, @@ -69,6 +70,7 @@ public static partial class OpenIddictClientHandlers ValidateBackchannelAccessToken.Descriptor, ValidateRefreshToken.Descriptor, + ResolveUserinfoEndpoint.Descriptor, EvaluateValidatedUserinfoToken.Descriptor, AttachUserinfoRequestParameters.Descriptor, SendUserinfoRequest.Descriptor, @@ -1406,6 +1408,37 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for resolving the address of the token endpoint. + /// + public class ResolveTokenEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context!!) + { + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } || + !configuration.TokenEndpoint.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + } + + context.TokenEndpoint = configuration.TokenEndpoint; + } + } + /// /// Contains the logic responsible for attaching the parameters to the token request, if applicable. /// @@ -1417,7 +1450,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000) + .SetOrder(ResolveTokenEndpoint.Descriptor.Order + 1_000) .Build(); /// @@ -1429,7 +1462,7 @@ public static partial class OpenIddictClientHandlers { return default; } - + // Attach a new request instance if necessary. context.TokenRequest ??= new OpenIddictRequest(); @@ -1501,9 +1534,12 @@ public static partial class OpenIddictClientHandlers /// public async ValueTask HandleAsync(ProcessAuthenticationContext context!!) { + Debug.Assert(context.TokenEndpoint is { IsAbsoluteUri: true } endpoint && + endpoint.IsWellFormedOriginalString(), SR.GetResourceString(SR.ID4014)); Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008)); - context.TokenResponse = await _service.SendTokenRequestAsync(context.Registration, context.TokenRequest); + context.TokenResponse = await _service.SendTokenRequestAsync( + context.Registration, context.TokenEndpoint, context.TokenRequest); } } @@ -2179,6 +2215,37 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for resolving the address of the userinfo endpoint. + /// + public class ResolveUserinfoEndpoint : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context!!) + { + var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + + if (configuration.UserinfoEndpoint is not { IsAbsoluteUri: true } || + !configuration.UserinfoEndpoint.IsWellFormedOriginalString()) + { + throw new InvalidOperationException(SR.FormatID0301(Metadata.UserinfoEndpoint)); + } + + context.UserinfoEndpoint = configuration.UserinfoEndpoint; + } + } + /// /// Contains the logic responsible for determining whether a userinfo token should be validated. /// @@ -2190,7 +2257,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) + .SetOrder(ResolveUserinfoEndpoint.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -2219,7 +2286,7 @@ public static partial class OpenIddictClientHandlers // or responses will be extracted and validated if the userinfo endpoint and either // a frontchannel or backchannel access token was extracted and is available. GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken - when configuration.UserinfoEndpoint is not null && context switch + when context.UserinfoEndpoint is not null && context switch { { ExtractBackchannelAccessToken: true, BackchannelAccessToken.Length: > 0 } => true, { ExtractFrontchannelAccessToken: true, FrontchannelAccessToken.Length: > 0 } => true, @@ -2293,6 +2360,8 @@ public static partial class OpenIddictClientHandlers /// public async ValueTask HandleAsync(ProcessAuthenticationContext context!!) { + Debug.Assert(context.UserinfoEndpoint is { IsAbsoluteUri: true } endpoint && + endpoint.IsWellFormedOriginalString(), SR.GetResourceString(SR.ID4015)); Debug.Assert(context.UserinfoRequest is not null, SR.GetResourceString(SR.ID4008)); // Note: userinfo responses can be of two types: @@ -2300,7 +2369,7 @@ public static partial class OpenIddictClientHandlers // - application/jwt responses containing a signed/encrypted JSON Web Token containing the user claims. (context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) = - await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoRequest); + await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoEndpoint, context.UserinfoRequest); } } diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index c343bf79..94faf009 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -413,19 +413,16 @@ public class OpenIddictClientService /// Sends the token request and retrieves the corresponding response. /// /// The client registration. + /// The address of the token endpoint. /// The token request. /// The that can be used to abort the operation. /// The token response. public async ValueTask SendTokenRequestAsync( - OpenIddictClientRegistration registration!!, OpenIddictRequest request, CancellationToken cancellationToken = default) + OpenIddictClientRegistration registration!!, Uri address!!, OpenIddictRequest request, CancellationToken cancellationToken = default) { - var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } || - !configuration.TokenEndpoint.IsWellFormedOriginalString()) + if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); } cancellationToken.ThrowIfCancellationRequested(); @@ -454,7 +451,7 @@ public class OpenIddictClientService { var context = new PrepareTokenRequestContext(transaction) { - Address = configuration.TokenEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -476,7 +473,7 @@ public class OpenIddictClientService { var context = new ApplyTokenRequestContext(transaction) { - Address = configuration.TokenEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -498,7 +495,7 @@ public class OpenIddictClientService { var context = new ExtractTokenResponseContext(transaction) { - Address = configuration.TokenEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -522,7 +519,7 @@ public class OpenIddictClientService { var context = new HandleTokenResponseContext(transaction) { - Address = configuration.TokenEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request, @@ -560,19 +557,16 @@ public class OpenIddictClientService /// Sends the userinfo request and retrieves the corresponding response. /// /// The client registration. + /// The address of the userinfo endpoint. /// The userinfo request. /// The that can be used to abort the operation. /// The response and the principal extracted from the userinfo response or the userinfo token. public async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserinfoRequestAsync( - OpenIddictClientRegistration registration!!, OpenIddictRequest request, CancellationToken cancellationToken = default) + OpenIddictClientRegistration registration!!, Uri address!!, OpenIddictRequest request, CancellationToken cancellationToken = default) { - var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - - if (configuration.UserinfoEndpoint is not { IsAbsoluteUri: true } || - !configuration.UserinfoEndpoint.IsWellFormedOriginalString()) + if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { - throw new InvalidOperationException(SR.FormatID0301(Metadata.UserinfoEndpoint)); + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); } cancellationToken.ThrowIfCancellationRequested(); @@ -601,7 +595,7 @@ public class OpenIddictClientService { var context = new PrepareUserinfoRequestContext(transaction) { - Address = configuration.UserinfoEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -623,7 +617,7 @@ public class OpenIddictClientService { var context = new ApplyUserinfoRequestContext(transaction) { - Address = configuration.UserinfoEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -645,7 +639,7 @@ public class OpenIddictClientService { var context = new ExtractUserinfoResponseContext(transaction) { - Address = configuration.UserinfoEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request @@ -669,7 +663,7 @@ public class OpenIddictClientService { var context = new HandleUserinfoResponseContext(transaction) { - Address = configuration.UserinfoEndpoint, + Address = address, Issuer = registration.Issuer, Registration = registration, Request = request, diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index fcee2ae6..1117ef42 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -311,7 +311,7 @@ public class OpenIddictValidationService public async ValueTask IntrospectTokenAsync( Uri address!!, string token, string? hint, CancellationToken cancellationToken = default) { - if (!address.IsAbsoluteUri) + if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); }