diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs
index 5745de46..433a1e7b 100644
--- a/samples/Mvc.Server/Startup.cs
+++ b/samples/Mvc.Server/Startup.cs
@@ -45,6 +45,7 @@ namespace Mvc.Server
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
+ options.ClaimsIdentity.EmailClaimType = Claims.Email;
});
// OpenIddict offers native integration with Quartz.NET to perform scheduled tasks
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index e48137d8..df816eae 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -341,6 +341,15 @@ namespace OpenIddict.Abstractions
/// true if the token has the specified type, false otherwise.
ValueTask HasTypeAsync(object token, string type, CancellationToken cancellationToken = default);
+ ///
+ /// Determines whether a given token has any of the specified types.
+ ///
+ /// The token.
+ /// The expected types.
+ /// The that can be used to abort the operation.
+ /// true if the token has any of the specified types, false otherwise.
+ ValueTask HasTypeAsync(object token, ImmutableArray types, CancellationToken cancellationToken = default);
+
///
/// Executes the specified query and returns all the corresponding elements.
///
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index d618a837..781766e3 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -134,7 +134,7 @@ To validate tokens received by custom API endpoints, the OpenIddict validation h
When implementing custom token deserialization, a 'oi_tkn_typ' claim containing the type of the token being processed must be added to the security principal.
- The type of token associated with the deserialized principal ({0}) doesn't match the expected token type ({1}).
+ The type of token associated with the deserialized principal ({0}) doesn't match one of the expected token types ({1}).
A challenge response cannot be returned from this endpoint.
@@ -447,10 +447,10 @@ To use key rollover, register both the new certificate and the old one in the cr
No custom verification request validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateVerificationRequestContext>' must be implemented to validate verification requests (e.g to ensure the user_code is valid).
- No custom verification authentication handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ProcessAuthenticationContext>' must be implemented to handle device and user codes (e.g by retrieving them from a database).
+ No custom token validation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ValidateTokenContext>' must be implemented to handle device and user codes (e.g by retrieving them from a database).
- No custom verification sign-in handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<ProcessSignInContext>' must be implemented to handle device and user codes and store them in a database, if applicable.
+ No custom token generation handler was found. When enabling the degraded mode, a custom 'IOpenIddictServerHandler<GenerateTokenContext>' must be implemented to handle device and user codes (e.g by storing them in a database).
The event handler of type '{0}' couldn't be resolved.
@@ -1476,6 +1476,9 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
The specified authorization type is not supported by the default token manager.
+
+ The token usage returned by the authorization server is not supported.
+
The '{0}' parameter shouldn't be null or empty at this point.
@@ -1552,65 +1555,18 @@ To register the OpenIddict core services, reference the 'OpenIddict.Core' packag
'{Claim}' was excluded from the identity token claims.
- The token entry for access token '{Identifier}' was successfully created.
+ The token entry for '{Type}' token '{Identifier}' was successfully created.
- A new access token was successfully created: {Payload}.
+ A new '{Type}' token was successfully created: {Payload}.
The principal used to create the token contained the following claims: {Claims}.
- The token entry for access token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
-
-
- The token entry for authorization code '{Identifier}' was successfully created.
-
-
- A new authorization code was successfully created: {Payload}.
-The principal used to create the token contained the following claims: {Claims}.
-
-
- The token entry for authorization code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
-
-
- The token entry for device code '{Identifier}' was successfully created.
-
-
- A new device code was successfully created: {Payload}.
-The principal used to create the token contained the following claims: {Claims}.
-
-
- The token entry for device code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
+ The token entry for '{Type}' token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
The reference token entry for device code '{Identifier}' was successfully updated'.
-
- The token entry for refresh token '{Identifier}' was successfully created.
-
-
- A new refresh token was successfully created: {Payload}.
-The principal used to create the token contained the following claims: {Claims}.
-
-
- The token entry for refresh token '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
-
-
- The token entry for user code '{Identifier}' was successfully created.
-
-
- A new user code was successfully created: {Payload}.
-The principal used to create the token contained the following claims: {Claims}.
-
-
- The token entry for user code '{Identifier}' was successfully converted to a reference token with the identifier '{ReferenceId}'.
-
-
- The token entry for identity token '{Identifier}' was successfully created.
-
-
- A new identity token was successfully created: {Payload}.
-The principal used to create the token contained the following claims: {Claims}.
-
The authorization request was successfully extracted: {Request}.
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index 7f52d3c0..f1da7661 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -898,6 +898,37 @@ namespace OpenIddict.Core
return string.Equals(await Store.GetTypeAsync(token, cancellationToken), type, StringComparison.OrdinalIgnoreCase);
}
+ ///
+ /// Determines whether a given token has any of the specified types.
+ ///
+ /// The token.
+ /// The expected types.
+ /// The that can be used to abort the operation.
+ /// true if the token has any of the specified types, false otherwise.
+ public virtual async ValueTask HasTypeAsync(TToken token, ImmutableArray types, CancellationToken cancellationToken = default)
+ {
+ if (token is null)
+ {
+ throw new ArgumentNullException(nameof(token));
+ }
+
+ var type = await Store.GetTypeAsync(token, cancellationToken);
+ if (string.IsNullOrEmpty(type))
+ {
+ return false;
+ }
+
+ for (var index = 0; index < types.Length; index++)
+ {
+ if (string.Equals(type, types[index], StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
///
/// Executes the specified query and returns all the corresponding elements.
///
@@ -1430,6 +1461,10 @@ namespace OpenIddict.Core
ValueTask IOpenIddictTokenManager.HasTypeAsync(object token, string type, CancellationToken cancellationToken)
=> HasTypeAsync((TToken) token, type, cancellationToken);
+ ///
+ ValueTask IOpenIddictTokenManager.HasTypeAsync(object token, ImmutableArray types, CancellationToken cancellationToken)
+ => HasTypeAsync((TToken) token, types, cancellationToken);
+
///
IAsyncEnumerable IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken);
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
index 87844bb4..227e96cc 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
@@ -28,10 +28,16 @@ namespace OpenIddict.Server.AspNetCore
public static class Properties
{
+ public const string AccessTokenPrincipal = ".access_token_principal";
+ public const string AuthorizationCodePrincipal = ".authorization_code_principal";
+ public const string DeviceCodePrincipal = ".device_code_principal";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
+ public const string IdentityTokenPrincipal = ".identity_token_principal";
+ public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string Scope = ".scope";
+ public const string UserCodePrincipal = ".user_code_principal";
}
}
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
index 536c2fbc..d30405a1 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
@@ -17,6 +16,7 @@ using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
+using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.AspNetCore
@@ -145,9 +145,9 @@ namespace OpenIddict.Server.AspNetCore
var properties = new AuthenticationProperties(new Dictionary
{
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = context.Error,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri
+ [Properties.Error] = context.Error,
+ [Properties.ErrorDescription] = context.ErrorDescription,
+ [Properties.ErrorUri] = context.ErrorUri
});
return AuthenticateResult.Fail(SR.GetResourceString(SR.ID0113), properties);
@@ -155,29 +155,131 @@ namespace OpenIddict.Server.AspNetCore
else
{
- Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009));
- Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010));
+ // A single main claims-based principal instance can be attached to an authentication ticket.
+ // To return the most appropriate one, the principal is selected based on the endpoint type.
+ // Independently of the selected main principal, all principals resolved from validated tokens
+ // are attached to the authentication properties bag so they can be accessed from user code.
+ var principal = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout
+ => context.IdentityTokenPrincipal,
+
+ OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
+ => context.AccessTokenPrincipal ??
+ context.RefreshTokenPrincipal ??
+ context.IdentityTokenPrincipal ??
+ context.AuthorizationCodePrincipal ??
+ context.DeviceCodePrincipal ??
+ context.UserCodePrincipal,
+
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => context.AuthorizationCodePrincipal,
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => context.DeviceCodePrincipal,
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => context.RefreshTokenPrincipal,
+
+ OpenIddictServerEndpointType.Userinfo => context.AccessTokenPrincipal,
+
+ OpenIddictServerEndpointType.Verification => context.UserCodePrincipal,
+
+ _ => null
+ };
+
+ if (principal is null)
+ {
+ return AuthenticateResult.NoResult();
+ }
- // Store the token to allow any OWIN/Katana component (e.g a controller)
- // to retrieve it (e.g to make an API request to another application).
var properties = new AuthenticationProperties
{
- ExpiresUtc = context.Principal.GetExpirationDate(),
- IssuedUtc = context.Principal.GetCreationDate()
+ ExpiresUtc = principal.GetExpirationDate(),
+ IssuedUtc = principal.GetCreationDate()
};
- properties.StoreTokens(new[]
+ List? tokens = null;
+
+ // 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 (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken))
{
- new AuthenticationToken
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
{
- Name = context.Principal.GetTokenType()!,
- Value = context.Token
- }
- });
+ Name = TokenTypeHints.AccessToken,
+ Value = context.AccessToken
+ });
+
+ properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
+ }
+
+ if (context.AuthorizationCodePrincipal is not null && !string.IsNullOrEmpty(context.AuthorizationCode))
+ {
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
+ {
+ Name = TokenTypeHints.AuthorizationCode,
+ Value = context.AuthorizationCode
+ });
+
+ properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
+ }
+
+ if (context.DeviceCodePrincipal is not null && !string.IsNullOrEmpty(context.DeviceCode))
+ {
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
+ {
+ Name = TokenTypeHints.DeviceCode,
+ Value = context.DeviceCode
+ });
+
+ properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal);
+ }
+
+ if (context.IdentityTokenPrincipal is not null && !string.IsNullOrEmpty(context.IdentityToken))
+ {
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
+ {
+ Name = TokenTypeHints.IdToken,
+ Value = context.IdentityToken
+ });
+
+ properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal);
+ }
+
+ if (context.RefreshTokenPrincipal is not null && !string.IsNullOrEmpty(context.RefreshToken))
+ {
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
+ {
+ Name = TokenTypeHints.RefreshToken,
+ Value = context.RefreshToken
+ });
+
+ properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
+ }
+
+ if (context.UserCodePrincipal is not null && !string.IsNullOrEmpty(context.UserCode))
+ {
+ tokens ??= new(capacity: 1);
+ tokens.Add(new AuthenticationToken
+ {
+ Name = TokenTypeHints.UserCode,
+ Value = context.UserCode
+ });
+
+ properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal);
+ }
+
+ if (tokens is { Count: > 0 })
+ {
+ properties.StoreTokens(tokens);
+ }
- return AuthenticateResult.Success(new AuthenticationTicket(
- context.Principal, properties,
+ return AuthenticateResult.Success(new AuthenticationTicket(principal, properties,
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
}
}
diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
index 3f7b3473..e598b117 100644
--- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
+++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
@@ -143,13 +143,13 @@ namespace OpenIddict.Server.AspNetCore
}
///
- /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled.
+ /// Represents a filter that excludes the associated handlers if status code pages support was not enabled.
///
- public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter
+ public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
- public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options)
+ public RequireStatusCodePagesIntegrationEnabled(IOptionsMonitor options)
=> _options = options;
public ValueTask IsActiveAsync(BaseContext context)
@@ -159,18 +159,18 @@ namespace OpenIddict.Server.AspNetCore
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(!_options.CurrentValue.DisableTransportSecurityRequirement);
+ return new ValueTask(_options.CurrentValue.EnableStatusCodePagesIntegration);
}
}
///
- /// Represents a filter that excludes the associated handlers if status code pages support was not enabled.
+ /// Represents a filter that excludes the associated handlers if the HTTPS requirement was disabled.
///
- public class RequireStatusCodePagesIntegrationEnabled : IOpenIddictServerHandlerFilter
+ public class RequireTransportSecurityRequirementEnabled : IOpenIddictServerHandlerFilter
{
private readonly IOptionsMonitor _options;
- public RequireStatusCodePagesIntegrationEnabled(IOptionsMonitor options)
+ public RequireTransportSecurityRequirementEnabled(IOptionsMonitor options)
=> _options = options;
public ValueTask IsActiveAsync(BaseContext context)
@@ -180,7 +180,7 @@ namespace OpenIddict.Server.AspNetCore
throw new ArgumentNullException(nameof(context));
}
- return new ValueTask(_options.CurrentValue.EnableStatusCodePagesIntegration);
+ return new ValueTask(!_options.CurrentValue.DisableTransportSecurityRequirement);
}
}
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs
index 1c4cbd88..5b3247a6 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs
+++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs
@@ -10,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Server;
using OpenIddict.Server.DataProtection;
-using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlers;
namespace Microsoft.Extensions.DependencyInjection
@@ -40,13 +39,6 @@ namespace Microsoft.Extensions.DependencyInjection
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor));
- // Register the built-in filter used by the default OpenIddict Data Protection event handlers.
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
- builder.Services.TryAddSingleton();
-
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs
deleted file mode 100644
index 44b918b0..00000000
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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;
-using System.ComponentModel;
-using System.Threading.Tasks;
-using Microsoft.Extensions.Options;
-using static OpenIddict.Server.OpenIddictServerEvents;
-
-namespace OpenIddict.Server.DataProtection
-{
- ///
- /// Contains a collection of event handler filters commonly used by the Data Protection handlers.
- ///
- [EditorBrowsable(EditorBrowsableState.Advanced)]
- public static class OpenIddictServerDataProtectionHandlerFilters
- {
- ///
- /// Represents a filter that excludes the associated handlers if OpenIddict
- /// was not configured to issue ASP.NET Core Data Protection access tokens.
- ///
- public class RequireDataProtectionAccessTokenFormatEnabled : IOpenIddictServerHandlerFilter
- {
- private readonly IOptionsMonitor _options;
-
- public RequireDataProtectionAccessTokenFormatEnabled(IOptionsMonitor options)
- => _options = options;
-
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!_options.CurrentValue.PreferDefaultAccessTokenFormat);
- }
- }
-
- ///
- /// Represents a filter that excludes the associated handlers if OpenIddict
- /// was not configured to issue ASP.NET Core Data Protection authorization codes.
- ///
- public class RequireDataProtectionAuthorizationCodeFormatEnabled : IOpenIddictServerHandlerFilter
- {
- private readonly IOptionsMonitor _options;
-
- public RequireDataProtectionAuthorizationCodeFormatEnabled(IOptionsMonitor options)
- => _options = options;
-
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!_options.CurrentValue.PreferDefaultAuthorizationCodeFormat);
- }
- }
-
- ///
- /// Represents a filter that excludes the associated handlers if OpenIddict
- /// was not configured to issue ASP.NET Core Data Protection device codes.
- ///
- public class RequireDataProtectionDeviceCodeFormatEnabled : IOpenIddictServerHandlerFilter
- {
- private readonly IOptionsMonitor _options;
-
- public RequireDataProtectionDeviceCodeFormatEnabled(IOptionsMonitor options)
- => _options = options;
-
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!_options.CurrentValue.PreferDefaultDeviceCodeFormat);
- }
- }
-
- ///
- /// Represents a filter that excludes the associated handlers if OpenIddict
- /// was not configured to issue ASP.NET Core Data Protection refresh tokens.
- ///
- public class RequireDataProtectionRefreshTokenFormatEnabled : IOpenIddictServerHandlerFilter
- {
- private readonly IOptionsMonitor _options;
-
- public RequireDataProtectionRefreshTokenFormatEnabled(IOptionsMonitor options)
- => _options = options;
-
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!_options.CurrentValue.PreferDefaultRefreshTokenFormat);
- }
- }
-
- ///
- /// Represents a filter that excludes the associated handlers if OpenIddict
- /// was not configured to issue ASP.NET Core Data Protection user codes.
- ///
- public class RequireDataProtectionUserCodeFormatEnabled : IOpenIddictServerHandlerFilter
- {
- private readonly IOptionsMonitor _options;
-
- public RequireDataProtectionUserCodeFormatEnabled(IOptionsMonitor options)
- => _options = options;
-
- public ValueTask IsActiveAsync(BaseContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new ValueTask(!_options.CurrentValue.PreferDefaultUserCodeFormat);
- }
- }
- }
-}
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
new file mode 100644
index 00000000..a6fa2961
--- /dev/null
+++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
@@ -0,0 +1,332 @@
+/*
+ * 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;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+using static OpenIddict.Abstractions.OpenIddictConstants;
+using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes;
+using static OpenIddict.Server.OpenIddictServerEvents;
+using static OpenIddict.Server.OpenIddictServerHandlers.Protection;
+using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes;
+using SR = OpenIddict.Abstractions.OpenIddictResources;
+
+namespace OpenIddict.Server.DataProtection
+{
+ public static partial class OpenIddictServerDataProtectionHandlers
+ {
+ public static class Protection
+ {
+ public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create(
+ /*
+ * Token validation:
+ */
+ ValidateDataProtectionToken.Descriptor,
+
+ /*
+ * Token validation:
+ */
+ GenerateDataProtectionToken.Descriptor);
+
+ ///
+ /// Contains the logic responsible of validating tokens generated using Data Protection.
+ ///
+ public class ValidateDataProtectionToken : IOpenIddictServerHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public ValidateDataProtectionToken(IOptionsMonitor options)
+ => _options = options;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ // If a principal was already attached, don't overwrite it.
+ if (context.Principal is not null)
+ {
+ return default;
+ }
+
+ // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds
+ // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads.
+ if (!context.Token.StartsWith("CfDJ8", StringComparison.Ordinal))
+ {
+ return default;
+ }
+
+ // Tokens generated using ASP.NET Core Data Protection are encrypted by symmetric keys
+ // that are derived from both a master key resolved from the key ring and a specific value
+ // known as "purpose" that helps ensure that Data Protection payloads can't be decrypted
+ // without the correct "purpose" value, which is different for all types of tokens.
+ //
+ // While offering extensive protection at the cryptographic level, this prevents decrypting
+ // unknown tokens without re-executing the entire decryption routine for each type of token
+ // considered valid. To speed up this process when supporting multiple types is required,
+ // the Data Protection integration relies on the "token_type_hint" parameter specified
+ // by the client when it is available (e.g with introspection or revocation requests).
+
+ var principal = context.ValidTokenTypes.Count switch
+ {
+ // If no valid token type was set, all supported token types are allowed.
+ //
+ // Note: if a "token_type_hint" was specified by the client, use it to optimize
+ // the token decryption lookup but fall back to other types of tokens
+ // if the token can't be decrypted using the specified token type hint.
+ //
+ // In this case, common types (e.g access/refresh tokens) are checked first.
+ 0 => context.TokenTypeHint switch
+ {
+ TokenTypeHints.AuthorizationCode =>
+ ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
+ ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
+ ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
+ ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
+ ValidateToken(context.Token, TokenTypeHints.UserCode),
+
+ TokenTypeHints.DeviceCode =>
+ ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
+ ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
+ ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
+ ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
+ ValidateToken(context.Token, TokenTypeHints.UserCode),
+
+ TokenTypeHints.RefreshToken =>
+ ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
+ ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
+ ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
+ ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
+ ValidateToken(context.Token, TokenTypeHints.UserCode),
+
+ TokenTypeHints.UserCode =>
+ ValidateToken(context.Token, TokenTypeHints.UserCode) ??
+ ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
+ ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
+ ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
+ ValidateToken(context.Token, TokenTypeHints.DeviceCode),
+
+ _ =>
+ ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
+ ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
+ ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
+ ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
+ ValidateToken(context.Token, TokenTypeHints.UserCode),
+ },
+
+ // If a single valid token type was set, ignore the specified token type hint.
+ 1 => context.ValidTokenTypes.ElementAt(0) switch
+ {
+ TokenTypeHints.AccessToken => ValidateToken(context.Token, TokenTypeHints.AccessToken),
+ TokenTypeHints.RefreshToken => ValidateToken(context.Token, TokenTypeHints.RefreshToken),
+ TokenTypeHints.AuthorizationCode => ValidateToken(context.Token, TokenTypeHints.AuthorizationCode),
+ TokenTypeHints.DeviceCode => ValidateToken(context.Token, TokenTypeHints.DeviceCode),
+ TokenTypeHints.UserCode => ValidateToken(context.Token, TokenTypeHints.UserCode),
+
+ _ => null // The token type is not supported by the Data Protection integration (e.g identity tokens).
+ },
+
+ // If multiple valid types were set, use the specified token type hint
+ // and select the first non-null token that can be successfully decrypted.
+ _ => context.ValidTokenTypes.OrderBy(type => type switch
+ {
+ // If the token type hint corresponds to one of the valid types, test it first.
+ string value when value == context.TokenTypeHint => 0,
+
+ TokenTypeHints.AccessToken => 1,
+ TokenTypeHints.RefreshToken => 2,
+ TokenTypeHints.AuthorizationCode => 3,
+ TokenTypeHints.DeviceCode => 4,
+ TokenTypeHints.UserCode => 5,
+
+ _ => int.MaxValue
+ })
+ .Select(type => type switch
+ {
+ TokenTypeHints.AccessToken => ValidateToken(context.Token, TokenTypeHints.AccessToken),
+ TokenTypeHints.RefreshToken => ValidateToken(context.Token, TokenTypeHints.RefreshToken),
+ TokenTypeHints.AuthorizationCode => ValidateToken(context.Token, TokenTypeHints.AuthorizationCode),
+ TokenTypeHints.DeviceCode => ValidateToken(context.Token, TokenTypeHints.DeviceCode),
+ TokenTypeHints.UserCode => ValidateToken(context.Token, TokenTypeHints.UserCode),
+
+ _ => null // The token type is not supported by the Data Protection integration (e.g identity tokens).
+ })
+ .Where(static principal => principal is not null)
+ .FirstOrDefault()
+ };
+
+ if (principal is null)
+ {
+ context.Reject(
+ error: Errors.InvalidToken,
+ description: SR.GetResourceString(SR.ID2004),
+ uri: SR.FormatID8000(SR.ID2004));
+
+ return default;
+ }
+
+ context.Principal = principal;
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims);
+
+ return default;
+
+ ClaimsPrincipal? ValidateToken(string token, string type)
+ {
+ // Create a Data Protection protector using the provider registered in the options.
+ var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch
+ {
+ // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
+ TokenTypeHints.AccessToken when !string.IsNullOrEmpty(context.TokenId)
+ => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
+
+ TokenTypeHints.AuthorizationCode when !string.IsNullOrEmpty(context.TokenId)
+ => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
+
+ TokenTypeHints.DeviceCode when !string.IsNullOrEmpty(context.TokenId)
+ => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
+
+ TokenTypeHints.RefreshToken when !string.IsNullOrEmpty(context.TokenId)
+ => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
+
+ TokenTypeHints.UserCode when !string.IsNullOrEmpty(context.TokenId)
+ => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ });
+
+ try
+ {
+ using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token)));
+ using var reader = new BinaryReader(buffer);
+
+ // Note: since the data format relies on a data protector using different "purposes" strings
+ // per token type, the token processed at this stage is guaranteed to be of the expected type.
+ return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type);
+ }
+
+ catch (Exception exception)
+ {
+ context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), token);
+
+ return null;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of generating a token using Data Protection.
+ ///
+ public class GenerateDataProtectionToken : IOpenIddictServerHandler
+ {
+ private readonly IOptionsMonitor _options;
+
+ public GenerateDataProtectionToken(IOptionsMonitor options)
+ => _options = options;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If an access token was already attached by another handler, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.Token))
+ {
+ return default;
+ }
+
+ if (context.TokenType switch
+ {
+ TokenTypeHints.AccessToken => _options.CurrentValue.PreferDefaultAccessTokenFormat,
+ TokenTypeHints.AuthorizationCode => _options.CurrentValue.PreferDefaultAuthorizationCodeFormat,
+ TokenTypeHints.DeviceCode => _options.CurrentValue.PreferDefaultDeviceCodeFormat,
+ TokenTypeHints.RefreshToken => _options.CurrentValue.PreferDefaultRefreshTokenFormat,
+ TokenTypeHints.UserCode => _options.CurrentValue.PreferDefaultUserCodeFormat,
+
+ _ => true // The token type is not supported by the Data Protection integration (e.g identity tokens).
+ })
+ {
+ return default;
+ }
+
+ // Create a Data Protection protector using the provider registered in the options.
+ var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(context.TokenType switch
+ {
+ // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
+ TokenTypeHints.AccessToken when context.Options.UseReferenceAccessTokens
+ => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
+
+ TokenTypeHints.AuthorizationCode when !context.Options.DisableTokenStorage
+ => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
+
+ TokenTypeHints.DeviceCode when !context.Options.DisableTokenStorage
+ => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
+
+ TokenTypeHints.RefreshToken when context.Options.UseReferenceRefreshTokens
+ => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
+
+ TokenTypeHints.UserCode when !context.Options.DisableTokenStorage
+ => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
+ TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ });
+
+ using var buffer = new MemoryStream();
+ using var writer = new BinaryWriter(buffer);
+
+ _options.CurrentValue.Formatter.WriteToken(writer, context.Principal);
+
+ context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType,
+ context.Token, context.Principal.Claims);
+
+ return default;
+ }
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
index 72d21610..2463b253 100644
--- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
+++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
@@ -4,478 +4,15 @@
* the license and the contributors participating to this project.
*/
-using System;
using System.Collections.Immutable;
using System.ComponentModel;
-using System.IO;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.DataProtection;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Microsoft.IdentityModel.Tokens;
-using OpenIddict.Abstractions;
-using static OpenIddict.Abstractions.OpenIddictConstants;
-using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes;
-using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
-using static OpenIddict.Server.OpenIddictServerEvents;
-using static OpenIddict.Server.OpenIddictServerHandlerFilters;
-using static OpenIddict.Server.OpenIddictServerHandlers;
-using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties;
-using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes;
-using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.DataProtection
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictServerDataProtectionHandlers
{
- public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create(
- /*
- * Authentication processing:
- */
- ValidateDataProtectionToken.Descriptor,
-
- /*
- * Sign-in processing:
- */
- GenerateDataProtectionAccessToken.Descriptor,
- GenerateDataProtectionAuthorizationCode.Descriptor,
- GenerateDataProtectionDeviceCode.Descriptor,
- GenerateDataProtectionRefreshToken.Descriptor,
- GenerateDataProtectionUserCode.Descriptor);
-
- ///
- /// Contains the logic responsible of validating tokens generated using Data Protection.
- ///
- public class ValidateDataProtectionToken : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public ValidateDataProtectionToken(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
- .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessAuthenticationContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If a principal was already attached, don't overwrite it.
- if (context.Principal is not null)
- {
- return default;
- }
-
- // Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds
- // to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads.
- if (string.IsNullOrEmpty(context.Token) || !context.Token.StartsWith("CfDJ8", StringComparison.Ordinal))
- {
- return default;
- }
-
- var principal = !string.IsNullOrEmpty(context.TokenType) ?
- ValidateToken(context.Token, context.TokenType) :
- ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
- ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
- ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
- ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
- ValidateToken(context.Token, TokenTypeHints.UserCode);
-
- if (principal is null)
- {
- context.Reject(
- error: context.EndpointType switch
- {
- OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
- _ => Errors.InvalidToken
- },
- description: SR.GetResourceString(SR.ID2004),
- uri: SR.FormatID8000(SR.ID2004));
-
- return default;
- }
-
- context.Principal = principal;
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims);
-
- return default;
-
- ClaimsPrincipal? ValidateToken(string token, string type)
- {
- // Create a Data Protection protector using the provider registered in the options.
- var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch
- {
- TokenTypeHints.AccessToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
- => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
-
- TokenTypeHints.AuthorizationCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
- => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
-
- TokenTypeHints.DeviceCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
- => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
-
- TokenTypeHints.RefreshToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
- => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
-
- TokenTypeHints.UserCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
- => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
-
- TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
- TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
- TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
- TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
- TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
-
- _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
- });
-
- try
- {
- using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token)));
- using var reader = new BinaryReader(buffer);
-
- // Note: since the data format relies on a data protector using different "purposes" strings
- // per token type, the token processed at this stage is guaranteed to be of the expected type.
- return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type);
- }
-
- catch (Exception exception)
- {
- context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), token);
-
- return null;
- }
- }
- }
- }
-
- ///
- /// Contains the logic responsible of generating an access token using Data Protection.
- ///
- public class GenerateDataProtectionAccessToken : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public GenerateDataProtectionAccessToken(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order - 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessSignInContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If an access token was already attached by another handler, don't overwrite it.
- if (!string.IsNullOrEmpty(context.Response.AccessToken))
- {
- return default;
- }
-
- if (context.AccessTokenPrincipal is null)
- {
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
- }
-
- // Create a Data Protection protector using the provider registered in the options.
- var protector = context.Options.UseReferenceAccessTokens ?
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server) :
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.AccessToken, Schemes.Server);
-
- using var buffer = new MemoryStream();
- using var writer = new BinaryWriter(buffer);
-
- _options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal);
-
- context.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6013),
- context.AccessTokenPrincipal.GetClaim(Claims.JwtId),
- context.AccessToken, context.AccessTokenPrincipal.Claims);
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible of generating an authorization code using Data Protection.
- ///
- public class GenerateDataProtectionAuthorizationCode : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public GenerateDataProtectionAuthorizationCode(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order - 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessSignInContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If an authorization code was already attached by another handler, don't overwrite it.
- if (!string.IsNullOrEmpty(context.Response.Code))
- {
- return default;
- }
-
- if (context.AuthorizationCodePrincipal is null)
- {
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
- }
-
- // Create a Data Protection protector using the provider registered in the options.
- var protector = !context.Options.DisableTokenStorage ?
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server) :
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.AuthorizationCode, Schemes.Server);
-
- using var buffer = new MemoryStream();
- using var writer = new BinaryWriter(buffer);
-
- _options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal);
-
- context.AuthorizationCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6016),
- context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId),
- context.AuthorizationCode, context.AuthorizationCodePrincipal.Claims);
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible of generating a device code using Data Protection.
- ///
- public class GenerateDataProtectionDeviceCode : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public GenerateDataProtectionDeviceCode(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order - 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessSignInContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If a device code was already attached by another handler, don't overwrite it.
- if (!string.IsNullOrEmpty(context.Response.DeviceCode))
- {
- return default;
- }
-
- if (context.DeviceCodePrincipal is null)
- {
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
- }
-
- // Create a Data Protection protector using the provider registered in the options.
- var protector = !context.Options.DisableTokenStorage ?
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server) :
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.DeviceCode, Schemes.Server);
-
- using var buffer = new MemoryStream();
- using var writer = new BinaryWriter(buffer);
-
- _options.CurrentValue.Formatter.WriteToken(writer, context.DeviceCodePrincipal);
-
- context.DeviceCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6019),
- context.DeviceCodePrincipal.GetClaim(Claims.JwtId),
- context.DeviceCode, context.DeviceCodePrincipal.Claims);
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible of generating a refresh token using Data Protection.
- ///
- public class GenerateDataProtectionRefreshToken : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public GenerateDataProtectionRefreshToken(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order - 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessSignInContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If a refresh token was already attached by another handler, don't overwrite it.
- if (!string.IsNullOrEmpty(context.Response.RefreshToken))
- {
- return default;
- }
-
- if (context.RefreshTokenPrincipal is null)
- {
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
- }
-
- // Create a Data Protection protector using the provider registered in the options.
- var protector = context.Options.UseReferenceRefreshTokens ?
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server) :
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.RefreshToken, Schemes.Server);
-
- using var buffer = new MemoryStream();
- using var writer = new BinaryWriter(buffer);
-
- _options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal);
-
- context.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6023),
- context.RefreshTokenPrincipal.GetClaim(Claims.JwtId),
- context.RefreshToken, context.RefreshTokenPrincipal.Claims);
-
- return default;
- }
- }
-
- ///
- /// Contains the logic responsible of generating a user code using Data Protection.
- ///
- public class GenerateDataProtectionUserCode : IOpenIddictServerHandler
- {
- private readonly IOptionsMonitor _options;
-
- public GenerateDataProtectionUserCode(IOptionsMonitor options)
- => _options = options;
-
- ///
- /// Gets the default descriptor definition assigned to this handler.
- ///
- public static OpenIddictServerHandlerDescriptor Descriptor { get; }
- = OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(GenerateIdentityModelUserCode.Descriptor.Order - 500)
- .SetType(OpenIddictServerHandlerType.BuiltIn)
- .Build();
-
- ///
- public ValueTask HandleAsync(ProcessSignInContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- // If a user code was already attached by another handler, don't overwrite it.
- if (!string.IsNullOrEmpty(context.Response.UserCode))
- {
- return default;
- }
-
- if (context.UserCodePrincipal is null)
- {
- throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
- }
-
- // Create a Data Protection protector using the provider registered in the options.
- var protector = !context.Options.DisableTokenStorage ?
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server) :
- _options.CurrentValue.DataProtectionProvider.CreateProtector(
- Handlers.Server, Formats.UserCode, Schemes.Server);
-
- using var buffer = new MemoryStream();
- using var writer = new BinaryWriter(buffer);
-
- _options.CurrentValue.Formatter.WriteToken(writer, context.UserCodePrincipal);
-
- context.UserCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
-
- context.Logger.LogTrace(SR.GetResourceString(SR.ID6026),
- context.UserCodePrincipal.GetClaim(Claims.JwtId),
- context.UserCode, context.UserCodePrincipal.Claims);
-
- return default;
- }
- }
+ public static ImmutableArray DefaultHandlers { get; }
+ = ImmutableArray.CreateRange(Protection.DefaultHandlers);
}
}
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
index 6d534859..e51b547e 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
@@ -39,10 +39,16 @@ namespace OpenIddict.Server.Owin
public static class Properties
{
+ public const string AccessTokenPrincipal = ".access_token_principal";
+ public const string AuthorizationCodePrincipal = ".authorization_code_principal";
+ public const string DeviceCodePrincipal = ".device_code_principal";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
+ public const string IdentityTokenPrincipal = ".identity_token_principal";
+ public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string Scope = ".scope";
+ public const string UserCodePrincipal = ".user_code_principal";
}
}
}
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
index e015ab32..2238f199 100644
--- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin;
@@ -15,6 +14,7 @@ using Microsoft.Owin.Security.Infrastructure;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
+using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server.Owin
@@ -155,11 +155,11 @@ namespace OpenIddict.Server.Owin
return null;
}
- var properties = new AuthenticationProperties(new Dictionary
+ var properties = new OpenIddictServerOwinProperties(new Dictionary
{
- [OpenIddictServerOwinConstants.Properties.Error] = context.Error,
- [OpenIddictServerOwinConstants.Properties.ErrorDescription] = context.ErrorDescription,
- [OpenIddictServerOwinConstants.Properties.ErrorUri] = context.ErrorUri
+ [Properties.Error] = context.Error,
+ [Properties.ErrorDescription] = context.ErrorDescription,
+ [Properties.ErrorUri] = context.ErrorUri
});
return new AuthenticationTicket(null, properties);
@@ -167,22 +167,88 @@ namespace OpenIddict.Server.Owin
else
{
- Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
- Debug.Assert(!string.IsNullOrEmpty(context.Principal.GetTokenType()), SR.GetResourceString(SR.ID4009));
- Debug.Assert(!string.IsNullOrEmpty(context.Token), SR.GetResourceString(SR.ID4010));
+ // A single main claims-based principal instance can be attached to an authentication ticket.
+ // To return the most appropriate one, the principal is selected based on the endpoint type.
+ // Independently of the selected main principal, all principals resolved from validated tokens
+ // are attached to the authentication properties bag so they can be accessed from user code.
+ var principal = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout
+ => context.IdentityTokenPrincipal,
+
+ OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
+ => context.AccessTokenPrincipal ??
+ context.RefreshTokenPrincipal ??
+ context.IdentityTokenPrincipal ??
+ context.AuthorizationCodePrincipal ??
+ context.DeviceCodePrincipal ??
+ context.UserCodePrincipal,
+
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => context.AuthorizationCodePrincipal,
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => context.DeviceCodePrincipal,
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => context.RefreshTokenPrincipal,
+
+ OpenIddictServerEndpointType.Userinfo => context.AccessTokenPrincipal,
- // Store the token to allow any OWIN/Katana component (e.g a controller)
- // to retrieve it (e.g to make an API request to another application).
- var properties = new AuthenticationProperties(new Dictionary
+ OpenIddictServerEndpointType.Verification => context.UserCodePrincipal,
+
+ _ => null
+ };
+
+ if (principal is null)
{
- [context.Principal.GetTokenType()!] = context.Token
- })
+ return null;
+ }
+
+ var properties = new OpenIddictServerOwinProperties
{
- ExpiresUtc = context.Principal.GetExpirationDate(),
- IssuedUtc = context.Principal.GetCreationDate()
+ ExpiresUtc = principal.GetExpirationDate(),
+ IssuedUtc = principal.GetCreationDate()
};
- return new AuthenticationTicket((ClaimsIdentity) context.Principal.Identity, properties);
+ // 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 (context.AccessTokenPrincipal is not null && !string.IsNullOrEmpty(context.AccessToken))
+ {
+ properties.Dictionary[TokenTypeHints.AccessToken] = context.AccessToken;
+ properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
+ }
+
+ if (context.AuthorizationCodePrincipal is not null && !string.IsNullOrEmpty(context.AuthorizationCode))
+ {
+ properties.Dictionary[TokenTypeHints.AuthorizationCode] = context.AuthorizationCode;
+ properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
+ }
+
+ if (context.DeviceCodePrincipal is not null && !string.IsNullOrEmpty(context.DeviceCode))
+ {
+ properties.Dictionary[TokenTypeHints.DeviceCode] = context.DeviceCode;
+ properties.SetParameter(Properties.DeviceCodePrincipal, context.DeviceCodePrincipal);
+ }
+
+ if (context.IdentityTokenPrincipal is not null && !string.IsNullOrEmpty(context.IdentityToken))
+ {
+ properties.Dictionary[TokenTypeHints.IdToken] = context.IdentityToken;
+ properties.SetParameter(Properties.IdentityTokenPrincipal, context.IdentityTokenPrincipal);
+ }
+
+ if (context.RefreshTokenPrincipal is not null && !string.IsNullOrEmpty(context.RefreshToken))
+ {
+ properties.Dictionary[TokenTypeHints.RefreshToken] = context.RefreshToken;
+ properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);
+ }
+
+ if (context.UserCodePrincipal is not null && !string.IsNullOrEmpty(context.UserCode))
+ {
+ properties.Dictionary[TokenTypeHints.UserCode] = context.UserCode;
+ properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal);
+ }
+
+ return new AuthenticationTicket((ClaimsIdentity) principal.Identity, properties);
}
}
@@ -205,7 +271,7 @@ namespace OpenIddict.Server.Owin
// corresponds to a challenge response, as LookupChallenge() will always return a non-null
// value when active authentication is used, even if no challenge was actually triggered.
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
- if (challenge is not null && (Response.StatusCode == 401 || Response.StatusCode == 403))
+ if (challenge is not null && Response.StatusCode is 401 or 403)
{
var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0112));
diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs
new file mode 100644
index 00000000..1f0b0bc1
--- /dev/null
+++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinProperties.cs
@@ -0,0 +1,89 @@
+/*
+ * 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;
+using System.Collections.Generic;
+using Microsoft.Owin.Security;
+using SR = OpenIddict.Abstractions.OpenIddictResources;
+
+namespace OpenIddict.Server.Owin
+{
+ ///
+ public class OpenIddictServerOwinProperties : AuthenticationProperties
+ {
+ ///
+ public OpenIddictServerOwinProperties()
+ : this(items: null)
+ {
+ }
+
+ ///
+ public OpenIddictServerOwinProperties(IDictionary? items)
+ : this(items, parameters: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// State values dictionary to use.
+ /// Parameters dictionary to use.
+ public OpenIddictServerOwinProperties(
+ IDictionary? items,
+ IDictionary? parameters)
+ : base(items)
+ => Parameters = parameters ?? new Dictionary(StringComparer.Ordinal);
+
+ ///
+ /// Gets the collection of parameters passed to the authentication handler.
+ ///
+ ///
+ /// Note: these properties are not intended for serialization or persistence,
+ /// only for flowing data between call sites.
+ ///
+ public IDictionary Parameters { get; }
+
+ ///
+ /// Gets a parameter from the collection.
+ ///
+ /// The parameter type.
+ /// The parameter name.
+ /// The parameter value or a default value if the property is not set.
+ public T? GetParameter(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(SR.ID0190, nameof(name));
+ }
+
+ return Parameters.TryGetValue(name, out var parameter) && parameter is T value ? value : default;
+ }
+
+ ///
+ /// Sets a parameter value in the collection.
+ ///
+ /// The parameter type.
+ /// The parameter key.
+ /// The value to set.
+ public void SetParameter(string name, T? value)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentException(SR.ID0190, nameof(name));
+ }
+
+ if (value is null)
+ {
+ Parameters.Remove(name);
+ }
+
+ else
+ {
+ Parameters[name] = value;
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
index 6879179e..9329c19b 100644
--- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
+++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs
@@ -227,13 +227,13 @@ namespace OpenIddict.Server
throw new InvalidOperationException(SR.GetResourceString(SR.ID0095));
}
- // If the degraded mode was enabled, ensure custom authentication/sign-in handlers
+ // If the degraded mode was enabled, ensure custom validation/generation handlers
// have been registered to deal with device/user codes validation and generation.
if (options.GrantTypes.Contains(GrantTypes.DeviceCode))
{
if (!options.Handlers.Any(
- descriptor => descriptor.ContextType == typeof(ProcessAuthenticationContext) &&
+ descriptor => descriptor.ContextType == typeof(ValidateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
@@ -241,7 +241,7 @@ namespace OpenIddict.Server
}
if (!options.Handlers.Any(
- descriptor => descriptor.ContextType == typeof(ProcessSignInContext) &&
+ descriptor => descriptor.ContextType == typeof(GenerateTokenContext) &&
descriptor.Type == OpenIddictServerHandlerType.Custom &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
@@ -258,8 +258,7 @@ namespace OpenIddict.Server
options.SigningCredentials.Sort((left, right) => Compare(left.Key, right.Key));
// Generate a key identifier for the encryption/signing keys that don't already have one.
- foreach (var key in options.EncryptionCredentials
- .Select(credentials => credentials.Key)
+ foreach (var key in options.EncryptionCredentials.Select(credentials => credentials.Key)
.Concat(options.SigningCredentials.Select(credentials => credentials.Key))
.Where(key => string.IsNullOrEmpty(key.KeyId)))
{
diff --git a/src/OpenIddict.Server/OpenIddictServerConstants.cs b/src/OpenIddict.Server/OpenIddictServerConstants.cs
deleted file mode 100644
index 2a53578c..00000000
--- a/src/OpenIddict.Server/OpenIddictServerConstants.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * 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.Server
-{
- public static class OpenIddictServerConstants
- {
- public static class Properties
- {
- public const string ReferenceTokenIdentifier = ".reference_token_identifier";
- }
- }
-}
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
new file mode 100644
index 00000000..0781c1aa
--- /dev/null
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
@@ -0,0 +1,129 @@
+/*
+ * 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;
+using System.Collections.Generic;
+using System.Security.Claims;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+
+namespace OpenIddict.Server
+{
+ public static partial class OpenIddictServerEvents
+ {
+ ///
+ /// Represents an event called when generating a token.
+ ///
+ public class GenerateTokenContext : BaseValidatingContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public GenerateTokenContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the security principal used to create the token.
+ ///
+ public ClaimsPrincipal Principal { get; set; } = default!;
+
+ ///
+ /// Gets or sets the encryption credentials used to encrypt the token.
+ ///
+ public EncryptingCredentials? EncryptionCredentials { get; set; }
+
+ ///
+ /// Gets or sets the signing credentials used to sign the token.
+ ///
+ public SigningCredentials? SigningCredentials { get; set; }
+
+ ///
+ /// Gets or sets the security token handler used to serialize the security principal.
+ ///
+ public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;
+
+ ///
+ /// Gets or sets the token returned to the client application.
+ ///
+ public string? Token { get; set; }
+
+ ///
+ /// Gets or sets the type of the token to create.
+ ///
+ public string TokenType { get; set; } = default!;
+ }
+
+ ///
+ /// Represents an event called when validating a token.
+ ///
+ public class ValidateTokenContext : BaseValidatingContext
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ public ValidateTokenContext(OpenIddictServerTransaction transaction)
+ : base(transaction)
+ {
+ }
+
+ ///
+ /// Gets or sets the request.
+ ///
+ public OpenIddictRequest Request
+ {
+ get => Transaction.Request!;
+ set => Transaction.Request = value;
+ }
+
+ ///
+ /// Gets or sets the security token handler used to validate the token.
+ ///
+ public JsonWebTokenHandler SecurityTokenHandler { get; set; } = default!;
+
+ ///
+ /// Gets or sets the validation parameters used to verify the authenticity of tokens.
+ ///
+ public TokenValidationParameters TokenValidationParameters { get; set; } = default!;
+
+ ///
+ /// Gets or sets the token to validate.
+ ///
+ public string Token { get; set; } = default!;
+
+ ///
+ /// Gets or sets the token type hint specified by the client, if applicable.
+ ///
+ public string? TokenTypeHint { get; set; } = default!;
+
+ ///
+ /// Gets or sets the token entry identifier associated with the token, if applicable.
+ ///
+ public string? TokenId { get; set; }
+
+ ///
+ /// Gets or sets the security principal resolved from the token.
+ ///
+ public ClaimsPrincipal? Principal { get; set; }
+
+ ///
+ /// Gets the token types that are considered valid.
+ ///
+ public HashSet ValidTokenTypes { get; } = new(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+}
diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs
index e8664787..2a0d167d 100644
--- a/src/OpenIddict.Server/OpenIddictServerEvents.cs
+++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs
@@ -272,19 +272,191 @@ namespace OpenIddict.Server
}
///
- /// Gets or sets the security principal.
+ /// Gets or sets a boolean indicating whether an access token
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
///
- public ClaimsPrincipal? Principal { get; set; }
+ public bool RequireAccessToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an authorization code
+ /// must be resolved for the authentication to 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 device code
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireDeviceCode { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a generic token
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireGenericToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an identity token
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireIdentityToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a refresh token
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireRefreshToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a user code
+ /// must be resolved for the authentication to considered valid.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool RequireUserCode { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an access token
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateAccessToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an authorization code
+ /// should be extracted from the current context and 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 a device code
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateDeviceCode { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a generic token
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateGenericToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether an identity token
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateIdentityToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a refresh token
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateRefreshToken { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether a user code
+ /// should be extracted from the current context and validated.
+ /// Note: overriding the value of this property is generally not
+ /// recommended, except when dealing with non-standard clients.
+ ///
+ public bool ValidateUserCode { get; set; }
+
+ ///
+ /// Gets or sets the access token to validate, if applicable.
+ ///
+ public string? AccessToken { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the access token, if applicable.
+ ///
+ public ClaimsPrincipal? AccessTokenPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the authorization code to validate, if applicable.
+ ///
+ public string? AuthorizationCode { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the authorization code, if applicable.
+ ///
+ public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
+
+ ///
+ /// Gets or sets the device code to validate, if applicable.
+ ///
+ public string? DeviceCode { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the device code, if applicable.
+ ///
+ public ClaimsPrincipal? DeviceCodePrincipal { get; set; }
+
+ ///
+ /// Gets or sets the generic token to validate, if applicable.
+ ///
+ public string? GenericToken { get; set; }
///
- /// Gets or sets the token to validate.
+ /// Gets or sets the optional hint indicating the type of the generic token, if applicable.
///
- public string? Token { get; set; }
+ public string? GenericTokenTypeHint { get; set; }
///
- /// Gets or sets the expected type of the token.
+ /// Gets or sets the principal extracted from the generic token, if applicable.
///
- public string? TokenType { get; set; }
+ public ClaimsPrincipal? GenericTokenPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the identity token to validate, if applicable.
+ ///
+ public string? IdentityToken { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the identity token, if applicable.
+ ///
+ public ClaimsPrincipal? IdentityTokenPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the refresh token to validate, if applicable.
+ ///
+ public string? RefreshToken { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the refresh token, if applicable.
+ ///
+ public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
+
+ ///
+ /// Gets or sets the user code to validate, if applicable.
+ ///
+ public string? UserCode { get; set; }
+
+ ///
+ /// Gets or sets the principal extracted from the user code, if applicable.
+ ///
+ public ClaimsPrincipal? UserCodePrincipal { get; set; }
}
///
diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
index db8cb5bb..b135c8b4 100644
--- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs
+++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs
@@ -45,7 +45,9 @@ namespace Microsoft.Extensions.DependencyInjection
// Register the built-in filters used by the default OpenIddict server event handlers.
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
@@ -53,16 +55,20 @@ namespace Microsoft.Extensions.DependencyInjection
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();
@@ -71,6 +77,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
index 35276797..4166aadc 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
@@ -30,6 +30,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no access token is validated.
+ ///
+ public class RequireAccessTokenValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateAccessToken);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if no authorization code is generated.
///
@@ -46,6 +62,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no authorization code is validated.
+ ///
+ public class RequireAuthorizationCodeValidated : IOpenIddictServerHandlerFilter
+ {
+ 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 the request is not an authorization request.
///
@@ -158,6 +190,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no device code is validated.
+ ///
+ public class RequireDeviceCodeValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateDeviceCode);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the request is not a device request.
///
@@ -190,6 +238,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no generic token is validated.
+ ///
+ public class RequireGenericTokenValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateGenericToken);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if grant type permissions were disabled.
///
@@ -222,6 +286,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no identity token is validated.
+ ///
+ public class RequireIdentityTokenValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateIdentityToken);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the request is not an introspection request.
///
@@ -318,6 +398,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no refresh token is validated.
+ ///
+ public class RequireRefreshTokenValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateRefreshToken);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if response type permissions were disabled.
///
@@ -446,6 +542,22 @@ namespace OpenIddict.Server
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if no user code is validated.
+ ///
+ public class RequireUserCodeValidated : IOpenIddictServerHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessAuthenticationContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new ValueTask(context.ValidateUserCode);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the request is not a userinfo request.
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
index a9b91fd4..b183298f 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
@@ -1190,7 +1190,7 @@ namespace OpenIddict.Server
}
// Attach the security principal extracted from the token to the validation context.
- context.Principal = notification.Principal;
+ context.Principal = notification.UserCodePrincipal;
}
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
index 2ff73e61..326448df 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
@@ -1318,7 +1318,9 @@ namespace OpenIddict.Server
}
// Attach the security principal extracted from the token to the validation context.
- context.Principal = notification.Principal;
+ context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal :
+ context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal :
+ context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null;
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
index f9504d30..4db4bb66 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
@@ -700,6 +700,10 @@ namespace OpenIddict.Server
var notification = new ProcessAuthenticationContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the authentication result without triggering a new authentication flow.
+ context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification);
+
if (notification.IsRequestHandled)
{
context.HandleRequest();
@@ -722,7 +726,7 @@ namespace OpenIddict.Server
}
// Attach the security principal extracted from the token to the validation context.
- context.Principal = notification.Principal;
+ context.Principal = notification.GenericTokenPrincipal;
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
new file mode 100644
index 00000000..1590da3d
--- /dev/null
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
@@ -0,0 +1,1543 @@
+/*
+ * 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;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.IdentityModel.JsonWebTokens;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Abstractions;
+using static OpenIddict.Abstractions.OpenIddictConstants;
+using static OpenIddict.Server.OpenIddictServerEvents;
+using static OpenIddict.Server.OpenIddictServerHandlerFilters;
+using SR = OpenIddict.Abstractions.OpenIddictResources;
+
+namespace OpenIddict.Server
+{
+ public static partial class OpenIddictServerHandlers
+ {
+ public static class Protection
+ {
+ public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create(
+ /*
+ * Token validation:
+ */
+ ResolveTokenValidationParameters.Descriptor,
+ ValidateReferenceTokenIdentifier.Descriptor,
+ ValidateIdentityModelToken.Descriptor,
+ NormalizeScopeClaims.Descriptor,
+ MapInternalClaims.Descriptor,
+ RestoreReferenceTokenProperties.Descriptor,
+ ValidatePrincipal.Descriptor,
+ ValidateTokenEntry.Descriptor,
+ ValidateAuthorizationEntry.Descriptor,
+ ValidateExpirationDate.Descriptor,
+
+ /*
+ * Token generation:
+ */
+ AttachSecurityCredentials.Descriptor,
+ CreateTokenEntry.Descriptor,
+ GenerateIdentityModelToken.Descriptor,
+ ConvertReferenceToken.Descriptor,
+ BeautifyToken.Descriptor);
+
+ ///
+ /// Contains the logic responsible of resolving the validation parameters used to validate tokens.
+ ///
+ public class ResolveTokenValidationParameters : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var parameters = context.Options.TokenValidationParameters.Clone();
+ parameters.ValidIssuer ??= context.Issuer?.AbsoluteUri;
+ parameters.ValidateIssuer = !string.IsNullOrEmpty(parameters.ValidIssuer);
+
+ parameters.ValidTypes = context.ValidTokenTypes.Count switch
+ {
+ // If no specific token type is expected, accept all token types at this stage.
+ // Additional filtering can be made based on the resolved/actual token type.
+ 0 => null,
+
+ // Otherwise, map the token types to their JWT public or internal representation.
+ _ => context.ValidTokenTypes.SelectMany(type => type switch
+ {
+ // For access tokens, both "at+jwt" and "application/at+jwt" are valid.
+ TokenTypeHints.AccessToken => new[]
+ {
+ JsonWebTokenTypes.AccessToken,
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
+ },
+
+ // For identity tokens, both "JWT" and "application/jwt" are valid.
+ TokenTypeHints.IdToken => new[]
+ {
+ JsonWebTokenTypes.IdentityToken,
+ JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken
+ },
+
+ // For authorization codes, only the short "oi_auc+jwt" form is valid.
+ TokenTypeHints.AuthorizationCode => new[] { JsonWebTokenTypes.Private.AuthorizationCode },
+
+ // For device codes, only the short "oi_dvc+jwt" form is valid.
+ TokenTypeHints.DeviceCode => new[] { JsonWebTokenTypes.Private.DeviceCode },
+
+ // For refresh tokens, only the short "oi_reft+jwt" form is valid.
+ TokenTypeHints.RefreshToken => new[] { JsonWebTokenTypes.Private.RefreshToken },
+
+ // For user codes, only the short "oi_usrc+jwt" form is valid.
+ TokenTypeHints.UserCode => new[] { JsonWebTokenTypes.Private.UserCode },
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ })
+ };
+
+ context.SecurityTokenHandler = context.Options.JsonWebTokenHandler;
+ context.TokenValidationParameters = parameters;
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of validating reference token identifiers.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ValidateReferenceTokenIdentifier : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictTokenManager _tokenManager;
+
+ public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager)
+ => _tokenManager = tokenManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ public async ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var token = context.Token.Length switch
+ {
+ // 12 may correspond to a normalized user code and 43 to any
+ // other base64url-encoded 256-bit reference token identifier.
+ 12 or 43 => await _tokenManager.FindByReferenceIdAsync(context.Token),
+
+ // A value higher than 12 (but lower than 50) may correspond to a user code
+ // containing dashes or any other non-digit character added by the end user.
+ // In this case, normalize the reference identifier before making the database lookup.
+ > 12 and < 50 => await _tokenManager.FindByReferenceIdAsync(NormalizeUserCode(context.Token)),
+
+ // If the token length differs, the token cannot be a reference token.
+ _ => null
+ };
+
+ // If the reference token cannot be found, don't return an error to allow another handler to validate it.
+ if (token is null)
+ {
+ return;
+ }
+
+ // If the type associated with the token entry doesn't match one of the expected types, return an error.
+ if (context.ValidTokenTypes.Count > 0 &&
+ !await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray()))
+ {
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2003),
+
+ _ => SR.GetResourceString(SR.ID2004)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2003),
+
+ _ => SR.FormatID8000(SR.ID2004),
+ });
+
+ return;
+ }
+
+ var payload = await _tokenManager.GetPayloadAsync(token);
+ if (string.IsNullOrEmpty(payload))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0026));
+ }
+
+ // Replace the token parameter by the payload resolved from the token entry
+ // and store the identifier of the reference token so it can be later
+ // used to restore the properties associated with the token.
+ context.Token = payload;
+ context.TokenId = await _tokenManager.GetIdAsync(token);
+
+ // Note: unlike other tokens, user codes may be potentially entered manually by users in a web form.
+ // To make that easier, user codes are generally "beautified" by adding intermediate dashes to
+ // make them easier to read and type. Since these additional characters are not part of the original
+ // user codes, non-digit characters are filtered from the reference identifier using this local method.
+ static string NormalizeUserCode(string token)
+ {
+ var builder = new StringBuilder(token);
+ for (var index = builder.Length - 1; index >= 0; index--)
+ {
+ var character = builder[index];
+ if (character < '0' || character > '9')
+ {
+ builder.Remove(index, 1);
+ }
+ }
+
+ return builder.ToString();
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of validating tokens generated using IdentityModel.
+ ///
+ public class ValidateIdentityModelToken : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If a principal was already attached, don't overwrite it.
+ if (context.Principal is not null)
+ {
+ return default;
+ }
+
+ // If the token cannot be read, don't return an error to allow another handler to validate it.
+ if (!context.SecurityTokenHandler.CanReadToken(context.Token))
+ {
+ return default;
+ }
+
+ // Special endpoints like introspection or revocation use a single parameter to convey
+ // multiple types of tokens (typically but not limited to access and refresh tokens).
+ //
+ // To speed up the token resolution process, the client can send a "token_type_hint"
+ // containing the type of the token: if the parameter doesn't match the actual type,
+ // the authorization server MUST use a fallback mechanism to determine whether the
+ // token can be introspected or revoked even if it's of a different type.
+ //
+ // This logic is not used by OpenIddict for IdentityModel tokens, as processing
+ // tokens of different type doesn't require re-parsing and re-validating them
+ // multiple times. As such, the "token_type_hint" parameter is only used in the
+ // Data Protection integration package and is ignored for IdentityModel tokens.
+ //
+ // For more information, see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
+ // and https://datatracker.ietf.org/doc/html/rfc7662#section-2.1.
+
+ var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters);
+ if (!result.IsValid)
+ {
+ context.Logger.LogTrace(result.Exception, SR.GetResourceString(SR.ID6000), context.Token);
+
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: result.Exception switch
+ {
+ SecurityTokenInvalidTypeException => context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2005),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2006),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2007),
+
+ OpenIddictServerEndpointType.Userinfo => SR.GetResourceString(SR.ID2008),
+
+ _ => SR.GetResourceString(SR.ID2089)
+ },
+
+ SecurityTokenInvalidIssuerException => SR.GetResourceString(SR.ID2088),
+ SecurityTokenSignatureKeyNotFoundException => SR.GetResourceString(SR.ID2090),
+ SecurityTokenInvalidSignatureException => SR.GetResourceString(SR.ID2091),
+
+ _ => SR.GetResourceString(SR.ID2004)
+ },
+ uri: result.Exception switch
+ {
+ SecurityTokenInvalidTypeException => context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2005),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2006),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2007),
+
+ OpenIddictServerEndpointType.Userinfo => SR.FormatID8000(SR.ID2008),
+
+ _ => SR.FormatID8000(SR.ID2089)
+ },
+
+ SecurityTokenInvalidIssuerException => SR.FormatID8000(SR.ID2088),
+ SecurityTokenSignatureKeyNotFoundException => SR.FormatID8000(SR.ID2090),
+ SecurityTokenInvalidSignatureException => SR.FormatID8000(SR.ID2091),
+
+ _ => SR.FormatID8000(SR.ID2004)
+ });
+
+ return default;
+ }
+
+ // Get the JWT token. If the token is encrypted using JWE, retrieve the inner token.
+ var token = (JsonWebToken) result.SecurityToken;
+ if (token.InnerToken is not null)
+ {
+ token = token.InnerToken;
+ }
+
+ // 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(result.ClaimsIdentity).SetTokenType(result.TokenType switch
+ {
+ null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
+
+ // Both at+jwt and application/at+jwt are supported for access tokens.
+ JsonWebTokenTypes.AccessToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.AccessToken
+ => TokenTypeHints.AccessToken,
+
+ // Both JWT and application/JWT are supported for identity tokens.
+ JsonWebTokenTypes.IdentityToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.IdentityToken
+ => TokenTypeHints.IdToken,
+
+ JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeHints.AuthorizationCode,
+ JsonWebTokenTypes.Private.DeviceCode => TokenTypeHints.DeviceCode,
+ JsonWebTokenTypes.Private.RefreshToken => TokenTypeHints.RefreshToken,
+ JsonWebTokenTypes.Private.UserCode => TokenTypeHints.UserCode,
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ });
+
+ // Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object).
+ if (token.TryGetPayloadValue(Claims.Private.ClaimDestinationsMap, out ImmutableDictionary destinations))
+ {
+ context.Principal.SetDestinations(destinations);
+ }
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6001), context.Token, context.Principal.Claims);
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of normalizing the scope claims stored in the tokens.
+ ///
+ public class NormalizeScopeClaims : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.Principal is null)
+ {
+ return default;
+ }
+
+ // Note: in previous OpenIddict versions, scopes were represented as a JSON array
+ // and deserialized as multiple claims. In OpenIddict 3.0, the public "scope" claim
+ // is formatted as a unique space-separated string containing all the granted scopes.
+ // To ensure access tokens generated by previous versions are still correctly handled,
+ // both formats (unique space-separated string or multiple scope claims) must be supported.
+ // To achieve that, all the "scope" claims are combined into a single one containg all the values.
+ // Visit https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information.
+ var scopes = context.Principal.GetClaims(Claims.Scope);
+ if (scopes.Length > 1)
+ {
+ context.Principal.SetClaim(Claims.Scope, string.Join(" ", scopes));
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of mapping internal claims used by OpenIddict.
+ ///
+ public class MapInternalClaims : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(NormalizeScopeClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.Principal is null)
+ {
+ return default;
+ }
+
+ // To reduce the size of tokens, some of the private claims used by OpenIddict
+ // are mapped to their standard equivalent before being removed from the token.
+ // This handler is responsible of adding back the private claims to the principal
+ // when receiving the token (e.g "oi_prst" is resolved from the "scope" claim).
+
+ // In OpenIddict 3.0, the creation date of a token is stored in "oi_crt_dt".
+ // If the claim doesn't exist, try to infer it from the standard "iat" JWT claim.
+ if (!context.Principal.HasClaim(Claims.Private.CreationDate))
+ {
+ var date = context.Principal.GetClaim(Claims.IssuedAt);
+ if (!string.IsNullOrEmpty(date) &&
+ long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ context.Principal.SetCreationDate(DateTimeOffset.FromUnixTimeSeconds(value));
+ }
+ }
+
+ // In OpenIddict 3.0, the expiration date of a token is stored in "oi_exp_dt".
+ // If the claim doesn't exist, try to infer it from the standard "exp" JWT claim.
+ if (!context.Principal.HasClaim(Claims.Private.ExpirationDate))
+ {
+ var date = context.Principal.GetClaim(Claims.ExpiresAt);
+ if (!string.IsNullOrEmpty(date) &&
+ long.TryParse(date, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+ {
+ context.Principal.SetExpirationDate(DateTimeOffset.FromUnixTimeSeconds(value));
+ }
+ }
+
+ // In OpenIddict 3.0, the audiences allowed to receive a token are stored in "oi_aud".
+ // If no such claim exists, try to infer them from the standard "aud" JWT claims.
+ if (!context.Principal.HasClaim(Claims.Private.Audience))
+ {
+ var audiences = context.Principal.GetClaims(Claims.Audience);
+ if (audiences.Any())
+ {
+ context.Principal.SetAudiences(audiences);
+ }
+ }
+
+ // In OpenIddict 3.0, the presenters allowed to use a token are stored in "oi_prst".
+ // If no such claim exists, try to infer them from the standard "azp" and "client_id" JWT claims.
+ //
+ // Note: in previous OpenIddict versions, the presenters were represented in JWT tokens
+ // using the "azp" claim (defined by OpenID Connect), for which a single value could be
+ // specified. To ensure presenters stored in JWT tokens created by OpenIddict 1.x/2.x
+ // can still be read with OpenIddict 3.0, the presenter is automatically inferred from
+ // the "azp" or "client_id" claim if no "oi_prst" claim was found in the principal.
+ if (!context.Principal.HasClaim(Claims.Private.Presenter))
+ {
+ var presenter = context.Principal.GetClaim(Claims.AuthorizedParty) ??
+ context.Principal.GetClaim(Claims.ClientId);
+
+ if (!string.IsNullOrEmpty(presenter))
+ {
+ context.Principal.SetPresenters(presenter);
+ }
+ }
+
+ // In OpenIddict 3.0, the scopes granted to an application are stored in "oi_scp".
+ // If no such claim exists, try to infer them from the standard "scope" JWT claim,
+ // which is guaranteed to be a unique space-separated claim containing all the values.
+ if (!context.Principal.HasClaim(Claims.Private.Scope))
+ {
+ var scope = context.Principal.GetClaim(Claims.Scope);
+ if (!string.IsNullOrEmpty(scope))
+ {
+ context.Principal.SetScopes(scope.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries));
+ }
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of restoring the properties associated with a reference token entry.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class RestoreReferenceTokenProperties : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictTokenManager _tokenManager;
+
+ public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager)
+ => _tokenManager = tokenManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(MapInternalClaims.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ public async ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.Principal is null || string.IsNullOrEmpty(context.TokenId))
+ {
+ return;
+ }
+
+ var token = await _tokenManager.FindByIdAsync(context.TokenId);
+ if (token is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
+ }
+
+ // Restore the creation/expiration dates/identifiers from the token entry metadata.
+ context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
+ .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
+ .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
+ .SetTokenId(await _tokenManager.GetIdAsync(token))
+ .SetTokenType(await _tokenManager.GetTypeAsync(token));
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved.
+ ///
+ public class ValidatePrincipal : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (context.Principal is null)
+ {
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout
+ => SR.GetResourceString(SR.ID2009),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2003),
+
+ _ => SR.GetResourceString(SR.ID2004)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout
+ => SR.FormatID8000(SR.ID2009),
+
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2003),
+
+ _ => SR.FormatID8000(SR.ID2004)
+ });
+
+
+ return default;
+ }
+
+ // When using JWT or Data Protection tokens, the correct token type is always enforced by IdentityModel
+ // (using the "typ" header) or by ASP.NET Core Data Protection (using per-token-type purposes strings).
+ // To ensure tokens deserialized using a custom routine are of the expected type, a manual check is used,
+ // which requires that a special claim containing the token type be present in the security principal.
+ if (context.ValidTokenTypes.Count > 0)
+ {
+ var type = context.Principal.GetTokenType();
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0004));
+ }
+
+ if (!context.ValidTokenTypes.Contains(type))
+ {
+ throw new InvalidOperationException(SR.FormatID0005(type, string.Join(", ", context.ValidTokenTypes)));
+ }
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of rejecting authentication demands that
+ /// use a token whose entry is no longer valid (e.g was revoked).
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ValidateTokenEntry : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictTokenManager _tokenManager;
+
+ public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateTokenEntry(IOpenIddictTokenManager tokenManager)
+ => _tokenManager = tokenManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidatePrincipal.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ public async ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // Extract the token identifier from the authentication principal.
+ // If no token identifier can be found, this indicates that the token
+ // has no backing database entry (e.g an access token or an identity token).
+ var identifier = context.Principal.GetTokenId();
+ if (string.IsNullOrEmpty(identifier))
+ {
+ return;
+ }
+
+ // If the token entry cannot be found, return a generic error.
+ var token = await _tokenManager.FindByIdAsync(identifier);
+ if (token is null)
+ {
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2003),
+
+ _ => SR.GetResourceString(SR.ID2004)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2001),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2002),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2003),
+
+ _ => SR.FormatID8000(SR.ID2004)
+ });
+
+ return;
+ }
+
+ if (context.EndpointType == OpenIddictServerEndpointType.Token && (context.Request.IsAuthorizationCodeGrantType() ||
+ context.Request.IsDeviceCodeGrantType() ||
+ context.Request.IsRefreshTokenGrantType()))
+ {
+ // If the authorization code/device code/refresh token is already marked as redeemed, this may indicate
+ // that it was compromised. In this case, revoke the entire chain of tokens associated with the authorization.
+ // Special logic is used to avoid revoking refresh tokens already marked as redeemed to allow for a small leeway.
+ // Note: the authorization itself is not revoked to allow the legitimate client to start a new flow.
+ // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information.
+ if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed))
+ {
+ if (!context.Request.IsRefreshTokenGrantType() || !await IsReusableAsync(token))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), identifier);
+
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2010),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2011),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2012),
+
+ _ => SR.GetResourceString(SR.ID2013)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2010),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2011),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2012),
+
+ _ => SR.FormatID8000(SR.ID2013)
+ });
+
+ // Revoke all the token entries associated with the authorization.
+ await TryRevokeChainAsync(await _tokenManager.GetAuthorizationIdAsync(token));
+
+ return;
+ }
+
+ return;
+ }
+
+ if (context.Request.IsDeviceCodeGrantType())
+ {
+ // If the device code is not marked as valid yet, return an authorization_pending error.
+ if (await _tokenManager.HasStatusAsync(token, Statuses.Inactive))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6003), identifier);
+
+ context.Reject(
+ error: Errors.AuthorizationPending,
+ description: SR.GetResourceString(SR.ID2014),
+ uri: SR.FormatID8000(SR.ID2014));
+
+ return;
+ }
+
+ // If the device code is marked as rejected, return an access_denied error.
+ if (await _tokenManager.HasStatusAsync(token, Statuses.Rejected))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6004), identifier);
+
+ context.Reject(
+ error: Errors.AccessDenied,
+ description: SR.GetResourceString(SR.ID2015),
+ uri: SR.FormatID8000(SR.ID2015));
+
+ return;
+ }
+ }
+ }
+
+ if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier);
+
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2016),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2017),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2018),
+
+ _ => SR.GetResourceString(SR.ID2019)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2016),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2017),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2018),
+
+ _ => SR.FormatID8000(SR.ID2019)
+ });
+
+ return;
+ }
+
+ // Restore the creation/expiration dates/identifiers from the token entry metadata.
+ context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
+ .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
+ .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
+ .SetTokenId(await _tokenManager.GetIdAsync(token))
+ .SetTokenType(await _tokenManager.GetTypeAsync(token));
+
+ async ValueTask IsReusableAsync(object token)
+ {
+ // If the reuse leeway was set to null, return false to indicate
+ // that the refresh token is already redeemed and cannot be reused.
+ if (context.Options.RefreshTokenReuseLeeway is null)
+ {
+ return false;
+ }
+
+ var date = await _tokenManager.GetRedemptionDateAsync(token);
+ if (date is null || DateTimeOffset.UtcNow < date + context.Options.RefreshTokenReuseLeeway)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ async ValueTask TryRevokeChainAsync(string? identifier)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ return;
+ }
+
+ // Revoke all the token entries associated with the authorization,
+ // including the redeemed token that was used in the token request.
+ await foreach (var token in _tokenManager.FindByAuthorizationIdAsync(identifier))
+ {
+ await _tokenManager.TryRevokeAsync(token);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of authentication demands a token whose
+ /// associated authorization entry is no longer valid (e.g was revoked).
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ValidateAuthorizationEntry : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictAuthorizationManager _authorizationManager;
+
+ public ValidateAuthorizationEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ValidateAuthorizationEntry(IOpenIddictAuthorizationManager authorizationManager)
+ => _authorizationManager = authorizationManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ public async ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ var identifier = context.Principal.GetAuthorizationId();
+ if (string.IsNullOrEmpty(identifier))
+ {
+ return;
+ }
+
+ var authorization = await _authorizationManager.FindByIdAsync(identifier);
+ if (authorization is null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid))
+ {
+ context.Logger.LogInformation(SR.GetResourceString(SR.ID6006), identifier);
+
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2020),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2021),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2022),
+
+ _ => SR.GetResourceString(SR.ID2023)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2020),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2021),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2022),
+
+ _ => SR.FormatID8000(SR.ID2023)
+ });
+
+ return;
+ }
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of rejecting authentication demands that use an expired token.
+ ///
+ public class ValidateExpirationDate : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateTokenEntry.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(ValidateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
+
+ // Don't validate the lifetime of id_tokens used as id_token_hints.
+ if (context.ValidTokenTypes.Count is 1 && context.ValidTokenTypes.ElementAt(0) is TokenTypeHints.IdToken &&
+ context.EndpointType is OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout)
+ {
+ return default;
+ }
+
+ var date = context.Principal.GetExpirationDate();
+ if (date.HasValue && date.Value < DateTimeOffset.UtcNow)
+ {
+ context.Reject(
+ error: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => Errors.ExpiredToken,
+
+ OpenIddictServerEndpointType.Token => Errors.InvalidGrant,
+
+ _ => Errors.InvalidToken
+ },
+ description: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.GetResourceString(SR.ID2016),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.GetResourceString(SR.ID2017),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.GetResourceString(SR.ID2018),
+
+ _ => SR.GetResourceString(SR.ID2019)
+ },
+ uri: context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
+ => SR.FormatID8000(SR.ID2016),
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
+ => SR.FormatID8000(SR.ID2017),
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
+ => SR.FormatID8000(SR.ID2018),
+
+ _ => SR.FormatID8000(SR.ID2019)
+ });
+
+ return default;
+ }
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of resolving the signing and encryption credentials used to protect tokens.
+ ///
+ public class AttachSecurityCredentials : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(int.MinValue + 100_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ context.SecurityTokenHandler = context.Options.JsonWebTokenHandler;
+
+ context.EncryptionCredentials = context.TokenType switch
+ {
+ // Note: unlike other tokens, encryption can be disabled for access tokens.
+ TokenTypeHints.AccessToken when context.Options.DisableAccessTokenEncryption => null,
+ TokenTypeHints.IdToken => null,
+
+ _ => context.Options.EncryptionCredentials.First()
+ };
+
+ context.SigningCredentials = context.TokenType switch
+ {
+ // Note: unlike other tokens, identity tokens can only be signed using an asymmetric key
+ // as they are meant to be validated by clients using the public keys exposed by the server.
+ TokenTypeHints.IdToken => context.Options.SigningCredentials.First(credentials =>
+ credentials.Key is AsymmetricSecurityKey),
+
+ _ => context.Options.SigningCredentials.First()
+ };
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of creating a token entry.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class CreateTokenEntry : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictApplicationManager _applicationManager;
+ private readonly IOpenIddictTokenManager _tokenManager;
+
+ public CreateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public CreateTokenEntry(
+ IOpenIddictApplicationManager applicationManager,
+ IOpenIddictTokenManager tokenManager)
+ {
+ _applicationManager = applicationManager;
+ _tokenManager = tokenManager;
+ }
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ var descriptor = new OpenIddictTokenDescriptor
+ {
+ AuthorizationId = context.Principal.GetAuthorizationId(),
+ CreationDate = context.Principal.GetCreationDate(),
+ ExpirationDate = context.Principal.GetExpirationDate(),
+ Principal = context.Principal,
+ Type = context.TokenType
+ };
+
+ descriptor.Status = context.TokenType switch
+ {
+ // When initially created, device codes are marked as inactive. When the user
+ // approves the authorization demand, the UpdateReferenceDeviceCodeEntry handler
+ // changes the status to "active" and attaches a new payload with the claims
+ // corresponding the user, which allows the client to redeem the device code.
+ TokenTypeHints.DeviceCode => Statuses.Inactive,
+
+ // For all other tokens, "valid" is the default status.
+ _ => Statuses.Valid
+ };
+
+ descriptor.Subject = context.TokenType switch
+ {
+ // Device and user codes are not bound to a user, until authorization is granted.
+ TokenTypeHints.DeviceCode or TokenTypeHints.UserCode => null,
+
+ // For all other tokens, the subject is resolved from the principal.
+ _ => context.Principal.GetClaim(Claims.Subject)
+ };
+
+ // If the client application is known, associate it with the token.
+ if (!string.IsNullOrEmpty(context.Request.ClientId))
+ {
+ var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId);
+ if (application is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
+ }
+
+ descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
+ }
+
+ var token = await _tokenManager.CreateAsync(descriptor);
+ if (token is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0019));
+ }
+
+ var identifier = await _tokenManager.GetIdAsync(token);
+
+ // Attach the token identifier to the principal so that it can be stored in the token.
+ context.Principal.SetTokenId(identifier);
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6012), context.TokenType, identifier);
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of generating a token using IdentityModel.
+ ///
+ public class GenerateIdentityModelToken : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(CreateTokenEntry.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // If a token was already attached by another handler, don't overwrite it.
+ if (!string.IsNullOrEmpty(context.Token))
+ {
+ return default;
+ }
+
+ if (context.Principal is null or { Identity: not ClaimsIdentity })
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0022));
+ }
+
+ // Clone the principal and exclude the private claims mapped to standard JWT claims.
+ var principal = context.Principal.Clone(claim => claim.Type switch
+ {
+ Claims.Private.CreationDate or Claims.Private.ExpirationDate or Claims.Private.TokenType => false,
+
+ Claims.Private.Audience
+ when context.TokenType is TokenTypeHints.AccessToken or TokenTypeHints.IdToken => false,
+
+ Claims.Private.Scope when context.TokenType is TokenTypeHints.AccessToken => false,
+
+ _ => true
+ });
+
+ if (principal is null or { Identity: not ClaimsIdentity })
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0020));
+ }
+
+ var claims = new Dictionary(StringComparer.Ordinal);
+
+ // For access and identity tokens, set the public audience claims
+ // using the private audience claims from the security principal.
+ if (context.TokenType is TokenTypeHints.AccessToken or TokenTypeHints.IdToken)
+ {
+ var audiences = context.Principal.GetAudiences();
+ if (audiences.Any())
+ {
+ claims.Add(Claims.Audience, audiences.Length switch
+ {
+ 1 => audiences.ElementAt(0),
+ _ => audiences
+ });
+ }
+ }
+
+ // For access tokens, set the public scope claim using the private scope claims from the principal.
+ // Note: scopes are deliberately formatted as a single space-separated
+ // string to respect the usual representation of the standard scope claim.
+ // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04.
+ if (context.TokenType is TokenTypeHints.AccessToken)
+ {
+ var scopes = context.Principal.GetScopes();
+ if (scopes.Any())
+ {
+ claims.Add(Claims.Scope, string.Join(" ", scopes));
+ }
+ }
+
+ // For authorization/device/user codes and refresh tokens,
+ // attach claims destinations to the JWT claims collection.
+ if (context.TokenType is TokenTypeHints.AuthorizationCode or TokenTypeHints.DeviceCode or
+ TokenTypeHints.RefreshToken or TokenTypeHints.UserCode)
+ {
+ var destinations = principal.GetDestinations();
+ if (destinations.Count != 0)
+ {
+ claims.Add(Claims.Private.ClaimDestinationsMap, destinations);
+ }
+ }
+
+ var descriptor = new SecurityTokenDescriptor
+ {
+ Claims = claims,
+ EncryptingCredentials = context.EncryptionCredentials,
+ Expires = context.Principal.GetExpirationDate()?.UtcDateTime,
+ IssuedAt = context.Principal.GetCreationDate()?.UtcDateTime,
+ Issuer = context.Issuer?.AbsoluteUri,
+ SigningCredentials = context.SigningCredentials,
+ Subject = (ClaimsIdentity) principal.Identity,
+ TokenType = context.TokenType switch
+ {
+ null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)),
+
+ TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken,
+ TokenTypeHints.IdToken => JsonWebTokenTypes.IdentityToken,
+ TokenTypeHints.AuthorizationCode => JsonWebTokenTypes.Private.AuthorizationCode,
+ TokenTypeHints.DeviceCode => JsonWebTokenTypes.Private.DeviceCode,
+ TokenTypeHints.RefreshToken => JsonWebTokenTypes.Private.RefreshToken,
+ TokenTypeHints.UserCode => JsonWebTokenTypes.Private.UserCode,
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ }
+ };
+
+ context.Token = context.SecurityTokenHandler.CreateToken(descriptor);
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, context.Token, principal.Claims);
+
+ return default;
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of converting the token to a reference token.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class ConvertReferenceToken : IOpenIddictServerHandler
+ {
+ private readonly IOpenIddictTokenManager _tokenManager;
+
+ public ConvertReferenceToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
+
+ public ConvertReferenceToken(IOpenIddictTokenManager tokenManager)
+ => _tokenManager = tokenManager;
+
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ .AddFilter()
+ .AddFilter()
+ .UseScopedHandler()
+ .SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public async ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (!(context.TokenType switch
+ {
+ // Access and refresh tokens can be converted to reference tokens
+ // if the corresponding option was enabled in the server options.
+ TokenTypeHints.AccessToken => context.Options.UseReferenceAccessTokens,
+ TokenTypeHints.RefreshToken => context.Options.UseReferenceRefreshTokens,
+
+ // By default, authorization/user codes are always converted to reference tokens.
+ TokenTypeHints.AuthorizationCode or TokenTypeHints.UserCode => true,
+
+ // Device codes are only converted to reference tokens if they are not generated
+ // as part of a device code swap made by the user code verification endpoint.
+ TokenTypeHints.DeviceCode => context.EndpointType is not OpenIddictServerEndpointType.Verification,
+
+ // Identity tokens cannot be converted to reference tokens.
+ TokenTypeHints.IdToken => false,
+
+ _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003))
+ }))
+ {
+ return;
+ }
+
+ var identifier = context.Principal.GetTokenId();
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0009));
+ }
+
+ var token = await _tokenManager.FindByIdAsync(identifier);
+ if (token is null)
+ {
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
+ }
+
+ var descriptor = new OpenIddictTokenDescriptor();
+ await _tokenManager.PopulateAsync(descriptor, token);
+
+ // Attach the generated token to the token entry.
+ descriptor.Payload = context.Token;
+ descriptor.Principal = context.Principal;
+
+ // Note: unlike other reference tokens, user codes are meant to be used by humans,
+ // who may have to enter it in a web form. To ensure it remains easy enough to type
+ // even by users with non-Latin keyboards, user codes generated by OpenIddict are
+ // only compound of 12 digits, generated using a crypto-secure random number generator.
+ // In this case, the resulting user code is estimated to have at most ~40 bits of entropy.
+ if (context.TokenType is TokenTypeHints.UserCode)
+ {
+ do
+ {
+ var data = new byte[12];
+#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
+ RandomNumberGenerator.Fill(data);
+#else
+ using var generator = RandomNumberGenerator.Create();
+ generator.GetBytes(data);
+#endif
+ var builder = new StringBuilder(data.Length);
+
+ for (var index = 0; index < data.Length; index += 4)
+ {
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(data, index) % 10000);
+ }
+
+ descriptor.ReferenceId = builder.ToString();
+ }
+
+ // User codes are relatively short. To help reduce the risks of collisions with
+ // existing entries, a database check is performed here before updating the entry.
+ while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null);
+ }
+
+ // For other tokens, generate a base64url-encoded 256-bit random identifier.
+ else
+ {
+ var data = new byte[256 / 8];
+#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
+ RandomNumberGenerator.Fill(data);
+#else
+ using var generator = RandomNumberGenerator.Create();
+ generator.GetBytes(data);
+#endif
+
+ descriptor.ReferenceId = Base64UrlEncoder.Encode(data);
+ }
+
+ await _tokenManager.UpdateAsync(token, descriptor);
+
+ // Replace the returned token by the reference identifier.
+ context.Token = descriptor.ReferenceId;
+
+ context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.TokenType, identifier, descriptor.ReferenceId);
+ }
+ }
+
+ ///
+ /// Contains the logic responsible of beautifying user-typed tokens.
+ /// Note: this handler is not used when the degraded mode is enabled.
+ ///
+ public class BeautifyToken : IOpenIddictServerHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictServerHandlerDescriptor Descriptor { get; }
+ = OpenIddictServerHandlerDescriptor.CreateBuilder()
+ // Technically, this handler doesn't require that the degraded mode be disabled
+ // but the default CreateReferenceEntry handler that creates the user code
+ // reference identifiers only works when the degraded mode is disabled.
+ .AddFilter()
+ .UseSingletonHandler()
+ .SetOrder(ConvertReferenceToken.Descriptor.Order + 1_000)
+ .SetType(OpenIddictServerHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ public ValueTask HandleAsync(GenerateTokenContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // To make user codes easier to read and type by humans, a dash is automatically
+ // appended before each new block of 4 integers. These dashes are expected to be
+ // stripped from the user codes when receiving them at the verification endpoint.
+ if (context.TokenType is TokenTypeHints.UserCode)
+ {
+ var builder = new StringBuilder(context.Token);
+ if (builder.Length % 4 != 0)
+ {
+ return default;
+ }
+
+ for (var index = builder.Length; index >= 0; index -= 4)
+ {
+ if (index != 0 && index != builder.Length)
+ {
+ builder.Insert(index, Separators.Dash[0]);
+ }
+ }
+
+ context.Token = builder.ToString();
+ }
+
+ return default;
+ }
+ }
+ }
+ }
+}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
index 366b0ddb..ba7eff0d 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
@@ -643,6 +643,10 @@ namespace OpenIddict.Server
var notification = new ProcessAuthenticationContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the authentication result without triggering a new authentication flow.
+ context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification);
+
if (notification.IsRequestHandled)
{
context.HandleRequest();
@@ -665,7 +669,7 @@ namespace OpenIddict.Server
}
// Attach the security principal extracted from the token to the validation context.
- context.Principal = notification.Principal;
+ context.Principal = notification.GenericTokenPrincipal;
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
index e97b547f..c91b9022 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
@@ -380,6 +380,10 @@ namespace OpenIddict.Server
var notification = new ProcessAuthenticationContext(context.Transaction);
await _dispatcher.DispatchAsync(notification);
+ // Store the context object in the transaction so it can be later retrieved by handlers
+ // that want to access the authentication result without triggering a new authentication flow.
+ context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, notification);
+
if (notification.IsRequestHandled)
{
context.HandleRequest();
@@ -402,7 +406,7 @@ namespace OpenIddict.Server
}
// Attach the security principal extracted from the token to the validation context.
- context.Principal = notification.Principal;
+ context.Principal = notification.AccessTokenPrincipal;
}
}
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 1e2238ad..6b425d35 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -9,20 +9,17 @@ using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
-using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
-using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlerFilters;
-using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties;
using SR = OpenIddict.Abstractions.OpenIddictResources;
namespace OpenIddict.Server
@@ -35,17 +32,15 @@ namespace OpenIddict.Server
* Authentication processing:
*/
ValidateAuthenticationDemand.Descriptor,
- ValidateTokenParameter.Descriptor,
- NormalizeUserCode.Descriptor,
- ValidateReferenceTokenIdentifier.Descriptor,
- ValidateIdentityModelToken.Descriptor,
- NormalizeScopeClaims.Descriptor,
- MapInternalClaims.Descriptor,
- RestoreReferenceTokenProperties.Descriptor,
- ValidatePrincipal.Descriptor,
- ValidateTokenEntry.Descriptor,
- ValidateAuthorizationEntry.Descriptor,
- ValidateExpirationDate.Descriptor,
+ EvaluateValidatedTokens.Descriptor,
+ ResolveValidatedTokens.Descriptor,
+ ValidateAccessToken.Descriptor,
+ ValidateAuthorizationCode.Descriptor,
+ ValidateDeviceCode.Descriptor,
+ ValidateGenericToken.Descriptor,
+ ValidateIdentityToken.Descriptor,
+ ValidateRefreshToken.Descriptor,
+ ValidateUserCode.Descriptor,
/*
* Challenge processing:
@@ -63,7 +58,7 @@ namespace OpenIddict.Server
AttachDefaultScopes.Descriptor,
AttachDefaultPresenters.Descriptor,
InferResources.Descriptor,
- EvaluateTokenTypes.Descriptor,
+ EvaluateGeneratedTokens.Descriptor,
AttachAuthorization.Descriptor,
PrepareAccessTokenPrincipal.Descriptor,
@@ -75,33 +70,18 @@ namespace OpenIddict.Server
RedeemTokenEntry.Descriptor,
- CreateAccessTokenEntry.Descriptor,
- GenerateIdentityModelAccessToken.Descriptor,
- ConvertReferenceAccessToken.Descriptor,
+ GenerateAccessToken.Descriptor,
+ GenerateAuthorizationCode.Descriptor,
+ GenerateDeviceCode.Descriptor,
+ GenerateRefreshToken.Descriptor,
- CreateAuthorizationCodeEntry.Descriptor,
- GenerateIdentityModelAuthorizationCode.Descriptor,
- ConvertReferenceAuthorizationCode.Descriptor,
-
- CreateDeviceCodeEntry.Descriptor,
- GenerateIdentityModelDeviceCode.Descriptor,
- ConvertReferenceDeviceCode.Descriptor,
- UpdateReferenceDeviceCodeEntry.Descriptor,
-
- CreateRefreshTokenEntry.Descriptor,
- GenerateIdentityModelRefreshToken.Descriptor,
- ConvertReferenceRefreshToken.Descriptor,
-
- CreateUserCodeEntry.Descriptor,
AttachDeviceCodeIdentifier.Descriptor,
- GenerateIdentityModelUserCode.Descriptor,
- ConvertReferenceUserCode.Descriptor,
-
+ UpdateReferenceDeviceCodeEntry.Descriptor,
AttachTokenDigests.Descriptor,
- CreateIdentityTokenEntry.Descriptor,
- GenerateIdentityModelIdentityToken.Descriptor,
- BeautifyUserCode.Descriptor,
+ GenerateUserCode.Descriptor,
+ GenerateIdentityToken.Descriptor,
+
AttachTokenParameters.Descriptor,
/*
@@ -114,6 +94,7 @@ namespace OpenIddict.Server
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Introspection.DefaultHandlers)
+ .AddRange(Protection.DefaultHandlers)
.AddRange(Revocation.DefaultHandlers)
.AddRange(Session.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
@@ -163,16 +144,16 @@ namespace OpenIddict.Server
}
///
- /// Contains the logic responsible of resolving the token from the incoming request.
+ /// Contains the logic responsible of selecting the token types that should be validated.
///
- public class ValidateTokenParameter : IOpenIddictServerHandler
+ public class EvaluateValidatedTokens : IOpenIddictServerHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
- .UseSingletonHandler()
+ .UseSingletonHandler()
.SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -185,64 +166,81 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
- var (token, type) = context.EndpointType switch
+ (context.ValidateAccessToken, context.RequireAccessToken) = context.EndpointType switch
+ {
+ // The userinfo endpoint requires sending a valid access token.
+ OpenIddictServerEndpointType.Userinfo => (true, true),
+
+ _ => (false, false)
+ };
+
+ (context.ValidateAuthorizationCode, context.RequireAuthorizationCode) = context.EndpointType switch
+ {
+ // The authorization code grant requires sending a valid authorization code.
+ OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => (true, true),
+
+ _ => (false, false)
+ };
+
+ (context.ValidateDeviceCode, context.RequireDeviceCode) = context.EndpointType switch
{
- OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout
- => (context.Request.IdTokenHint, TokenTypeHints.IdToken),
+ // The device code grant requires sending a valid device code.
+ OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() => (true, true),
+
+ _ => (false, false)
+ };
+ (context.ValidateGenericToken, context.RequireGenericToken) = context.EndpointType switch
+ {
// Tokens received by the introspection and revocation endpoints can be of any type.
// Additional token type filtering is made by the endpoint themselves, if needed.
- OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation
- => (context.Request.Token, null),
-
- OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
- => (context.Request.Code, TokenTypeHints.AuthorizationCode),
- OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType()
- => (context.Request.DeviceCode, TokenTypeHints.DeviceCode),
- OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
- => (context.Request.RefreshToken, TokenTypeHints.RefreshToken),
+ OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation => (true, true),
- OpenIddictServerEndpointType.Userinfo => (context.Request.AccessToken, TokenTypeHints.AccessToken),
+ _ => (false, false)
+ };
- OpenIddictServerEndpointType.Verification => (context.Request.UserCode, TokenTypeHints.UserCode),
+ (context.ValidateIdentityToken, context.RequireIdentityToken) = context.EndpointType switch
+ {
+ // The identity token received by the authorization and logout
+ // endpoints are not required and serve as optional hints.
+ OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout => (true, false),
- _ => (null, null)
+ _ => (false, false)
};
- if (string.IsNullOrEmpty(token))
+ (context.ValidateRefreshToken, context.RequireRefreshToken) = context.EndpointType switch
{
- context.Reject(
- error: Errors.InvalidRequest,
- description: SR.GetResourceString(SR.ID2000),
- uri: SR.FormatID8000(SR.ID2000));
+ // The refresh token grant requires sending a valid refresh token.
+ OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => (true, true),
- return default;
- }
+ _ => (false, false)
+ };
- context.Token = token;
- context.TokenType = type;
+ (context.ValidateUserCode, context.RequireUserCode) = context.EndpointType switch
+ {
+ // Note: the verification endpoint can be accessed without specifying a
+ // user code (that can be later set by the user using a form, for instance).
+ OpenIddictServerEndpointType.Verification => (true, false),
+
+ _ => (false, false)
+ };
return default;
}
}
///
- /// Contains the logic responsible of normalizing user codes.
- /// Note: this handler is not used when the degraded mode is enabled.
+ /// Contains the logic responsible of resolving the token from the incoming request.
///
- public class NormalizeUserCode : IOpenIddictServerHandler
+ public class ResolveValidatedTokens : IOpenIddictServerHandler
{
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
- // Technically, this handler doesn't require that the degraded mode be disabled
- // but the default CreateReferenceUserCodeEntry that creates the user code
- // reference identifiers only works when the degraded mode is disabled.
- .AddFilter()
- .UseSingletonHandler()
- .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000)
+ .UseSingletonHandler()
+ .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@@ -254,57 +252,85 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
- if (!string.Equals(context.TokenType, TokenTypeHints.UserCode, StringComparison.OrdinalIgnoreCase))
+ context.AccessToken = context.EndpointType switch
{
- return default;
- }
+ OpenIddictServerEndpointType.Userinfo when context.ValidateAccessToken => context.Request.AccessToken,
+
+ _ => null
+ };
+
+ context.AuthorizationCode = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.ValidateAuthorizationCode => context.Request.Code,
- // Note: unlike other tokens, user codes may be potentially entered manually by users in a web form.
- // To make that easier, user codes are generally "beautified" by adding intermediate dashes to
- // make them easier to read and type. Since these additional characters are not part of the original
- // user codes, non-digit characters are automatically filtered from the reference identifier.
+ _ => null
+ };
- var builder = new StringBuilder(context.Token);
- for (var index = builder.Length - 1; index >= 0; index--)
+ context.DeviceCode = context.EndpointType switch
{
- var character = builder[index];
- if (character < '0' || character > '9')
- {
- builder.Remove(index, 1);
- }
- }
+ OpenIddictServerEndpointType.Token when context.ValidateDeviceCode => context.Request.DeviceCode,
+
+ _ => null
+ };
+
+ (context.GenericToken, context.GenericTokenTypeHint) = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Introspection or
+ OpenIddictServerEndpointType.Revocation
+ when context.ValidateGenericToken => (context.Request.Token, context.Request.TokenTypeHint),
+
+ _ => (null, null)
+ };
- context.Token = builder.ToString();
+ context.IdentityToken = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Authorization or
+ OpenIddictServerEndpointType.Logout
+ when context.ValidateIdentityToken => context.Request.IdTokenHint,
+
+ _ => null
+ };
+
+ context.RefreshToken = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Token when context.ValidateRefreshToken => context.Request.RefreshToken,
+
+ _ => null
+ };
+
+ context.UserCode = context.EndpointType switch
+ {
+ OpenIddictServerEndpointType.Verification when context.ValidateUserCode => context.Request.UserCode,
+
+ _ => null
+ };
return default;
}
}
///
- /// Contains the logic responsible of validating reference token identifiers.
- /// Note: this handler is not used when the degraded mode is enabled.
+ /// Contains the logic responsible of validating the access token resolved from the context.
///
- public class ValidateReferenceTokenIdentifier : IOpenIddictServerHandler
+ public class ValidateAccessToken : IOpenIddictServerHandler
{
- private readonly IOpenIddictTokenManager _tokenManager;
+ private readonly IOpenIddictServerDispatcher _dispatcher;
- public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
-
- public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager)
- => _tokenManager = tokenManager;
+ public ValidateAccessToken(IOpenIddictServerDispatcher dispatcher)
+ => _dispatcher = dispatcher;
///
/// Gets the default descriptor definition assigned to this handler.
///
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder()
- .AddFilter()
- .AddFilter