diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
index 67ef74a8..cb440579 100644
--- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
+++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
@@ -102,7 +102,7 @@ public class AuthenticationController : Controller
// Preserve the access and refresh tokens returned in the token response, if available.
{
Name: OpenIddictClientAspNetCoreConstants.Tokens.BackchannelAccessToken or
- OpenIddictClientAspNetCoreConstants.Tokens.BackchannelRefreshToken
+ OpenIddictClientAspNetCoreConstants.Tokens.RefreshToken
} => true,
// Ignore the other tokens.
diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
index 98b6973d..b078ab77 100644
--- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs
+++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs
@@ -482,6 +482,7 @@ public static class OpenIddictConstants
public const string IdToken = "id_token";
public const string RefreshToken = "refresh_token";
public const string StateToken = "state_token";
+ public const string UserinfoToken = "userinfo_token";
public const string UserCode = "user_code";
}
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 08ee8d03..a38aba6c 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1578,6 +1578,15 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa
The specified state token is not valid in this context.
+
+ The '{0}' claim extracted from the specified userinfo response/token is malformed or isn't of the expected type.
+
+
+ The mandatory '{0}' claim cannot be found in the specified userinfo response/token.
+
+
+ The '{0}' claim returned in the specified userinfo response/token doesn't match the expected value.
+
The '{0}' parameter shouldn't be null or empty at this point.
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
index 7f75f244..178184e6 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
@@ -90,4 +90,9 @@ public class OpenIddictConfiguration
/// Gets the client authentication methods supported by the token endpoint.
///
public HashSet TokenEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets or sets the address of the userinfo endpoint.
+ ///
+ public Uri? UserinfoEndpoint { get; set; }
}
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
index 55ed7def..6443512e 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
+++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
@@ -13,27 +13,29 @@ public static class OpenIddictClientAspNetCoreConstants
{
public static class Properties
{
+ public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_id_token_principal";
- public const string BackchannelRefreshTokenPrincipal = ".backchannel_refresh_token_principal";
public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal";
- public const string FrontchannelAuthorizationCodePrincipal = ".frontchannel_authorization_code_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_id_token_principal";
- public const string FrontchannelStateTokenPrincipal = ".frontchannel_state_token_principal";
public const string Issuer = ".issuer";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
+ public const string RefreshTokenPrincipal = ".refresh_token_principal";
+ public const string StateTokenPrincipal = ".state_token_principal";
+ public const string UserinfoTokenPrincipal = ".userinfo_token_principal";
}
public static class Tokens
{
+ public const string AuthorizationCode = "authorization_code";
public const string BackchannelAccessToken = "backchannel_access_token";
public const string BackchannelIdentityToken = "backchannel_id_token";
- public const string BackchannelRefreshToken = "backchannel_refresh_token";
public const string FrontchannelAccessToken = "frontchannel_access_token";
- public const string FrontchannelAuthorizationCode = "frontchannel_authorization_code";
public const string FrontchannelIdentityToken = "frontchannel_id_token";
- public const string FrontchannelStateToken = "frontchannel_state_token";
+ public const string RefreshToken = "refresh_token";
+ public const string StateToken = "state_token";
+ public const string UserinfoToken = "userinfo_token";
}
}
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
index d9c76139..81d80909 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
+++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
@@ -8,6 +8,7 @@ using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants;
using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties;
@@ -141,16 +142,12 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler
- context.BackchannelIdentityTokenPrincipal ??
- context.FrontchannelIdentityTokenPrincipal ??
- new ClaimsPrincipal(new ClaimsIdentity()),
+ // Create a composite principal containing claims resolved from the frontchannel
+ // and backchannel identity tokens and the userinfo token principal, if available.
+ OpenIddictClientEndpointType.Redirection => CreatePrincipal(
+ context.FrontchannelIdentityTokenPrincipal,
+ context.BackchannelIdentityTokenPrincipal,
+ context.UserinfoTokenPrincipal),
_ => null
};
@@ -167,7 +164,7 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler? tokens = null;
@@ -175,6 +172,16 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler 0 })
@@ -264,8 +257,87 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
+ {
+ return new ClaimsPrincipal(new ClaimsIdentity());
+ }
+
+ // Create a new composite identity containing the claims of all the principals.
+ var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
+
+ foreach (var principal in principals)
+ {
+ // Note: the principal may be null if no value was extracted from the corresponding token.
+ if (principal is null)
+ {
+ continue;
+ }
+
+ foreach (var claim in principal.Claims)
+ {
+ // If a claim with the same type and the same value already exist, skip it.
+ if (identity.HasClaim(claim.Type, claim.Value))
+ {
+ continue;
+ }
+
+ identity.AddClaim(claim);
+ }
+ }
+
+ return new ClaimsPrincipal(identity);
+ }
}
}
diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
index 322192c4..80930d3c 100644
--- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
+++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
@@ -363,9 +363,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.AddFilter()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
- .SetOrder(ValidateFrontchannelStateToken.Descriptor.Order + 500)
+ .SetOrder(ValidateStateToken.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -377,7 +377,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
@@ -390,7 +390,7 @@ public static partial class OpenIddictClientAspNetCoreHandlers
// Resolve the request forgery protection from the state token principal.
// If the claim cannot be found, this means the protection was disabled
// using a custom event handler. In this case, bypass the validation.
- var claim = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection);
+ var claim = context.StateTokenPrincipal.GetClaim(Claims.RequestForgeryProtection);
if (string.IsNullOrEmpty(claim))
{
return default;
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs
new file mode 100644
index 00000000..5ca792c5
--- /dev/null
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs
@@ -0,0 +1,18 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+namespace OpenIddict.Client.SystemNetHttp;
+
+///
+/// Exposes common constants used by the OpenIddict System.Net.Http integration.
+///
+public static class OpenIddictClientSystemNetHttpConstants
+{
+ public static class ContentTypes
+ {
+ public const string JsonWebToken = "application/jwt";
+ }
+}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs
new file mode 100644
index 00000000..50e100ed
--- /dev/null
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs
@@ -0,0 +1,136 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Net.Http.Headers;
+using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants;
+
+namespace OpenIddict.Client.SystemNetHttp;
+
+public static partial class OpenIddictClientSystemNetHttpHandlers
+{
+ public static class Userinfo
+ {
+ public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create(
+ /*
+ * Userinfo request processing:
+ */
+ PrepareGetHttpRequest.Descriptor,
+ AttachBearerAccessToken.Descriptor,
+ AttachFormParameters.Descriptor,
+ SendHttpRequest.Descriptor,
+ DisposeHttpRequest.Descriptor,
+
+ /*
+ * Userinfo response processing:
+ */
+ ExtractUserinfoHttpResponse.Descriptor,
+ DisposeHttpResponse.Descriptor);
+
+ ///
+ /// Contains the logic responsible of attaching the access token to the HTTP Authorization header.
+ ///
+ public class AttachBearerAccessToken : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachFormParameters.Descriptor.Order - 1000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(PrepareUserinfoRequestContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008));
+
+ // This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
+ // this may indicate that the request was incorrectly processed by another client stack.
+ var request = context.Transaction.GetHttpRequestMessage();
+ if (request is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
+ }
+
+ // Attach the authorization header containing the access token to the HTTP request.
+ request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, context.Request.AccessToken);
+
+ // Remove the access from the request payload to ensure it's not sent twice.
+ context.Request.AccessToken = null;
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of extracting the response from the userinfo response.
+ ///
+ public class ExtractUserinfoHttpResponse : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(DisposeHttpResponse.Descriptor.Order - 50_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ExtractUserinfoResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved,
+ // this may indicate that the request was incorrectly processed by another client stack.
+ var response = context.Transaction.GetHttpResponseMessage();
+ if (response is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
+ }
+
+ // The status code is deliberately not validated to ensure even errored responses
+ // (typically in the 4xx range) can be deserialized and handled by the event handlers.
+
+ // Note: userinfo responses can be of two types:
+ // - application/json responses containing a JSON object listing the user claims as-is.
+ // - application/jwt responses containing a signed/encrypted JSON Web Token containing the user claims.
+ //
+ // As such, this handler implements a selection routine to extract the userinfo token as-is
+ // if the media type is application/jwt and fall back to JSON in any other case.
+
+ if (string.Equals(response.Content.Headers.ContentType?.MediaType,
+ ContentTypes.JsonWebToken, StringComparison.OrdinalIgnoreCase))
+ {
+ context.Response = new OpenIddictResponse();
+ context.UserinfoToken = await response.Content.ReadAsStringAsync();
+ }
+
+ else
+ {
+ // Note: ReadFromJsonAsync() automatically validates the content type and the content encoding
+ // and transcode the response stream if a non-UTF-8 response is returned by the remote server.
+ context.Response = await response.Content.ReadFromJsonAsync();
+ }
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
index cb1807f6..9699d65e 100644
--- a/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
+++ b/src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
@@ -19,7 +19,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
public static ImmutableArray DefaultHandlers { get; }
= ImmutableArray.Create()
.AddRange(Discovery.DefaultHandlers)
- .AddRange(Exchange.DefaultHandlers);
+ .AddRange(Exchange.DefaultHandlers)
+ .AddRange(Userinfo.DefaultHandlers);
///
/// Contains the logic responsible of preparing an HTTP GET request message.
diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Userinfo.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Userinfo.cs
new file mode 100644
index 00000000..b16cf7cb
--- /dev/null
+++ b/src/OpenIddict.Client/OpenIddictClientEvents.Userinfo.cs
@@ -0,0 +1,140 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Security.Claims;
+
+namespace OpenIddict.Client;
+
+public static partial class OpenIddictClientEvents
+{
+ ///
+ /// Represents an event called for each request to the userinfo endpoint
+ /// to give the user code a chance to add parameters to the userinfo request.
+ ///
+ public class PrepareUserinfoRequestContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public PrepareUserinfoRequestContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+ }
+
+ ///
+ /// Represents an event called for each request to the userinfo endpoint
+ /// to send the userinfo request to the remote authorization server.
+ ///
+ public class ApplyUserinfoRequestContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ApplyUserinfoRequestContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+ }
+
+ ///
+ /// Represents an event called for each userinfo response
+ /// to extract the response parameters from the server response.
+ ///
+ public class ExtractUserinfoResponseContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ExtractUserinfoResponseContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the response, or null if it wasn't extracted yet.
+ ///
+ public OpenIddictResponse? Response
+ {
+ get => Transaction.Response;
+ set => Transaction.Response = value;
+ }
+
+ ///
+ /// Gets or sets the userinfo token, if available.
+ ///
+ public string? UserinfoToken { get; set; }
+ }
+
+ ///
+ /// Represents an event called for each userinfo response.
+ ///
+ public class HandleUserinfoResponseContext : BaseExternalContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public HandleUserinfoResponseContext(OpenIddictClientTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the response.
+ ///
+ public OpenIddictResponse Response
+ {
+ get => Transaction.Response!;
+ set => Transaction.Response = value;
+ }
+
+ ///
+ /// Gets or sets the userinfo token, if available.
+ ///
+ public string? UserinfoToken { get; set; }
+
+ ///
+ /// Gets or sets the principal containing the claims resolved from the userinfo response.
+ ///
+ public ClaimsPrincipal? Principal { get; set; }
+ }
+}
diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs
index 7bc8d5ff..86380185 100644
--- a/src/OpenIddict.Client/OpenIddictClientEvents.cs
+++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs
@@ -284,6 +284,14 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
+ ///
+ /// Gets or sets a boolean indicating whether an authorization
+ /// code should be extracted from the current context.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ExtractAuthorizationCode { get; set; }
+
///
/// Gets or sets a boolean indicating whether a backchannel
/// access token should be extracted from the current context.
@@ -301,44 +309,52 @@ public static partial class OpenIddictClientEvents
public bool ExtractBackchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a backchannel
- /// refresh token should be extracted from the current context.
+ /// Gets or sets a boolean indicating whether a frontchannel
+ /// access token should be extracted from the current context.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ExtractBackchannelRefreshToken { get; set; }
+ public bool ExtractFrontchannelAccessToken { get; set; }
///
/// Gets or sets a boolean indicating whether a frontchannel
- /// access token should be extracted from the current context.
+ /// identity token should be extracted from the current context.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ExtractFrontchannelAccessToken { get; set; }
+ public bool ExtractFrontchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a frontchannel
- /// authorization code should be extracted from the current context.
+ /// Gets or sets a boolean indicating whether a refresh
+ /// token should be extracted from the current context.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ExtractFrontchannelAuthorizationCode { get; set; }
+ public bool ExtractRefreshToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a frontchannel
- /// identity token should be extracted from the current context.
+ /// Gets or sets a boolean indicating whether a state
+ /// token should be extracted from the current context.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ExtractFrontchannelIdentityToken { get; set; }
+ public bool ExtractStateToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a frontchannel
- /// state token should be extracted from the current context.
+ /// Gets or sets a boolean indicating whether a userinfo
+ /// token should be extracted from the current context.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ExtractFrontchannelStateToken { get; set; }
+ public bool ExtractUserinfoToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an authorization
+ /// code must be resolved for the authentication to be considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireAuthorizationCode { get; set; }
///
/// Gets or sets a boolean indicating whether a backchannel access
@@ -357,12 +373,12 @@ public static partial class OpenIddictClientEvents
public bool RequireBackchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a backchannel refresh
+ /// Gets or sets a boolean indicating whether a frontchannel identity
/// token must be resolved for the authentication to be considered valid.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool RequireBackchannelRefreshToken { get; set; }
+ public bool RequireFrontchannelAccessToken { get; set; }
///
/// Gets or sets a boolean indicating whether a frontchannel identity
@@ -370,31 +386,39 @@ public static partial class OpenIddictClientEvents
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool RequireFrontchannelAccessToken { get; set; }
+ public bool RequireFrontchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a backchannel authorization
- /// code must be resolved for the authentication to be considered valid.
+ /// Gets or sets a boolean indicating whether a refresh token
+ /// must be resolved for the authentication to be considered valid.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool RequireFrontchannelAuthorizationCode { get; set; }
+ public bool RequireRefreshToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a frontchannel identity
- /// token must be resolved for the authentication to be considered valid.
+ /// Gets or sets a boolean indicating whether a state token
+ /// must be resolved for the authentication to be considered valid.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool RequireFrontchannelIdentityToken { get; set; }
+ public bool RequireStateToken { get; set; }
///
- /// Gets or sets a boolean indicating whether a frontchannel state token
+ /// Gets or sets a boolean indicating whether a userinfo token
/// must be resolved for the authentication to be considered valid.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool RequireFrontchannelStateToken { get; set; }
+ public bool RequireUserinfoToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether the authorization
+ /// code extracted from the current context should be validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateAuthorizationCode { get; set; }
///
/// Gets or sets a boolean indicating whether the backchannel access
@@ -413,44 +437,49 @@ public static partial class OpenIddictClientEvents
public bool ValidateBackchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether the backchannel refresh token
- /// extracted from the current context should be validated.
+ /// Gets or sets a boolean indicating whether the frontchannel access
+ /// token extracted from the current context should be validated.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ValidateBackchannelRefreshToken { get; set; }
+ public bool ValidateFrontchannelAccessToken { get; set; }
///
- /// Gets or sets a boolean indicating whether the frontchannel access
+ /// Gets or sets a boolean indicating whether the frontchannel identity
/// token extracted from the current context should be validated.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ValidateFrontchannelAccessToken { get; set; }
+ public bool ValidateFrontchannelIdentityToken { get; set; }
///
- /// Gets or sets a boolean indicating whether the frontchannel authorization
- /// code extracted from the current context should be validated.
+ /// Gets or sets a boolean indicating whether the refresh token
+ /// extracted from the current context should be validated.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ValidateFrontchannelAuthorizationCode { get; set; }
+ public bool ValidateRefreshToken { get; set; }
///
- /// Gets or sets a boolean indicating whether the frontchannel identity
- /// token extracted from the current context should be validated.
+ /// Gets or sets a boolean indicating whether the state token
+ /// extracted from the current context should be validated.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ValidateFrontchannelIdentityToken { get; set; }
+ public bool ValidateStateToken { get; set; }
///
- /// Gets or sets a boolean indicating whether the frontchannel state token
+ /// Gets or sets a boolean indicating whether the userinfo token
/// extracted from the current context should be validated.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
///
- public bool ValidateFrontchannelStateToken { get; set; }
+ public bool ValidateUserinfoToken { get; set; }
+
+ ///
+ /// Gets or sets the authorization code to validate, if applicable.
+ ///
+ public string? AuthorizationCode { get; set; }
///
/// Gets or sets the backchannel access token to validate, if applicable.
@@ -462,30 +491,35 @@ public static partial class OpenIddictClientEvents
///
public string? BackchannelIdentityToken { get; set; }
- ///
- /// Gets or sets the backchannel refresh token to validate, if applicable.
- ///
- public string? BackchannelRefreshToken { get; set; }
-
///
/// Gets or sets the frontchannel access token to validate, if applicable.
///
public string? FrontchannelAccessToken { get; set; }
///
- /// Gets or sets the frontchannel authorization code to validate, if applicable.
+ /// Gets or sets the frontchannel identity token to validate, if applicable.
///
- public string? FrontchannelAuthorizationCode { get; set; }
+ public string? FrontchannelIdentityToken { get; set; }
///
- /// Gets or sets the frontchannel identity token to validate, if applicable.
+ /// Gets or sets the refresh token to validate, if applicable.
///
- public string? FrontchannelIdentityToken { get; set; }
+ public string? RefreshToken { get; set; }
///
/// Gets or sets the frontchannel state token to validate, if applicable.
///
- public string? FrontchannelStateToken { get; set; }
+ public string? StateToken { get; set; }
+
+ ///
+ /// Gets or sets the userinfo token to validate, if applicable.
+ ///
+ public string? UserinfoToken { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the authorization code, if applicable.
+ ///
+ public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
///
/// Gets or sets the principal extracted from the backchannel access token, if applicable.
@@ -497,11 +531,6 @@ public static partial class OpenIddictClientEvents
///
public ClaimsPrincipal? BackchannelIdentityTokenPrincipal { get; set; }
- ///
- /// Gets or sets the principal extracted from the backchannel refresh token, if applicable.
- ///
- public ClaimsPrincipal? BackchannelRefreshTokenPrincipal { get; set; }
-
///
/// Gets or sets the principal extracted from the frontchannel access token, if applicable.
///
@@ -513,14 +542,19 @@ public static partial class OpenIddictClientEvents
public ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; set; }
///
- /// Gets or sets the principal extracted from the frontchannel authorization code, if applicable.
+ /// Gets or sets the principal extracted from the refresh token, if applicable.
///
- public ClaimsPrincipal? FrontchannelAuthorizationCodePrincipal { get; set; }
+ public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
///
- /// Gets or sets the principal extracted from the frontchannel state token, if applicable.
+ /// Gets or sets the principal extracted from the state token, if applicable.
///
- public ClaimsPrincipal? FrontchannelStateTokenPrincipal { get; set; }
+ public ClaimsPrincipal? StateTokenPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the userinfo token, if applicable.
+ ///
+ public ClaimsPrincipal? UserinfoTokenPrincipal { get; set; }
///
/// Gets or sets the request sent to the token endpoint, if applicable.
@@ -531,6 +565,16 @@ public static partial class OpenIddictClientEvents
/// Gets or sets the response returned by the token endpoint, if applicable.
///
public OpenIddictResponse? TokenResponse { get; set; }
+
+ ///
+ /// Gets or sets the request sent to the userinfo endpoint, if applicable.
+ ///
+ public OpenIddictRequest? UserinfoRequest { get; set; }
+
+ ///
+ /// Gets or sets the response returned by the userinfo endpoint, if applicable.
+ ///
+ public OpenIddictResponse? UserinfoResponse { get; set; }
}
///
diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
index a24cbdd5..3ed65016 100644
--- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs
+++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
@@ -36,19 +36,26 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton();
// Register the built-in filters used by the default OpenIddict client event handlers.
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
// Register the built-in client event handlers used by the OpenIddict client components.
// Note: the order used here is not important, as the actual order is set in the options.
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
index 8cd751a5..79774859 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
@@ -11,6 +11,22 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientHandlerFilters
{
+ ///
+ /// Represents a filter that excludes the associated handlers if no authorization code is extracted.
+ ///
+ public class RequireAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ExtractAuthorizationCode);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the challenge
/// doesn't correspond to an authorization code or implicit grant operation.
@@ -28,6 +44,22 @@ public static class OpenIddictClientHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no authorization code is validated.
+ ///
+ public class RequireAuthorizationCodeValidated : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateAuthorizationCode);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if no backchannel access token is validated.
///
@@ -44,6 +76,22 @@ public static class OpenIddictClientHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no backchannel identity token principal is available.
+ ///
+ public class RequireBackchannelIdentityTokenPrincipal : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.BackchannelIdentityTokenPrincipal is not null);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if no backchannel identity token is validated.
///
@@ -61,9 +109,9 @@ public static class OpenIddictClientHandlerFilters
}
///
- /// Represents a filter that excludes the associated handlers if no backchannel refresh token is validated.
+ /// Represents a filter that excludes the associated handlers if no frontchannel access token is validated.
///
- public class RequireBackchannelRefreshTokenValidated : IOpenIddictClientHandlerFilter
+ public class RequireFrontchannelAccessTokenValidated : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -72,14 +120,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ValidateBackchannelRefreshToken);
+ return new ValueTask(context.ValidateFrontchannelAccessToken);
}
}
///
- /// Represents a filter that excludes the associated handlers if no backchannel request is expected to be sent.
+ /// Represents a filter that excludes the associated handlers if no frontchannel identity token principal is available.
///
- public class RequireBackchannelRequest : IOpenIddictClientHandlerFilter
+ public class RequireFrontchannelIdentityTokenPrincipal : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -88,14 +136,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.TokenRequest is not null);
+ return new ValueTask(context.FrontchannelIdentityTokenPrincipal is not null);
}
}
///
- /// Represents a filter that excludes the associated handlers if no backchannel response was received.
+ /// Represents a filter that excludes the associated handlers if no frontchannel identity token is validated.
///
- public class RequireBackchannelResponse : IOpenIddictClientHandlerFilter
+ public class RequireFrontchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -104,14 +152,30 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.TokenResponse is not null);
+ return new ValueTask(context.ValidateFrontchannelIdentityToken);
}
}
///
- /// Represents a filter that excludes the associated handlers if no frontchannel access token is validated.
+ /// Represents a filter that excludes the associated handlers if the request is not a redirection request.
///
- public class RequireFrontchannelAccessTokenValidated : IOpenIddictClientHandlerFilter
+ public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(BaseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.EndpointType == OpenIddictClientEndpointType.Redirection);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no refresh token is validated.
+ ///
+ public class RequireRefreshTokenValidated : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -120,14 +184,30 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ValidateFrontchannelAccessToken);
+ return new ValueTask(context.ValidateRefreshToken);
}
}
///
- /// Represents a filter that excludes the associated handlers if no frontchannel authorization code is extracted.
+ /// Represents a filter that excludes the associated handlers if no state token is generated.
///
- public class RequireFrontchannelAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter
+ public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.GenerateStateToken);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no state token principal is available.
+ ///
+ public class RequireStateTokenPrincipal : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -136,14 +216,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ExtractFrontchannelAuthorizationCode);
+ return new ValueTask(context.StateTokenPrincipal is not null);
}
}
///
- /// Represents a filter that excludes the associated handlers if no frontchannel authorization code is validated.
+ /// Represents a filter that excludes the associated handlers if no state token is validated.
///
- public class RequireFrontchannelAuthorizationCodeValidated : IOpenIddictClientHandlerFilter
+ public class RequireStateTokenValidated : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -152,14 +232,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ValidateFrontchannelAuthorizationCode);
+ return new ValueTask(context.ValidateStateToken);
}
}
///
- /// Represents a filter that excludes the associated handlers if no frontchannel identity token is validated.
+ /// Represents a filter that excludes the associated handlers if no token request is expected to be sent.
///
- public class RequireFrontchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter
+ public class RequireTokenRequest : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -168,14 +248,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ValidateFrontchannelIdentityToken);
+ return new ValueTask(context.TokenRequest is not null);
}
}
///
- /// Represents a filter that excludes the associated handlers if no frontchannel state token is validated.
+ /// Represents a filter that excludes the associated handlers if no token response was received.
///
- public class RequireFrontchannelStateTokenValidated : IOpenIddictClientHandlerFilter
+ public class RequireTokenResponse : IOpenIddictClientHandlerFilter
{
public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
@@ -184,39 +264,71 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.ValidateFrontchannelStateToken);
+ return new ValueTask(context.TokenResponse is not null);
}
}
///
- /// Represents a filter that excludes the associated handlers if the request is not a redirection request.
+ /// Represents a filter that excludes the associated handlers if no userinfo request is expected to be sent.
///
- public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter
+ public class RequireUserinfoRequest : IOpenIddictClientHandlerFilter
{
- public ValueTask IsActiveAsync(BaseContext context)
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.EndpointType == OpenIddictClientEndpointType.Redirection);
+ return new ValueTask(context.UserinfoRequest is not null);
}
}
///
- /// Represents a filter that excludes the associated handlers if no state token is generated.
+ /// Represents a filter that excludes the associated handlers if no userinfo response was received.
///
- public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter
+ public class RequireUserinfoResponse : IOpenIddictClientHandlerFilter
{
- public ValueTask IsActiveAsync(ProcessChallengeContext context)
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(context.GenerateStateToken);
+ return new ValueTask(context.UserinfoResponse is not null);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no userinfo token is extracted.
+ ///
+ public class RequireUserinfoTokenExtracted : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ExtractUserinfoToken);
+ }
+ }
+
+ ///
+ /// Represents a filter that excludes the associated handlers if no userinfo token principal is available.
+ ///
+ public class RequireUserinfoTokenPrincipal : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.UserinfoTokenPrincipal is not null);
}
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
index 12639c0e..ba6eaed1 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
@@ -500,9 +500,9 @@ public static partial class OpenIddictClientHandlers
return;
}
- // Attach the security principal extracted from the token to the validation context.
+ // Attach the security principals extracted from the tokens to the validation context.
context.Principal = notification.FrontchannelIdentityTokenPrincipal;
- context.StateTokenPrincipal = notification.FrontchannelStateTokenPrincipal;
+ context.StateTokenPrincipal = notification.StateTokenPrincipal;
}
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
index fd75f692..433bbebb 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
@@ -22,13 +22,14 @@ public static partial class OpenIddictClientHandlers
ExtractAuthorizationEndpoint.Descriptor,
ExtractCryptographyEndpoint.Descriptor,
ExtractTokenEndpoint.Descriptor,
- ExtractTokenEndpointClientAuthenticationMethods.Descriptor,
+ ExtractUserinfoEndpoint.Descriptor,
ExtractGrantTypes.Descriptor,
ExtractResponseModes.Descriptor,
ExtractResponseTypes.Descriptor,
ExtractCodeChallengeMethods.Descriptor,
ExtractScopes.Descriptor,
ExtractIssuerParameterRequirement.Descriptor,
+ ExtractTokenEndpointClientAuthenticationMethods.Descriptor,
/*
* Cryptography response handling:
@@ -88,6 +89,58 @@ public static partial class OpenIddictClientHandlers
}
}
+ ///
+ /// Contains the logic responsible of extracting the authorization endpoint address from the discovery document.
+ ///
+ public class ExtractAuthorizationEndpoint : 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(HandleConfigurationResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Note: the authorization_endpoint node is required by the OpenID Connect discovery specification
+ // but is optional in the OAuth 2.0 authorization server metadata specification. To make OpenIddict
+ // compatible with the newer OAuth 2.0 specification, null/empty and missing values are allowed here.
+ //
+ // Handlers that require a non-null authorization endpoint URL are expected to return an error
+ // if the authorization endpoint URL couldn't be resolved from the authorization server metadata.
+ // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationClient
+ // and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information.
+ //
+ var address = (string?) context.Response[Metadata.AuthorizationEndpoint];
+ if (!string.IsNullOrEmpty(address))
+ {
+ if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
+ {
+ context.Reject(
+ error: Errors.ServerError,
+ description: SR.FormatID2100(Metadata.AuthorizationEndpoint),
+ uri: SR.FormatID8000(SR.ID2100));
+
+ return default;
+ }
+
+ context.Configuration.AuthorizationEndpoint = uri;
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document.
///
@@ -99,7 +152,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
+ .SetOrder(ExtractAuthorizationEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -184,17 +237,16 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of extracting the authentication methods
- /// supported by the token endpoint from the discovery document.
+ /// Contains the logic responsible of extracting the userinfo endpoint address from the discovery document.
///
- public class ExtractTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler
+ public class ExtractUserinfoEndpoint : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
+ .UseSingletonHandler()
.SetOrder(ExtractTokenEndpoint.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -207,71 +259,20 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- // Resolve the client authentication methods supported by the token endpoint, if available.
- var methods = context.Response[Metadata.TokenEndpointAuthMethodsSupported]?.GetUnnamedParameters();
- if (methods is { Count: > 0 })
- {
- for (var index = 0; index < methods.Count; index++)
- {
- // Note: custom values are allowed in this case.
- var method = (string?) methods[index];
- if (!string.IsNullOrEmpty(method))
- {
- context.Configuration.TokenEndpointAuthMethodsSupported.Add(method);
- }
- }
- }
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible of extracting the authorization endpoint address from the discovery document.
- ///
- public class ExtractAuthorizationEndpoint : IOpenIddictClientHandler
- {
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictClientHandlerDescriptor Descriptor { get; }
- = OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
- .SetType(OpenIddictClientHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(HandleConfigurationResponseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // Note: the authorization_endpoint node is required by the OpenID Connect discovery specification
- // but is optional in the OAuth 2.0 authorization server metadata specification. To make OpenIddict
- // compatible with the newer OAuth 2.0 specification, null/empty and missing values are allowed here.
- //
- // Handlers that require a non-null authorization endpoint URL are expected to return an error
- // if the authorization endpoint URL couldn't be resolved from the authorization server metadata.
- // See https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationClient
- // and https://datatracker.ietf.org/doc/html/rfc8414#section-2 for more information.
- //
- var address = (string?) context.Response[Metadata.AuthorizationEndpoint];
+ var address = (string?) context.Response[Metadata.UserinfoEndpoint];
if (!string.IsNullOrEmpty(address))
{
if (!Uri.TryCreate(address, UriKind.Absolute, out Uri? uri) || !uri.IsWellFormedOriginalString())
{
context.Reject(
error: Errors.ServerError,
- description: SR.FormatID2100(Metadata.AuthorizationEndpoint),
+ description: SR.FormatID2100(Metadata.UserinfoEndpoint),
uri: SR.FormatID8000(SR.ID2100));
return default;
}
- context.Configuration.AuthorizationEndpoint = uri;
+ context.Configuration.UserinfoEndpoint = uri;
}
return default;
@@ -519,6 +520,49 @@ public static partial class OpenIddictClientHandlers
}
}
+ ///
+ /// Contains the logic responsible of extracting the authentication methods
+ /// supported by the token endpoint from the discovery document.
+ ///
+ public class ExtractTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ExtractIssuerParameterRequirement.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleConfigurationResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Resolve the client authentication methods supported by the token endpoint, if available.
+ var methods = context.Response[Metadata.TokenEndpointAuthMethodsSupported]?.GetUnnamedParameters();
+ if (methods is { Count: > 0 })
+ {
+ for (var index = 0; index < methods.Count; index++)
+ {
+ // Note: custom values are allowed in this case.
+ var method = (string?) methods[index];
+ if (!string.IsNullOrEmpty(method))
+ {
+ context.Configuration.TokenEndpointAuthMethodsSupported.Add(method);
+ }
+ }
+ }
+
+ return default;
+ }
+ }
+
///
/// Contains the logic responsible of extracting the signing keys from the JWKS document.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
index fa041b79..6a98612a 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
@@ -212,20 +212,33 @@ public static partial class OpenIddictClientHandlers
_ => true // Allow any other claim.
});
- // Attach the principal extracted from the token to the parent event context and store
- // the token type (resolved from "typ" or "token_usage") as a special private claim.
- context.Principal = new ClaimsPrincipal(identity).SetTokenType(result.TokenType switch
+ if (context.ValidTokenTypes.Contains(TokenTypeHints.StateToken))
{
- null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
+ // Attach the principal extracted from the token to the parent event context and store
+ // the token type (resolved from "typ" or "token_usage") as a special private claim.
+ context.Principal = new ClaimsPrincipal(identity).SetTokenType(result.TokenType switch
+ {
+ null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
- // Both JWT and application/JWT are supported for identity tokens.
- JsonWebTokenTypes.IdentityToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken
- => TokenTypeHints.IdToken,
+ JsonWebTokenTypes.Private.StateToken => TokenTypeHints.StateToken,
- JsonWebTokenTypes.Private.StateToken => TokenTypeHints.StateToken,
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ });
+ }
- _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
- });
+ else if (context.ValidTokenTypes.Count is 1)
+ {
+ // JSON Web Tokens defined by the OpenID Connect core specification (e.g identity or userinfo tokens)
+ // don't have to include a specific "typ" header and all values are allowed. As such, the tokens
+ // as assumed to be of the type that is expected by the authentication routine. Additional checks
+ // like audience validation can be implemented to prevent tokens mix-up/confused deputy attacks.
+ context.Principal = new ClaimsPrincipal(identity).SetTokenType(context.ValidTokenTypes.Single());
+ }
+
+ else
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0308));
+ }
// Store the resolved signing algorithm from the token and attach it to the principal.
context.Principal.SetClaim(Claims.Private.SigningAlgorithm, token.Alg);
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs
new file mode 100644
index 00000000..e2150491
--- /dev/null
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs
@@ -0,0 +1,214 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+ * See https://github.com/openiddict/openiddict-core for more information concerning
+ * the license and the contributors participating to this project.
+ */
+
+using System.Collections.Immutable;
+using System.Globalization;
+using System.Security.Claims;
+using System.Text.Json;
+using Microsoft.IdentityModel.JsonWebTokens;
+
+namespace OpenIddict.Client;
+
+public static partial class OpenIddictClientHandlers
+{
+ public static class Userinfo
+ {
+ public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create(
+ /*
+ * Userinfo response handling:
+ */
+ HandleErrorResponse.Descriptor,
+ ValidateWellKnownClaims.Descriptor,
+ PopulateClaims.Descriptor);
+
+ ///
+ /// Contains the logic responsible of validating the well-known parameters contained in the userinfo response.
+ ///
+ public class ValidateWellKnownClaims : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleUserinfoResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Ignore the response instance if a userinfo token was extracted.
+ if (!string.IsNullOrEmpty(context.UserinfoToken))
+ {
+ return default;
+ }
+
+ foreach (var parameter in context.Response.GetParameters())
+ {
+ if (ValidateClaimType(parameter.Key, parameter.Value.Value))
+ {
+ continue;
+ }
+
+ context.Reject(
+ error: Errors.ServerError,
+ description: SR.FormatID2107(parameter.Key),
+ uri: SR.FormatID8000(SR.ID2107));
+
+ return default;
+ }
+
+ return default;
+
+ static bool ValidateClaimType(string name, object? value) => name switch
+ {
+ // The 'sub' parameter MUST be formatted as a string value.
+ Claims.Subject => value is string or JsonElement { ValueKind: JsonValueKind.String },
+
+ // Parameters that are not in the well-known list can be of any type.
+ _ => true
+ };
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of extracting the claims from the introspection response.
+ ///
+ public class PopulateClaims : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(HandleUserinfoResponseContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Ignore the response instance if a userinfo token was extracted.
+ if (!string.IsNullOrEmpty(context.UserinfoToken))
+ {
+ return default;
+ }
+
+ // Create a new claims-based identity using the same authentication type
+ // and the name/role claims as the one used by IdentityModel for JWT tokens.
+ var identity = new ClaimsIdentity(
+ context.Registration.TokenValidationParameters.AuthenticationType,
+ context.Registration.TokenValidationParameters.NameClaimType,
+ context.Registration.TokenValidationParameters.RoleClaimType);
+
+ // Resolve the issuer that will be attached to the claims created by this handler.
+ // Note: at this stage, the optional issuer extracted from the response is assumed
+ // to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
+ var issuer = (string?) context.Response[Claims.Issuer] ?? context.Issuer?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer;
+
+ foreach (var parameter in context.Response.GetParameters())
+ {
+ // Always exclude null keys and values, as they can't be represented as valid claims.
+ if (string.IsNullOrEmpty(parameter.Key) || OpenIddictParameter.IsNullOrEmpty(parameter.Value))
+ {
+ continue;
+ }
+
+ // Exclude OpenIddict-specific private claims, that MUST NOT be set based on data returned
+ // by the remote authorization server (that may or may not be an OpenIddict server).
+ if (parameter.Key.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ // Ignore all protocol claims that shouldn't be mapped to CLR claims.
+ if (parameter.Key is Claims.Active or Claims.Issuer or Claims.NotBefore or Claims.TokenType)
+ {
+ continue;
+ }
+
+ switch (parameter.Value.Value)
+ {
+ // Claims represented as arrays are split and mapped to multiple CLR claims.
+ case JsonElement { ValueKind: JsonValueKind.Array } value:
+ foreach (var element in value.EnumerateArray())
+ {
+ var item = element.GetString();
+ if (string.IsNullOrEmpty(item))
+ {
+ continue;
+ }
+
+ identity.AddClaim(new Claim(parameter.Key, item,
+ GetClaimValueType(value.ValueKind), issuer, issuer, identity));
+ }
+ break;
+
+ case JsonElement value:
+ identity.AddClaim(new Claim(parameter.Key, value.ToString()!,
+ GetClaimValueType(value.ValueKind), issuer, issuer, identity));
+ break;
+
+ // Note: in the typical case, the introspection parameters should be deserialized from
+ // a JSON response and thus represented as System.Text.Json.JsonElement instances.
+ // However, to support responses resolved from custom locations and parameters manually added
+ // by the application using the events model, the CLR primitive types are also supported.
+
+ case bool value:
+ identity.AddClaim(new Claim(parameter.Key, value.ToString(),
+ ClaimValueTypes.Boolean, issuer, issuer, identity));
+ break;
+
+ case long value:
+ identity.AddClaim(new Claim(parameter.Key, value.ToString(CultureInfo.InvariantCulture),
+ ClaimValueTypes.Integer64, issuer, issuer, identity));
+ break;
+
+ case string value:
+ identity.AddClaim(new Claim(parameter.Key, value, ClaimValueTypes.String, issuer, issuer, identity));
+ break;
+
+ // Claims represented as arrays are split and mapped to multiple CLR claims.
+ case string[] value:
+ for (var index = 0; index < value.Length; index++)
+ {
+ identity.AddClaim(new Claim(parameter.Key, value[index], ClaimValueTypes.String, issuer, issuer, identity));
+ }
+ break;
+ }
+ }
+
+ context.Principal = new ClaimsPrincipal(identity);
+
+ return default;
+
+ static string GetClaimValueType(JsonValueKind kind) => kind switch
+ {
+ JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean,
+
+ JsonValueKind.String => ClaimValueTypes.String,
+ JsonValueKind.Number => ClaimValueTypes.Integer64,
+
+ JsonValueKind.Array => JsonClaimValueTypes.JsonArray,
+ JsonValueKind.Object or _ => JsonClaimValueTypes.Json
+ };
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 4cbed4b2..d0050572 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -27,9 +27,9 @@ public static partial class OpenIddictClientHandlers
*/
ValidateAuthenticationDemand.Descriptor,
EvaluateValidatedUpfrontTokens.Descriptor,
- ResolveUpfrontTokens.Descriptor,
- ValidateRequiredUpfrontTokens.Descriptor,
- ValidateFrontchannelStateToken.Descriptor,
+ ResolveValidatedStateToken.Descriptor,
+ ValidateRequiredStateToken.Descriptor,
+ ValidateStateToken.Descriptor,
ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor,
ValidateFrontchannelErrorParameters.Descriptor,
@@ -47,13 +47,13 @@ public static partial class OpenIddictClientHandlers
ValidateFrontchannelTokenDigests.Descriptor,
ValidateFrontchannelAccessToken.Descriptor,
- ValidateFrontchannelAuthorizationCode.Descriptor,
+ ValidateAuthorizationCode.Descriptor,
EvaluateValidatedBackchannelTokens.Descriptor,
- AttachBackchannelRequestParameters.Descriptor,
- SendBackchannelRequest.Descriptor,
- ValidateBackchannelErrorParameters.Descriptor,
+ AttachTokenRequestParameters.Descriptor,
+ SendTokenRequest.Descriptor,
+ ValidateTokenErrorParameters.Descriptor,
ResolveValidatedBackchannelTokens.Descriptor,
ValidateRequiredBackchannelTokens.Descriptor,
@@ -66,7 +66,16 @@ public static partial class OpenIddictClientHandlers
ValidateBackchannelTokenDigests.Descriptor,
ValidateBackchannelAccessToken.Descriptor,
- ValidateBackchannelRefreshToken.Descriptor,
+ ValidateRefreshToken.Descriptor,
+
+ EvaluateValidatedUserinfoToken.Descriptor,
+ AttachUserinfoRequestParameters.Descriptor,
+ SendUserinfoRequest.Descriptor,
+ ValidateUserinfoErrorParameters.Descriptor,
+ ValidateRequiredUserinfoToken.Descriptor,
+ ValidateUserinfoToken.Descriptor,
+ ValidateUserinfoTokenWellknownClaims.Descriptor,
+ ValidateUserinfoTokenWellknownSubject.Descriptor,
/*
* Challenge processing:
@@ -95,7 +104,8 @@ public static partial class OpenIddictClientHandlers
.AddRange(Authentication.DefaultHandlers)
.AddRange(Discovery.DefaultHandlers)
- .AddRange(Protection.DefaultHandlers);
+ .AddRange(Protection.DefaultHandlers)
+ .AddRange(Userinfo.DefaultHandlers);
///
/// Contains the logic responsible of rejecting invalid authentication demands.
@@ -155,9 +165,9 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- (context.ExtractFrontchannelStateToken,
- context.RequireFrontchannelStateToken,
- context.ValidateFrontchannelStateToken) = context.EndpointType switch
+ (context.ExtractStateToken,
+ context.RequireStateToken,
+ context.ValidateStateToken) = context.EndpointType switch
{
// While the OAuth 2.0/2.1 and OpenID Connect specifications don't require sending a
// state as part of authorization requests, the identity provider MUST return the state
@@ -174,16 +184,16 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of resolving the tokens to validate upfront from the incoming request.
+ /// Contains the logic responsible of resolving the state token to validate upfront from the incoming request.
///
- public class ResolveUpfrontTokens : IOpenIddictClientHandler
+ public class ResolveValidatedStateToken : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
+ .UseSingletonHandler()
.SetOrder(EvaluateValidatedUpfrontTokens.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -196,9 +206,9 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- context.FrontchannelStateToken = context.EndpointType switch
+ context.StateToken = context.EndpointType switch
{
- OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelStateToken
+ OpenIddictClientEndpointType.Redirection when context.ExtractStateToken
=> context.Request.State,
_ => null
@@ -209,9 +219,9 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of rejecting authentication demands that lack required upfront tokens.
+ /// Contains the logic responsible of rejecting authentication demands that lack the required state token.
///
- public class ValidateRequiredUpfrontTokens : IOpenIddictClientHandler
+ public class ValidateRequiredStateToken : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
@@ -219,7 +229,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .SetOrder(ResolveUpfrontTokens.Descriptor.Order + 1_000)
+ .SetOrder(ResolveValidatedStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -231,7 +241,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.RequireFrontchannelStateToken && string.IsNullOrEmpty(context.FrontchannelStateToken))
+ if (context.RequireStateToken && string.IsNullOrEmpty(context.StateToken))
{
context.Reject(
error: Errors.MissingToken,
@@ -248,11 +258,11 @@ public static partial class OpenIddictClientHandlers
///
/// Contains the logic responsible of validating the state token resolved from the context.
///
- public class ValidateFrontchannelStateToken : IOpenIddictClientHandler
+ public class ValidateStateToken : IOpenIddictClientHandler
{
private readonly IOpenIddictClientDispatcher _dispatcher;
- public ValidateFrontchannelStateToken(IOpenIddictClientDispatcher dispatcher)
+ public ValidateStateToken(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher;
///
@@ -260,9 +270,9 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseScopedHandler()
- .SetOrder(ValidateRequiredUpfrontTokens.Descriptor.Order + 1_000)
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateRequiredStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -274,15 +284,15 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.FrontchannelStateTokenPrincipal is not null ||
- string.IsNullOrEmpty(context.FrontchannelStateToken))
+ if (context.StateTokenPrincipal is not null ||
+ string.IsNullOrEmpty(context.StateToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
- Token = context.FrontchannelStateToken,
+ Token = context.StateToken,
ValidTokenTypes = { TokenTypeHints.StateToken }
};
@@ -309,7 +319,7 @@ public static partial class OpenIddictClientHandlers
return;
}
- context.FrontchannelStateTokenPrincipal = notification.Principal;
+ context.StateTokenPrincipal = notification.Principal;
}
}
@@ -324,9 +334,9 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
- .SetOrder(ValidateFrontchannelStateToken.Descriptor.Order + 1_000)
+ .SetOrder(ValidateStateToken.Descriptor.Order + 1_000)
.Build();
///
@@ -337,7 +347,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Retrieve the client definition using the authorization server stored in the state token.
//
@@ -349,7 +359,7 @@ public static partial class OpenIddictClientHandlers
// Restore the identity of the authorization server from the special "as" claim.
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-2
// for more information.
- var value = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.AuthorizationServer);
+ var value = context.StateTokenPrincipal.GetClaim(Claims.AuthorizationServer);
if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? issuer) ||
!issuer.IsWellFormedOriginalString())
{
@@ -514,7 +524,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@@ -528,10 +538,10 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the negotiated grant type from the state token.
- var type = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType);
+ var type = context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType);
// Note: OpenIddict currently only supports the implicit and authorization code
// grants but additional grants (like CIBA) may be supported in future versions.
@@ -564,6 +574,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateGrantType.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@@ -577,47 +588,47 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the grant grant and the response type stored in the state token and extract its individual elements.
var types = (
- GrantType: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType),
- ResponseTypes: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
+ GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType),
+ ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)
.ToImmutableHashSet());
- (context.ExtractFrontchannelAccessToken,
- context.RequireFrontchannelAccessToken,
- context.ValidateFrontchannelAccessToken) = types switch
+ (context.ExtractAuthorizationCode,
+ context.RequireAuthorizationCode,
+ context.ValidateAuthorizationCode) = types switch
{
- // An access token is returned for the authorization code and implicit grants when
- // the response type contains the "token" value, which includes some variations of
- // the implicit and hybrid flows, but not the authorization code flow. As such,
- // a frontchannel access token is only considered required if a token was requested.
+ // An authorization code is returned for the authorization code and implicit grants when
+ // the response type contains the "code" value, which includes the authorization code
+ // flow and some variations of the hybrid flow. As such, an authorization code is only
+ // considered required if the negotiated response_type includes "code".
//
- // Note: since access tokens are supposed to be opaque to the clients, they are never
+ // Note: since authorization codes are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set)
- when set.Contains(ResponseTypes.Token) => (true, true, false),
+ when set.Contains(ResponseTypes.Code) => (true, true, false),
_ => (false, false, false)
};
- (context.ExtractFrontchannelAuthorizationCode,
- context.RequireFrontchannelAuthorizationCode,
- context.ValidateFrontchannelAuthorizationCode) = types switch
+ (context.ExtractFrontchannelAccessToken,
+ context.RequireFrontchannelAccessToken,
+ context.ValidateFrontchannelAccessToken) = types switch
{
- // An authorization code is returned for the authorization code and implicit grants when
- // the response type contains the "code" value, which includes the authorization code
- // flow and some variations of the hybrid flow. As such, an authorization code is only
- // considered required if the negotiated response_type includes "code".
+ // An access token is returned for the authorization code and implicit grants when
+ // the response type contains the "token" value, which includes some variations of
+ // the implicit and hybrid flows, but not the authorization code flow. As such,
+ // a frontchannel access token is only considered required if a token was requested.
//
- // Note: since authorization codes are supposed to be opaque to the clients, they are never
+ // Note: since access tokens are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set)
- when set.Contains(ResponseTypes.Code) => (true, true, false),
+ when set.Contains(ResponseTypes.Token) => (true, true, false),
_ => (false, false, false)
};
@@ -666,18 +677,18 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- context.FrontchannelAccessToken = context.EndpointType switch
+ context.AuthorizationCode = context.EndpointType switch
{
- OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken
- => context.Request.AccessToken,
+ OpenIddictClientEndpointType.Redirection when context.ExtractAuthorizationCode
+ => context.Request.Code,
_ => null
};
- context.FrontchannelAuthorizationCode = context.EndpointType switch
+ context.FrontchannelAccessToken = context.EndpointType switch
{
- OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAuthorizationCode
- => context.Request.Code,
+ OpenIddictClientEndpointType.Redirection when context.ExtractFrontchannelAccessToken
+ => context.Request.AccessToken,
_ => null
};
@@ -717,9 +728,9 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if ((context.RequireFrontchannelAccessToken && string.IsNullOrEmpty(context.FrontchannelAccessToken)) ||
- (context.RequireFrontchannelAuthorizationCode && string.IsNullOrEmpty(context.FrontchannelAuthorizationCode)) ||
- (context.RequireFrontchannelIdentityToken && string.IsNullOrEmpty(context.FrontchannelIdentityToken)))
+ if ((context.RequireAuthorizationCode && string.IsNullOrEmpty(context.AuthorizationCode)) ||
+ (context.RequireFrontchannelAccessToken && string.IsNullOrEmpty(context.FrontchannelAccessToken)) ||
+ (context.RequireFrontchannelIdentityToken && string.IsNullOrEmpty(context.FrontchannelIdentityToken)))
{
context.Reject(
error: Errors.MissingToken,
@@ -811,7 +822,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateFrontchannelIdentityToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@@ -948,7 +959,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateFrontchannelIdentityTokenWellknownClaims.Descriptor.Order + 1_000)
.Build();
@@ -993,7 +1004,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateFrontchannelIdentityTokenAudience.Descriptor.Order + 1_000)
.Build();
@@ -1036,8 +1047,8 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
+ .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateFrontchannelIdentityTokenPresenter.Descriptor.Order + 1_000)
.Build();
@@ -1051,11 +1062,11 @@ public static partial class OpenIddictClientHandlers
}
Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
switch ((
FrontchannelIdentityTokenNonce: context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
- StateTokenNonce: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
+ StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
{
// If no nonce if no present in the state token (e.g because the authorization server doesn't
// support OpenID Connect and response_type=code was negotiated), bypass the validation logic.
@@ -1100,7 +1111,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateFrontchannelIdentityTokenNonce.Descriptor.Order + 1_000)
.Build();
@@ -1161,7 +1172,7 @@ public static partial class OpenIddictClientHandlers
// If an authorization code was returned in the authorization response,
// ensure the c_hash claim matches the hash of the actual authorization code.
- if (!string.IsNullOrEmpty(context.FrontchannelAuthorizationCode))
+ if (!string.IsNullOrEmpty(context.AuthorizationCode))
{
var hash = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.CodeHash);
if (string.IsNullOrEmpty(hash))
@@ -1174,7 +1185,7 @@ public static partial class OpenIddictClientHandlers
return default;
}
- if (!ValidateTokenHash(algorithm, context.FrontchannelAuthorizationCode, hash))
+ if (!ValidateTokenHash(algorithm, context.AuthorizationCode, hash))
{
context.Reject(
error: Errors.InvalidRequest,
@@ -1297,15 +1308,15 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of validating the frontchannel authorization code resolved from the context.
+ /// Contains the logic responsible of validating the authorization code resolved from the context.
/// Note: this handler is typically not used for standard-compliant implementations as authorization codes
/// are supposed to be opaque to clients.
///
- public class ValidateFrontchannelAuthorizationCode : IOpenIddictClientHandler
+ public class ValidateAuthorizationCode : IOpenIddictClientHandler
{
private readonly IOpenIddictClientDispatcher _dispatcher;
- public ValidateFrontchannelAuthorizationCode(IOpenIddictClientDispatcher dispatcher)
+ public ValidateAuthorizationCode(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher;
///
@@ -1313,8 +1324,8 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseScopedHandler()
+ .AddFilter()
+ .UseScopedHandler()
.SetOrder(ValidateFrontchannelAccessToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -1327,15 +1338,15 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.FrontchannelAuthorizationCodePrincipal is not null ||
- string.IsNullOrEmpty(context.FrontchannelAuthorizationCode))
+ if (context.AuthorizationCodePrincipal is not null ||
+ string.IsNullOrEmpty(context.AuthorizationCode))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
- Token = context.FrontchannelAuthorizationCode,
+ Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeHints.AuthorizationCode }
};
@@ -1362,7 +1373,7 @@ public static partial class OpenIddictClientHandlers
return;
}
- context.FrontchannelAuthorizationCodePrincipal = notification.Principal;
+ context.AuthorizationCodePrincipal = notification.Principal;
}
}
@@ -1376,9 +1387,9 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
- .SetOrder(ValidateFrontchannelAuthorizationCode.Descriptor.Order + 1_000)
+ .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -1390,12 +1401,12 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
-
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
// Resolve the grant grant and the response type stored in the state token and extract its individual elements.
var types = (
- GrantType: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType),
- ResponseTypes: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
+ GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType),
+ ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)
.ToImmutableHashSet());
@@ -1426,14 +1437,14 @@ public static partial class OpenIddictClientHandlers
// include "openid", which indicates the initial request was an OpenID Connect request.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set)
when set.Contains(ResponseTypes.Code) &&
- context.FrontchannelStateTokenPrincipal.HasScope(Scopes.OpenId) => (true, true, true),
+ context.StateTokenPrincipal.HasScope(Scopes.OpenId) => (true, true, true),
_ => (false, false, false)
};
- (context.ExtractBackchannelRefreshToken,
- context.RequireBackchannelRefreshToken,
- context.ValidateBackchannelRefreshToken) = types switch
+ (context.ExtractRefreshToken,
+ context.RequireRefreshToken,
+ context.ValidateRefreshToken) = types switch
{
// A refresh token may be returned as part of token responses, depending on the
// policy enforced by the remote authorization server (e.g the "offline_access"
@@ -1454,18 +1465,18 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of attaching the parameters to the backchannel token request, if applicable.
+ /// Contains the logic responsible of attaching the parameters to the token request, if applicable.
///
- public class AttachBackchannelRequestParameters : IOpenIddictClientHandler
+ public class AttachTokenRequestParameters : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
.SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000)
.Build();
@@ -1477,13 +1488,13 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Attach a new request instance if none was created already.
context.TokenRequest ??= new OpenIddictRequest();
// Attach the grant type selected during the challenge phase.
- context.TokenRequest.GrantType = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.GrantType) switch
+ context.TokenRequest.GrantType = context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType) switch
{
null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0294)),
@@ -1509,9 +1520,9 @@ public static partial class OpenIddictClientHandlers
// the redirect_uri from the state token principal and attach them to the request, if available.
if (context.TokenRequest.GrantType is GrantTypes.AuthorizationCode)
{
- context.TokenRequest.Code = context.FrontchannelAuthorizationCode;
- context.TokenRequest.CodeVerifier = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.CodeVerifier);
- context.TokenRequest.RedirectUri = context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.RedirectUri);
+ context.TokenRequest.Code = context.AuthorizationCode;
+ context.TokenRequest.CodeVerifier = context.StateTokenPrincipal.GetClaim(Claims.Private.CodeVerifier);
+ context.TokenRequest.RedirectUri = context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri);
}
return default;
@@ -1521,11 +1532,11 @@ public static partial class OpenIddictClientHandlers
///
/// Contains the logic responsible of sending the token request, if applicable.
///
- public class SendBackchannelRequest : IOpenIddictClientHandler
+ public class SendTokenRequest : IOpenIddictClientHandler
{
private readonly OpenIddictClientService _service;
- public SendBackchannelRequest(OpenIddictClientService service)
+ public SendTokenRequest(OpenIddictClientService service)
=> _service = service;
///
@@ -1533,9 +1544,9 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(AttachBackchannelRequestParameters.Descriptor.Order + 1_000)
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachTokenRequestParameters.Descriptor.Order + 1_000)
.Build();
///
@@ -1555,16 +1566,16 @@ public static partial class OpenIddictClientHandlers
///
/// Contains the logic responsible of rejecting errored token responses.
///
- public class ValidateBackchannelErrorParameters : IOpenIddictClientHandler
+ public class ValidateTokenErrorParameters : IOpenIddictClientHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(SendBackchannelRequest.Descriptor.Order + 1_000)
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(SendTokenRequest.Descriptor.Order + 1_000)
.Build();
///
@@ -1601,9 +1612,9 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
- .SetOrder(ValidateBackchannelErrorParameters.Descriptor.Order + 1_000)
+ .SetOrder(ValidateTokenErrorParameters.Descriptor.Order + 1_000)
.Build();
///
@@ -1628,7 +1639,7 @@ public static partial class OpenIddictClientHandlers
false => null
};
- context.BackchannelRefreshToken = context.ExtractBackchannelRefreshToken switch
+ context.RefreshToken = context.ExtractRefreshToken switch
{
true => context.TokenResponse.RefreshToken,
false => null
@@ -1661,9 +1672,9 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if ((context.RequireBackchannelAccessToken && string.IsNullOrEmpty(context.BackchannelAccessToken)) ||
- (context.RequireBackchannelIdentityToken && string.IsNullOrEmpty(context.BackchannelIdentityToken)) ||
- (context.RequireBackchannelRefreshToken && string.IsNullOrEmpty(context.BackchannelRefreshToken)))
+ if ((context.RequireBackchannelAccessToken && string.IsNullOrEmpty(context.BackchannelAccessToken)) ||
+ (context.RequireBackchannelIdentityToken && string.IsNullOrEmpty(context.BackchannelIdentityToken)) ||
+ (context.RequireRefreshToken && string.IsNullOrEmpty(context.RefreshToken)))
{
context.Reject(
error: Errors.MissingToken,
@@ -1755,7 +1766,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateBackchannelIdentityToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@@ -1892,7 +1903,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateBackchannelIdentityTokenWellknownClaims.Descriptor.Order + 1_000)
.Build();
@@ -1937,7 +1948,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateBackchannelIdentityTokenAudience.Descriptor.Order + 1_000)
.Build();
@@ -1980,7 +1991,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateBackchannelIdentityTokenPresenter.Descriptor.Order + 1_000)
.Build();
@@ -1994,11 +2005,11 @@ public static partial class OpenIddictClientHandlers
}
Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- Debug.Assert(context.FrontchannelStateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+ Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
switch ((
BackchannelIdentityTokenNonce: context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Nonce),
- StateTokenNonce: context.FrontchannelStateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
+ StateTokenNonce: context.StateTokenPrincipal.GetClaim(Claims.Private.Nonce)))
{
// If no nonce if no present in the state token (e.g because the authorization server doesn't
// support OpenID Connect and response_type=code was negotiated), bypass the validation logic.
@@ -2043,7 +2054,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(ValidateBackchannelIdentityTokenNonce.Descriptor.Order + 1_000)
.Build();
@@ -2213,15 +2224,15 @@ public static partial class OpenIddictClientHandlers
}
///
- /// Contains the logic responsible of validating the backchannel refresh token resolved from the context.
+ /// Contains the logic responsible of validating the refresh token resolved from the context.
/// Note: this handler is typically not used for standard-compliant implementations as refresh tokens
/// are supposed to be opaque to clients.
///
- public class ValidateBackchannelRefreshToken : IOpenIddictClientHandler
+ public class ValidateRefreshToken : IOpenIddictClientHandler
{
private readonly IOpenIddictClientDispatcher _dispatcher;
- public ValidateBackchannelRefreshToken(IOpenIddictClientDispatcher dispatcher)
+ public ValidateRefreshToken(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher;
///
@@ -2229,8 +2240,8 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .UseScopedHandler()
+ .AddFilter()
+ .UseScopedHandler()
.SetOrder(ValidateBackchannelAccessToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@@ -2243,15 +2254,15 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
- if (context.BackchannelRefreshTokenPrincipal is not null ||
- string.IsNullOrEmpty(context.BackchannelRefreshToken))
+ if (context.RefreshTokenPrincipal is not null ||
+ string.IsNullOrEmpty(context.RefreshToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
- Token = context.BackchannelRefreshToken,
+ Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeHints.RefreshToken }
};
@@ -2278,7 +2289,455 @@ public static partial class OpenIddictClientHandlers
return;
}
- context.BackchannelRefreshTokenPrincipal = notification.Principal;
+ context.RefreshTokenPrincipal = notification.Principal;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of determining whether a userinfo token should be validated.
+ ///
+ public class EvaluateValidatedUserinfoToken : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .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));
+
+ // Resolve the grant grant and the response type stored in the state token and extract its individual elements.
+ var types = (
+ GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType),
+ ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
+ !.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)
+ .ToImmutableHashSet());
+
+ (context.ExtractUserinfoToken,
+ context.RequireUserinfoToken,
+ context.ValidateUserinfoToken) = types switch
+ {
+ // Information about the authenticated user can be retrieved from the userinfo
+ // endpoint when a backchannel access token is available. In this case, user data
+ // will be returned either as a JSON object or as a signed and/or encrypted
+ // JSON Web Token if the client registration indicates the client supports it.
+ //
+ // By default, OpenIddict doesn't require that userinfo-as-JWT responses be used
+ // but userinfo tokens will be extracted and validated if they are available.
+ (GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet set)
+ when context.StateTokenPrincipal.HasScope(Scopes.OpenId) &&
+ (set.Contains(ResponseTypes.Code) || set.Contains(ResponseTypes.Token))
+ => (true, false, true),
+
+ _ => (false, false, false)
+ };
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of attaching the parameters to the userinfo request, if applicable.
+ ///
+ public class AttachUserinfoRequestParameters : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
+
+ // Ensure the issuer resolved from the configuration matches the expected value.
+ if (configuration.Issuer != context.Issuer)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
+ }
+
+ var token = context switch
+ {
+ // Note: the backchannel access token (retrieved from the token endpoint) is always preferred to
+ // the frontchannel access token if available, as it may grant a greater access to user's resources.
+ { ExtractBackchannelAccessToken: true, BackchannelAccessToken: { Length: > 0 } value }
+ // If the userinfo endpoint is not available, skip the request.
+ when configuration.UserinfoEndpoint is not null => value,
+
+ // If the backchannel access token is not available, try to use the frontchannel access token.
+ { ExtractFrontchannelAccessToken: true, FrontchannelAccessToken: { Length: > 0 } value }
+ // If the userinfo endpoint is not available, skip the request.
+ when configuration.UserinfoEndpoint is not null => value,
+
+ // Otherwise, skip the userinfo request.
+ _ => null
+ };
+
+ if (string.IsNullOrEmpty(token))
+ {
+ return;
+ }
+
+ // Attach a new request instance if none was created already.
+ context.UserinfoRequest ??= new OpenIddictRequest();
+
+ // Attach the access token.
+ context.UserinfoRequest.AccessToken = token;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of sending the userinfo request, if applicable.
+ ///
+ public class SendUserinfoRequest : IOpenIddictClientHandler
+ {
+ private readonly OpenIddictClientService _service;
+
+ public SendUserinfoRequest(OpenIddictClientService service)
+ => _service = service;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(AttachUserinfoRequestParameters.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.UserinfoRequest is not null, SR.GetResourceString(SR.ID4008));
+
+ // Note: userinfo responses can be of two types:
+ // - application/json responses containing a JSON object listing the user claims as-is.
+ // - 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);
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of rejecting errored userinfo responses.
+ ///
+ public class ValidateUserinfoErrorParameters : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(SendUserinfoRequest.Descriptor.Order + 1_000)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.UserinfoResponse is not null, SR.GetResourceString(SR.ID4007));
+
+ if (!string.IsNullOrEmpty(context.UserinfoResponse.Error))
+ {
+ context.Reject(
+ error: context.UserinfoResponse.Error,
+ description: context.UserinfoResponse.ErrorDescription,
+ uri: context.UserinfoResponse.ErrorUri);
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of rejecting authentication demands that lack the required userinfo token.
+ ///
+ public class ValidateRequiredUserinfoToken : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateUserinfoErrorParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.RequireUserinfoToken && string.IsNullOrEmpty(context.UserinfoToken))
+ {
+ context.Reject(
+ error: Errors.MissingToken,
+ description: SR.GetResourceString(SR.ID2000),
+ uri: SR.FormatID8000(SR.ID2000));
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of validating the userinfo token resolved from the context.
+ ///
+ public class ValidateUserinfoToken : IOpenIddictClientHandler
+ {
+ private readonly IOpenIddictClientDispatcher _dispatcher;
+
+ public ValidateUserinfoToken(IOpenIddictClientDispatcher dispatcher)
+ => _dispatcher = dispatcher;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateRequiredUserinfoToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.UserinfoTokenPrincipal is not null || string.IsNullOrEmpty(context.UserinfoToken))
+ {
+ return;
+ }
+
+ var notification = new ValidateTokenContext(context.Transaction)
+ {
+ Token = context.UserinfoToken,
+ ValidTokenTypes = { TokenTypeHints.UserinfoToken }
+ };
+
+ await _dispatcher.DispatchAsync(notification);
+
+ if (notification.IsRequestHandled)
+ {
+ context.HandleRequest();
+ return;
+ }
+
+ else if (notification.IsRequestSkipped)
+ {
+ context.SkipRequest();
+ return;
+ }
+
+ else if (notification.IsRejected)
+ {
+ context.Reject(
+ error: notification.Error ?? Errors.InvalidRequest,
+ description: notification.ErrorDescription,
+ uri: notification.ErrorUri);
+ return;
+ }
+
+ context.UserinfoTokenPrincipal = notification.Principal;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of validating the well-known claims contained in the userinfo token.
+ ///
+ public class ValidateUserinfoTokenWellknownClaims : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateUserinfoToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .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));
+ Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints
+ // but must also support non-standard implementations, that are common with OAuth 2.0-only servers.
+ //
+ // As such, protocol requirements are only enforced if an OpenID Connect request was initially sent.
+ if (context.StateTokenPrincipal.HasScope(Scopes.OpenId))
+ {
+ foreach (var group in context.UserinfoTokenPrincipal.Claims
+ .GroupBy(claim => claim.Type)
+ .ToDictionary(group => group.Key, group => group.ToList()))
+ {
+ if (ValidateClaimGroup(group))
+ {
+ continue;
+ }
+
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2131(group.Key),
+ uri: SR.FormatID8000(SR.ID2131));
+
+ return default;
+ }
+ }
+
+ return default;
+
+ static bool ValidateClaimGroup(KeyValuePair> claims) => claims switch
+ {
+ // The following JWT claims MUST be represented as unique strings.
+ {
+ Key: Claims.Subject,
+ Value: List values
+ } => values.Count is 1 && values[0].ValueType is ClaimValueTypes.String,
+
+ // Claims that are not in the well-known list can be of any type.
+ _ => true
+ };
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of validating the subject claim contained in the userinfo token.
+ ///
+ public class ValidateUserinfoTokenWellknownSubject : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ValidateUserinfoTokenWellknownClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .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));
+ Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints
+ // but must also support non-standard implementations, that are common with OAuth 2.0-only servers.
+ //
+ // As such, protocol requirements are only enforced if an OpenID Connect request was initially sent.
+ if (context.StateTokenPrincipal.HasScope(Scopes.OpenId))
+ {
+ // Standard OpenID Connect userinfo responses/tokens MUST contain a "sub" claim. For more
+ // information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse.
+ if (!context.UserinfoTokenPrincipal.HasClaim(Claims.Subject))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2132(Claims.Subject),
+ uri: SR.FormatID8000(SR.ID2132));
+
+ return default;
+ }
+
+ // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value
+ // returned in the frontchannel identity token, if one was returned. For more information,
+ // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse.
+ if (context.FrontchannelIdentityTokenPrincipal is not null && !string.Equals(
+ context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Subject),
+ context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2133(Claims.Subject),
+ uri: SR.FormatID8000(SR.ID2133));
+
+ return default;
+ }
+
+ // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value
+ // returned in the frontchannel identity token, if one was returned. For more information,
+ // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse.
+ if (context.BackchannelIdentityTokenPrincipal is not null && !string.Equals(
+ context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Subject),
+ context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal))
+ {
+ context.Reject(
+ error: Errors.InvalidRequest,
+ description: SR.FormatID2133(Claims.Subject),
+ uri: SR.FormatID8000(SR.ID2133));
+
+ return default;
+ }
+ }
+
+ return default;
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientRegistration.cs b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
index 4dfa7325..05e7cf99 100644
--- a/src/OpenIddict.Client/OpenIddictClientRegistration.cs
+++ b/src/OpenIddict.Client/OpenIddictClientRegistration.cs
@@ -99,6 +99,7 @@ public class OpenIddictClientRegistration
///
public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters
{
+ AuthenticationType = TokenValidationParameters.DefaultAuthenticationType,
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,
diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs
index 0383dd44..8776a13a 100644
--- a/src/OpenIddict.Client/OpenIddictClientService.cs
+++ b/src/OpenIddict.Client/OpenIddictClientService.cs
@@ -5,6 +5,7 @@
*/
using System.Diagnostics;
+using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
@@ -461,4 +462,157 @@ public class OpenIddictClientService
}
}
}
+
+ ///
+ /// Sends the userinfo request and retrieves the corresponding response.
+ ///
+ /// The client registration.
+ /// 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)
+ {
+ if (registration is null)
+ {
+ throw new ArgumentNullException(nameof(registration));
+ }
+
+ var configuration = await 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));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Note: this service is registered as a singleton service. As such, it cannot
+ // directly depend on scoped services like the validation provider. To work around
+ // this limitation, a scope is manually created for each method to this service.
+ var scope = _provider.CreateScope();
+
+ // Note: a try/finally block is deliberately used here to ensure the service scope
+ // can be disposed of asynchronously if it implements IAsyncDisposable.
+ try
+ {
+ var dispatcher = scope.ServiceProvider.GetRequiredService();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ var transaction = await factory.CreateTransactionAsync();
+
+ request = await PrepareUserinfoRequestAsync();
+ request = await ApplyUserinfoRequestAsync();
+
+ var (response, token) = await ExtractUserinfoResponseAsync();
+
+ return await HandleUserinfoResponseAsync();
+
+ async ValueTask PrepareUserinfoRequestAsync()
+ {
+ var context = new PrepareUserinfoRequestContext(transaction)
+ {
+ Address = configuration.UserinfoEndpoint,
+ Issuer = registration.Issuer,
+ Registration = registration,
+ Request = request
+ };
+
+ await dispatcher.DispatchAsync(context);
+
+ if (context.IsRejected)
+ {
+ throw new OpenIddictExceptions.GenericException(
+ SR.FormatID0152(context.Error, context.ErrorDescription, context.ErrorUri),
+ context.Error, context.ErrorDescription, context.ErrorUri);
+ }
+
+ return context.Request;
+ }
+
+ async ValueTask ApplyUserinfoRequestAsync()
+ {
+ var context = new ApplyUserinfoRequestContext(transaction)
+ {
+ Address = configuration.UserinfoEndpoint,
+ Issuer = registration.Issuer,
+ Registration = registration,
+ Request = request
+ };
+
+ await dispatcher.DispatchAsync(context);
+
+ if (context.IsRejected)
+ {
+ throw new OpenIddictExceptions.GenericException(
+ SR.FormatID0153(context.Error, context.ErrorDescription, context.ErrorUri),
+ context.Error, context.ErrorDescription, context.ErrorUri);
+ }
+
+ return context.Request;
+ }
+
+ async ValueTask<(OpenIddictResponse, string?)> ExtractUserinfoResponseAsync()
+ {
+ var context = new ExtractUserinfoResponseContext(transaction)
+ {
+ Address = configuration.UserinfoEndpoint,
+ Issuer = registration.Issuer,
+ Registration = registration,
+ Request = request
+ };
+
+ await dispatcher.DispatchAsync(context);
+
+ if (context.IsRejected)
+ {
+ throw new OpenIddictExceptions.GenericException(
+ SR.FormatID0154(context.Error, context.ErrorDescription, context.ErrorUri),
+ context.Error, context.ErrorDescription, context.ErrorUri);
+ }
+
+ Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007));
+
+ return (context.Response, context.UserinfoToken);
+ }
+
+ async ValueTask<(OpenIddictResponse, (ClaimsPrincipal?, string?))> HandleUserinfoResponseAsync()
+ {
+ var context = new HandleUserinfoResponseContext(transaction)
+ {
+ Address = configuration.UserinfoEndpoint,
+ Issuer = registration.Issuer,
+ Registration = registration,
+ Request = request,
+ Response = response,
+ UserinfoToken = token
+ };
+
+ await dispatcher.DispatchAsync(context);
+
+ if (context.IsRejected)
+ {
+ throw new OpenIddictExceptions.GenericException(
+ SR.FormatID0155(context.Error, context.ErrorDescription, context.ErrorUri),
+ context.Error, context.ErrorDescription, context.ErrorUri);
+ }
+
+ return (context.Response, (context.Principal, context.UserinfoToken));
+ }
+ }
+
+ finally
+ {
+ if (scope is IAsyncDisposable disposable)
+ {
+ await disposable.DisposeAsync();
+ }
+
+ else
+ {
+ scope.Dispose();
+ }
+ }
+ }
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
index 10cc5620..fcb59933 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
@@ -200,8 +200,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler true,