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));
}