Browse Source

Add userinfo support

pull/1395/head
Kévin Chalet 4 years ago
parent
commit
fdf34448f2
  1. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  2. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  3. 9
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  4. 5
      src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs
  5. 14
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
  6. 152
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  7. 8
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  8. 18
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConstants.cs
  9. 136
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.Userinfo.cs
  10. 3
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  11. 140
      src/OpenIddict.Client/OpenIddictClientEvents.Userinfo.cs
  12. 158
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  13. 19
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  14. 176
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  15. 4
      src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
  16. 164
      src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
  17. 33
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  18. 214
      src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs
  19. 733
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  20. 1
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  21. 154
      src/OpenIddict.Client/OpenIddictClientService.cs
  22. 38
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  23. 3
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  24. 2
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

2
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.

1
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";
}

9
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1578,6 +1578,15 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa
<data name="ID2130" xml:space="preserve">
<value>The specified state token is not valid in this context.</value>
</data>
<data name="ID2131" xml:space="preserve">
<value>The '{0}' claim extracted from the specified userinfo response/token is malformed or isn't of the expected type.</value>
</data>
<data name="ID2132" xml:space="preserve">
<value>The mandatory '{0}' claim cannot be found in the specified userinfo response/token.</value>
</data>
<data name="ID2133" xml:space="preserve">
<value>The '{0}' claim returned in the specified userinfo response/token doesn't match the expected value.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>

5
src/OpenIddict.Abstractions/Primitives/OpenIddictConfiguration.cs

@ -90,4 +90,9 @@ public class OpenIddictConfiguration
/// Gets the client authentication methods supported by the token endpoint.
/// </summary>
public HashSet<string> TokenEndpointAuthMethodsSupported { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the address of the userinfo endpoint.
/// </summary>
public Uri? UserinfoEndpoint { get; set; }
}

14
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";
}
}

152
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<OpenIddic
// are attached to the authentication properties bag so they can be accessed from user code.
var principal = context.EndpointType switch
{
// Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow ASP.NET Core
// to return a "successful" authentication result for these delegation-only scenarios.
OpenIddictClientEndpointType.Redirection =>
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<OpenIddic
// Restore the return URL using the "target_link_uri" that was stored
// in the state token when the challenge operation started, if available.
RedirectUri = context.FrontchannelStateTokenPrincipal?.GetClaim(Claims.TargetLinkUri)
RedirectUri = context.StateTokenPrincipal?.GetClaim(Claims.TargetLinkUri)
};
List<AuthenticationToken>? tokens = null;
@ -175,6 +172,16 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
// Attach the tokens to allow any ASP.NET Core component (e.g a controller)
// to retrieve them (e.g to make an API request to another application).
if (!string.IsNullOrEmpty(context.AuthorizationCode))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.AuthorizationCode,
Value = context.AuthorizationCode
});
}
if (!string.IsNullOrEmpty(context.BackchannelAccessToken))
{
tokens ??= new(capacity: 1);
@ -183,8 +190,6 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.BackchannelAccessToken,
Value = context.BackchannelAccessToken
});
properties.SetParameter(Properties.BackchannelAccessTokenPrincipal, context.BackchannelAccessTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.BackchannelIdentityToken))
@ -195,68 +200,56 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.BackchannelIdentityToken,
Value = context.BackchannelIdentityToken
});
properties.SetParameter(Properties.BackchannelIdentityTokenPrincipal, context.BackchannelIdentityTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.BackchannelRefreshToken))
if (!string.IsNullOrEmpty(context.FrontchannelAccessToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.BackchannelRefreshToken,
Value = context.BackchannelRefreshToken
Name = Tokens.FrontchannelAccessToken,
Value = context.FrontchannelAccessToken
});
properties.SetParameter(Properties.BackchannelRefreshTokenPrincipal, context.BackchannelRefreshTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.FrontchannelAccessToken))
if (!string.IsNullOrEmpty(context.FrontchannelIdentityToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.FrontchannelAccessToken,
Value = context.FrontchannelAccessToken
Name = Tokens.FrontchannelIdentityToken,
Value = context.FrontchannelIdentityToken
});
properties.SetParameter(Properties.FrontchannelAccessTokenPrincipal, context.FrontchannelAccessTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.FrontchannelAuthorizationCode))
if (!string.IsNullOrEmpty(context.RefreshToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.FrontchannelAuthorizationCode,
Value = context.FrontchannelAuthorizationCode
Name = Tokens.RefreshToken,
Value = context.RefreshToken
});
properties.SetParameter(Properties.FrontchannelAuthorizationCodePrincipal, context.FrontchannelAuthorizationCodePrincipal);
}
if (!string.IsNullOrEmpty(context.FrontchannelIdentityToken))
if (!string.IsNullOrEmpty(context.StateToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.FrontchannelIdentityToken,
Value = context.FrontchannelIdentityToken
Name = Tokens.StateToken,
Value = context.StateToken
});
properties.SetParameter(Properties.FrontchannelIdentityTokenPrincipal, context.FrontchannelIdentityTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.FrontchannelStateToken))
if (!string.IsNullOrEmpty(context.UserinfoToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.FrontchannelStateToken,
Value = context.FrontchannelStateToken
Name = Tokens.UserinfoToken,
Value = context.UserinfoToken
});
properties.SetParameter(Properties.FrontchannelStateTokenPrincipal, context.FrontchannelStateTokenPrincipal);
}
if (tokens is { Count: > 0 })
@ -264,8 +257,87 @@ public class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<OpenIddic
properties.StoreTokens(tokens);
}
if (context.AuthorizationCodePrincipal is not null)
{
properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
}
if (context.BackchannelAccessTokenPrincipal is not null)
{
properties.SetParameter(Properties.BackchannelAccessTokenPrincipal, context.BackchannelAccessTokenPrincipal);
}
if (context.BackchannelIdentityTokenPrincipal is not null)
{
properties.SetParameter(Properties.BackchannelIdentityTokenPrincipal, context.BackchannelIdentityTokenPrincipal);
}
if (context.FrontchannelAccessTokenPrincipal is not null)
{
properties.SetParameter(Properties.FrontchannelAccessTokenPrincipal, context.FrontchannelAccessTokenPrincipal);
}
if (context.FrontchannelIdentityTokenPrincipal is not null)
{
properties.SetParameter(Properties.FrontchannelIdentityTokenPrincipal, context.FrontchannelIdentityTokenPrincipal);
}
if (context.RefreshTokenPrincipal is not null)
{
properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
}
if (context.StateTokenPrincipal is not null)
{
properties.SetParameter(Properties.StateTokenPrincipal, context.StateTokenPrincipal);
}
if (context.UserinfoTokenPrincipal is not null)
{
properties.SetParameter(Properties.UserinfoTokenPrincipal, context.UserinfoTokenPrincipal);
}
return AuthenticateResult.Success(new AuthenticationTicket(principal, properties,
OpenIddictClientAspNetCoreDefaults.AuthenticationScheme));
static ClaimsPrincipal CreatePrincipal(params ClaimsPrincipal?[] principals)
{
// Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow ASP.NET Core
// to return a "successful" authentication result for these delegation-only scenarios.
if (!principals.Any(principal => 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);
}
}
}

8
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -363,9 +363,9 @@ public static partial class OpenIddictClientAspNetCoreHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpRequest>()
.AddFilter<RequireFrontchannelStateTokenValidated>()
.AddFilter<RequireStateTokenValidated>()
.UseSingletonHandler<ValidateCorrelationCookie>()
.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;

18
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;
/// <summary>
/// Exposes common constants used by the OpenIddict System.Net.Http integration.
/// </summary>
public static class OpenIddictClientSystemNetHttpConstants
{
public static class ContentTypes
{
public const string JsonWebToken = "application/jwt";
}
}

136
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<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo request processing:
*/
PrepareGetHttpRequest<PrepareUserinfoRequestContext>.Descriptor,
AttachBearerAccessToken.Descriptor,
AttachFormParameters<PrepareUserinfoRequestContext>.Descriptor,
SendHttpRequest<ApplyUserinfoRequestContext>.Descriptor,
DisposeHttpRequest<ApplyUserinfoRequestContext>.Descriptor,
/*
* Userinfo response processing:
*/
ExtractUserinfoHttpResponse.Descriptor,
DisposeHttpResponse<ExtractUserinfoResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of attaching the access token to the HTTP Authorization header.
/// </summary>
public class AttachBearerAccessToken : IOpenIddictClientHandler<PrepareUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareUserinfoRequestContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<AttachBearerAccessToken>()
.SetOrder(AttachFormParameters<PrepareUserinfoRequestContext>.Descriptor.Order - 1000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the response from the userinfo response.
/// </summary>
public class ExtractUserinfoHttpResponse : IOpenIddictClientHandler<ExtractUserinfoResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ExtractUserinfoResponseContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<ExtractUserinfoHttpResponse>()
.SetOrder(DisposeHttpResponse<ExtractUserinfoResponseContext>.Descriptor.Order - 50_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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<OpenIddictResponse>();
}
}
}
}
}

3
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -19,7 +19,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; }
= ImmutableArray.Create<OpenIddictClientHandlerDescriptor>()
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers);
.AddRange(Exchange.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
/// <summary>
/// Contains the logic responsible of preparing an HTTP GET request message.

140
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
{
/// <summary>
/// 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.
/// </summary>
public class PrepareUserinfoRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="PrepareUserinfoRequestContext"/> class.
/// </summary>
public PrepareUserinfoRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each request to the userinfo endpoint
/// to send the userinfo request to the remote authorization server.
/// </summary>
public class ApplyUserinfoRequestContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyUserinfoRequestContext"/> class.
/// </summary>
public ApplyUserinfoRequestContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
}
/// <summary>
/// Represents an event called for each userinfo response
/// to extract the response parameters from the server response.
/// </summary>
public class ExtractUserinfoResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractUserinfoResponseContext"/> class.
/// </summary>
public ExtractUserinfoResponseContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response, or <c>null</c> if it wasn't extracted yet.
/// </summary>
public OpenIddictResponse? Response
{
get => Transaction.Response;
set => Transaction.Response = value;
}
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
public string? UserinfoToken { get; set; }
}
/// <summary>
/// Represents an event called for each userinfo response.
/// </summary>
public class HandleUserinfoResponseContext : BaseExternalContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleUserinfoResponseContext"/> class.
/// </summary>
public HandleUserinfoResponseContext(OpenIddictClientTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the request.
/// </summary>
public OpenIddictRequest Request
{
get => Transaction.Request!;
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the response.
/// </summary>
public OpenIddictResponse Response
{
get => Transaction.Response!;
set => Transaction.Response = value;
}
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
public string? UserinfoToken { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims resolved from the userinfo response.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
}
}

158
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -284,6 +284,14 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
/// <summary>
/// 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.
/// </summary>
public bool ExtractAuthorizationCode { get; set; }
/// <summary>
/// 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; }
/// <summary>
/// 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.
/// </summary>
public bool ExtractBackchannelRefreshToken { get; set; }
public bool ExtractFrontchannelAccessToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ExtractFrontchannelAccessToken { get; set; }
public bool ExtractFrontchannelIdentityToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ExtractFrontchannelAuthorizationCode { get; set; }
public bool ExtractRefreshToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ExtractFrontchannelIdentityToken { get; set; }
public bool ExtractStateToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ExtractFrontchannelStateToken { get; set; }
public bool ExtractUserinfoToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a backchannel access
@ -357,12 +373,12 @@ public static partial class OpenIddictClientEvents
public bool RequireBackchannelIdentityToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireBackchannelRefreshToken { get; set; }
public bool RequireFrontchannelAccessToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireFrontchannelAccessToken { get; set; }
public bool RequireFrontchannelIdentityToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireFrontchannelAuthorizationCode { get; set; }
public bool RequireRefreshToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireFrontchannelIdentityToken { get; set; }
public bool RequireStateToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool RequireFrontchannelStateToken { get; set; }
public bool RequireUserinfoToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the backchannel access
@ -413,44 +437,49 @@ public static partial class OpenIddictClientEvents
public bool ValidateBackchannelIdentityToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateBackchannelRefreshToken { get; set; }
public bool ValidateFrontchannelAccessToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateFrontchannelAccessToken { get; set; }
public bool ValidateFrontchannelIdentityToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateFrontchannelAuthorizationCode { get; set; }
public bool ValidateRefreshToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateFrontchannelIdentityToken { get; set; }
public bool ValidateStateToken { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool ValidateFrontchannelStateToken { get; set; }
public bool ValidateUserinfoToken { get; set; }
/// <summary>
/// Gets or sets the authorization code to validate, if applicable.
/// </summary>
public string? AuthorizationCode { get; set; }
/// <summary>
/// Gets or sets the backchannel access token to validate, if applicable.
@ -462,30 +491,35 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? BackchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets the backchannel refresh token to validate, if applicable.
/// </summary>
public string? BackchannelRefreshToken { get; set; }
/// <summary>
/// Gets or sets the frontchannel access token to validate, if applicable.
/// </summary>
public string? FrontchannelAccessToken { get; set; }
/// <summary>
/// Gets or sets the frontchannel authorization code to validate, if applicable.
/// Gets or sets the frontchannel identity token to validate, if applicable.
/// </summary>
public string? FrontchannelAuthorizationCode { get; set; }
public string? FrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets the frontchannel identity token to validate, if applicable.
/// Gets or sets the refresh token to validate, if applicable.
/// </summary>
public string? FrontchannelIdentityToken { get; set; }
public string? RefreshToken { get; set; }
/// <summary>
/// Gets or sets the frontchannel state token to validate, if applicable.
/// </summary>
public string? FrontchannelStateToken { get; set; }
public string? StateToken { get; set; }
/// <summary>
/// Gets or sets the userinfo token to validate, if applicable.
/// </summary>
public string? UserinfoToken { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the authorization code, if applicable.
/// </summary>
public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the backchannel access token, if applicable.
@ -497,11 +531,6 @@ public static partial class OpenIddictClientEvents
/// </summary>
public ClaimsPrincipal? BackchannelIdentityTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the backchannel refresh token, if applicable.
/// </summary>
public ClaimsPrincipal? BackchannelRefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the frontchannel access token, if applicable.
/// </summary>
@ -513,14 +542,19 @@ public static partial class OpenIddictClientEvents
public ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; set; }
/// <summary>
/// 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.
/// </summary>
public ClaimsPrincipal? FrontchannelAuthorizationCodePrincipal { get; set; }
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
/// <summary>
/// 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.
/// </summary>
public ClaimsPrincipal? FrontchannelStateTokenPrincipal { get; set; }
public ClaimsPrincipal? StateTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token, if applicable.
/// </summary>
public ClaimsPrincipal? UserinfoTokenPrincipal { get; set; }
/// <summary>
/// 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.
/// </summary>
public OpenIddictResponse? TokenResponse { get; set; }
/// <summary>
/// Gets or sets the request sent to the userinfo endpoint, if applicable.
/// </summary>
public OpenIddictRequest? UserinfoRequest { get; set; }
/// <summary>
/// Gets or sets the response returned by the userinfo endpoint, if applicable.
/// </summary>
public OpenIddictResponse? UserinfoResponse { get; set; }
}
/// <summary>

19
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -36,19 +36,26 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<OpenIddictClientService>();
// Register the built-in filters used by the default OpenIddict client event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationCodeExtracted>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeOrImplicitGrantType>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireBackchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelRequest>();
builder.Services.TryAddSingleton<RequireBackchannelResponse>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireFrontchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelAuthorizationCodeExtracted>();
builder.Services.TryAddSingleton<RequireFrontchannelAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelStateTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireStateTokenPrincipal>();
builder.Services.TryAddSingleton<RequireStateTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenRequest>();
builder.Services.TryAddSingleton<RequireTokenResponse>();
builder.Services.TryAddSingleton<RequireUserinfoRequest>();
builder.Services.TryAddSingleton<RequireUserinfoResponse>();
builder.Services.TryAddSingleton<RequireUserinfoTokenExtracted>();
builder.Services.TryAddSingleton<RequireUserinfoTokenPrincipal>();
// 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.

176
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -11,6 +11,22 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is extracted.
/// </summary>
public class RequireAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ExtractAuthorizationCode);
}
}
/// <summary>
/// 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
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is validated.
/// </summary>
public class RequireAuthorizationCodeValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateAuthorizationCode);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no backchannel access token is validated.
/// </summary>
@ -44,6 +76,22 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no backchannel identity token principal is available.
/// </summary>
public class RequireBackchannelIdentityTokenPrincipal : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.BackchannelIdentityTokenPrincipal is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no backchannel identity token is validated.
/// </summary>
@ -61,9 +109,9 @@ public static class OpenIddictClientHandlerFilters
}
/// <summary>
/// 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.
/// </summary>
public class RequireBackchannelRefreshTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireFrontchannelAccessTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -72,14 +120,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateBackchannelRefreshToken);
return new ValueTask<bool>(context.ValidateFrontchannelAccessToken);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireBackchannelRequest : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireFrontchannelIdentityTokenPrincipal : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -88,14 +136,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.TokenRequest is not null);
return new ValueTask<bool>(context.FrontchannelIdentityTokenPrincipal is not null);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireBackchannelResponse : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireFrontchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -104,14 +152,30 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.TokenResponse is not null);
return new ValueTask<bool>(context.ValidateFrontchannelIdentityToken);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireFrontchannelAccessTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.EndpointType == OpenIddictClientEndpointType.Redirection);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no refresh token is validated.
/// </summary>
public class RequireRefreshTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -120,14 +184,30 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateFrontchannelAccessToken);
return new ValueTask<bool>(context.ValidateRefreshToken);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireFrontchannelAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.GenerateStateToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no state token principal is available.
/// </summary>
public class RequireStateTokenPrincipal : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -136,14 +216,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ExtractFrontchannelAuthorizationCode);
return new ValueTask<bool>(context.StateTokenPrincipal is not null);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireFrontchannelAuthorizationCodeValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireStateTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -152,14 +232,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateFrontchannelAuthorizationCode);
return new ValueTask<bool>(context.ValidateStateToken);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireFrontchannelIdentityTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireTokenRequest : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -168,14 +248,14 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateFrontchannelIdentityToken);
return new ValueTask<bool>(context.TokenRequest is not null);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireFrontchannelStateTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
public class RequireTokenResponse : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
@ -184,39 +264,71 @@ public static class OpenIddictClientHandlerFilters
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ValidateFrontchannelStateToken);
return new ValueTask<bool>(context.TokenResponse is not null);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireRedirectionRequest : IOpenIddictClientHandlerFilter<BaseContext>
public class RequireUserinfoRequest : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(BaseContext context)
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.EndpointType == OpenIddictClientEndpointType.Redirection);
return new ValueTask<bool>(context.UserinfoRequest is not null);
}
}
/// <summary>
/// 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.
/// </summary>
public class RequireStateTokenGenerated : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
public class RequireUserinfoResponse : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.GenerateStateToken);
return new ValueTask<bool>(context.UserinfoResponse is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no userinfo token is extracted.
/// </summary>
public class RequireUserinfoTokenExtracted : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ExtractUserinfoToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no userinfo token principal is available.
/// </summary>
public class RequireUserinfoTokenPrincipal : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.UserinfoTokenPrincipal is not null);
}
}
}

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

164
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
}
}
/// <summary>
/// Contains the logic responsible of extracting the authorization endpoint address from the discovery document.
/// </summary>
public class ExtractAuthorizationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractAuthorizationEndpoint>()
.SetOrder(ValidateIssuer.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the JWKS endpoint address from the discovery document.
/// </summary>
@ -99,7 +152,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractCryptographyEndpoint>()
.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
}
/// <summary>
/// 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.
/// </summary>
public class ExtractTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
public class ExtractUserinfoEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractTokenEndpoint>()
.UseSingletonHandler<ExtractUserinfoEndpoint>()
.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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the authorization endpoint address from the discovery document.
/// </summary>
public class ExtractAuthorizationEndpoint : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractAuthorizationEndpoint>()
.SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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
}
}
/// <summary>
/// Contains the logic responsible of extracting the authentication methods
/// supported by the token endpoint from the discovery document.
/// </summary>
public class ExtractTokenEndpointClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<ExtractTokenEndpoint>()
.SetOrder(ExtractIssuerParameterRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible of extracting the signing keys from the JWKS document.
/// </summary>

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

214
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<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo response handling:
*/
HandleErrorResponse<HandleUserinfoResponseContext>.Descriptor,
ValidateWellKnownClaims.Descriptor,
PopulateClaims.Descriptor);
/// <summary>
/// Contains the logic responsible of validating the well-known parameters contained in the userinfo response.
/// </summary>
public class ValidateWellKnownClaims : IOpenIddictClientHandler<HandleUserinfoResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleUserinfoResponseContext>()
.UseSingletonHandler<ValidateWellKnownClaims>()
.SetOrder(int.MinValue + 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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
};
}
}
/// <summary>
/// Contains the logic responsible of extracting the claims from the introspection response.
/// </summary>
public class PopulateClaims : IOpenIddictClientHandler<HandleUserinfoResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleUserinfoResponseContext>()
.UseSingletonHandler<PopulateClaims>()
.SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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
};
}
}
}
}

733
src/OpenIddict.Client/OpenIddictClientHandlers.cs

File diff suppressed because it is too large

1
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -99,6 +99,7 @@ public class OpenIddictClientRegistration
/// </summary>
public TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters
{
AuthenticationType = TokenValidationParameters.DefaultAuthenticationType,
ClockSkew = TimeSpan.Zero,
NameClaimType = Claims.Name,
RoleClaimType = Claims.Role,

154
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
}
}
}
/// <summary>
/// Sends the userinfo request and retrieves the corresponding response.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="request">The userinfo request.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the userinfo response or the userinfo token.</returns>
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
request = await PrepareUserinfoRequestAsync();
request = await ApplyUserinfoRequestAsync();
var (response, token) = await ExtractUserinfoResponseAsync();
return await HandleUserinfoResponseAsync();
async ValueTask<OpenIddictRequest> 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<OpenIddictRequest> 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();
}
}
}
}

38
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -200,8 +200,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.AccessToken,
Value = context.AccessToken
});
properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.AuthorizationCode))
@ -212,8 +210,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.AuthorizationCode,
Value = context.AuthorizationCode
});
properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
}
if (!string.IsNullOrEmpty(context.DeviceCode))
@ -224,8 +220,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.DeviceCode,
Value = context.DeviceCode
});
properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal);
}
if (!string.IsNullOrEmpty(context.IdentityToken))
@ -236,8 +230,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.IdentityToken,
Value = context.IdentityToken
});
properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.RefreshToken))
@ -248,8 +240,6 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.RefreshToken,
Value = context.RefreshToken
});
properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
}
if (!string.IsNullOrEmpty(context.UserCode))
@ -260,7 +250,35 @@ public class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<OpenIddic
Name = Tokens.UserCode,
Value = context.UserCode
});
}
if (context.AccessTokenPrincipal is not null)
{
properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
}
if (context.AuthorizationCodePrincipal is not null)
{
properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
}
if (context.DeviceCodePrincipal is not null)
{
properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal);
}
if (context.IdentityTokenPrincipal is not null)
{
properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal);
}
if (context.RefreshTokenPrincipal is not null)
{
properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
}
if (context.UserCodePrincipal is not null)
{
properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal);
}

3
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs

@ -177,7 +177,10 @@ public class OpenIddictValidationAspNetCoreHandler : AuthenticationHandler<OpenI
Name = Tokens.AccessToken,
Value = context.AccessToken
});
}
if (context.AccessTokenPrincipal is not null)
{
properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
}

2
src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs

@ -316,7 +316,7 @@ public static partial class OpenIddictValidationHandlers
// Note: by default, OpenIddict only allows access/refresh tokens to be
// introspected but additional types can be added using the events model.
TokenTypeHints.AccessToken or TokenTypeHints.AuthorizationCode or
TokenTypeHints.IdToken or TokenTypeHints.RefreshToken or
TokenTypeHints.IdToken or TokenTypeHints.RefreshToken or
TokenTypeHints.UserCode
=> true,

Loading…
Cancel
Save