diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index f5979ba0..917f84f2 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -228,7 +228,7 @@ public static class OpenIddictConstants public static class JsonWebTokenTypes { public const string AccessToken = "at+jwt"; - public const string JsonWebToken = "JWT"; + public const string Jwt = "JWT"; public static class Prefixes { @@ -489,6 +489,18 @@ public static class OpenIddictConstants public const string Public = "public"; } + public static class TokenFormats + { + public const string Jwt = "urn:ietf:params:oauth:token-type:jwt"; + public const string Saml1 = "urn:ietf:params:oauth:token-type:saml1"; + public const string Saml2 = "urn:ietf:params:oauth:token-type:saml2"; + + public static class Private + { + public const string DataProtection = "urn:openiddict:params:oauth:token-type:dp"; + } + } + public static class TokenTypeHints { public const string AccessToken = "access_token"; diff --git a/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj b/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj index e4801053..3684fabb 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj +++ b/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj @@ -33,6 +33,7 @@ + diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs index e06fc840..bb4284e5 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs @@ -36,6 +36,9 @@ public static class OpenIddictClientDataProtectionExtensions // Note: the order used here is not important, as the actual order is set in the options. builder.Services.TryAdd(OpenIddictClientDataProtectionHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + // Register the built-in filters used by the default OpenIddict Data Protection event handlers. + 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.Client.DataProtection/OpenIddictClientDataProtectionHandlerFilters.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlerFilters.cs new file mode 100644 index 00000000..0fe81630 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlerFilters.cs @@ -0,0 +1,30 @@ +/* + * 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.ComponentModel; + +namespace OpenIddict.Client.DataProtection; + +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictClientDataProtectionHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers if + /// the selected token format is not ASP.NET Core Data Protection. + /// + public class RequireDataProtectionTokenFormat : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.TokenFormat is TokenFormats.Private.DataProtection); + } + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs index 845db047..f15018ed 100644 --- a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs @@ -29,6 +29,7 @@ public static partial class OpenIddictClientDataProtectionHandlers /* * Token generation: */ + OverrideGeneratedTokenFormat.Descriptor, GenerateDataProtectionToken.Descriptor); /// @@ -100,15 +101,18 @@ public static partial class OpenIddictClientDataProtectionHandlers ClaimsPrincipal? ValidateToken(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.StateToken when !string.IsNullOrEmpty(context.TokenId) - => new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, - TokenTypeHints.StateToken => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, + // + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + (type, context.TokenId) switch + { + (TokenTypeHints.StateToken, { Length: not 0 }) + => new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.StateToken, null or { Length: 0 }) + => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); try { @@ -130,6 +134,53 @@ public static partial class OpenIddictClientDataProtectionHandlers } } + /// + /// Contains the logic responsible for overriding the default token format + /// to generate ASP.NET Core Data Protection tokens instead of JSON Web Tokens. + /// + public class OverrideGeneratedTokenFormat : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public OverrideGeneratedTokenFormat(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSecurityCredentials.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // ASP.NET Core Data Protection can be used to format certain types of tokens in lieu + // of the default token format (typically, JSON Web Token). By default, Data Protection + // is automatically used for all the supported token types once the integration is enabled + // but the default token format can be re-enabled in the options. Alternatively, the token + // format can be overriden manually using a custom event handler registered after this one. + + context.TokenFormat = context.TokenType switch + { + TokenTypeHints.StateToken when !_options.CurrentValue.PreferDefaultStateTokenFormat + => TokenFormats.Private.DataProtection, + + _ => context.TokenFormat // Don't override the format if the token type is not supported. + }; + + return default; + } + } + /// /// Contains the logic responsible for generating a token using Data Protection. /// @@ -145,6 +196,7 @@ public static partial class OpenIddictClientDataProtectionHandlers /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() .UseSingletonHandler() .SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500) .SetType(OpenIddictClientHandlerType.BuiltIn) @@ -164,27 +216,19 @@ public static partial class OpenIddictClientDataProtectionHandlers return default; } - if (context.TokenType switch - { - TokenTypeHints.StateToken => _options.CurrentValue.PreferDefaultStateTokenFormat, - - // The token type is not supported by the Data Protection integration (e.g client assertion tokens). - _ => true - }) - { - 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.StateToken when !context.Options.DisableTokenStorage - => new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, - TokenTypeHints.StateToken => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, + // + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + (context.TokenType, context.PersistTokenPayload) switch + { + (TokenTypeHints.StateToken, true) + => new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.StateToken, false) + => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); using var buffer = new MemoryStream(); using var writer = new BinaryWriter(buffer); diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index 840643b3..0f7abb59 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -299,7 +299,7 @@ public class OpenIddictClientBuilder => AddEncryptionCredentials(new EncryptingCredentials(CreateRsaSecurityKey(2048), algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)), - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)) }; static SymmetricSecurityKey CreateSymmetricSecurityKey(int size) @@ -741,7 +741,7 @@ public class OpenIddictClientBuilder => throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069)), #endif - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)) }; [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs index f837accd..f6885e0e 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs @@ -71,6 +71,11 @@ public static partial class OpenIddictClientEvents /// public string? Token { get; set; } + /// + /// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create. + /// + public string TokenFormat { get; set; } = default!; + /// /// Gets or sets the type of the token to create. /// diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs index dd2d2f5a..9531ea5a 100644 --- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs +++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs @@ -45,6 +45,7 @@ public static class OpenIddictClientExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs index ea3be88f..9c3deb03 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -156,6 +156,22 @@ public static class OpenIddictClientHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token. + /// + public class RequireJsonWebTokenFormat : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.TokenFormat is TokenFormats.Jwt); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a redirection request. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index c763e7b9..049fd2a3 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -707,6 +707,7 @@ public static partial class OpenIddictClientHandlers /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() .UseSingletonHandler() .SetOrder(CreateTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) @@ -776,7 +777,7 @@ public static partial class OpenIddictClientHandlers null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), // For client assertion tokens, use the generic "JWT" type. - TokenTypeHints.ClientAssertionToken => JsonWebTokenTypes.JsonWebToken, + TokenTypeHints.ClientAssertionToken => JsonWebTokenTypes.Jwt, // For state tokens, use its private representation. TokenTypeHints.StateToken => JsonWebTokenTypes.Private.StateToken, diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 2eaca70c..85264ee1 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -1661,7 +1661,16 @@ public static partial class OpenIddictClientHandlers // // See https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication // and https://datatracker.ietf.org/doc/html/rfc7523#section-3 for more information. - principal.SetAudiences(context.TokenEndpoint!.OriginalString); + if (!string.IsNullOrEmpty(context.TokenEndpoint?.OriginalString)) + { + principal.SetAudiences(context.TokenEndpoint.OriginalString); + } + + // If the token endpoint address is not available, use the issuer address as the audience. + else + { + principal.SetAudiences(context.Issuer.OriginalString); + } // Use the client_id as both the subject and the issuer, as required by the specifications. // @@ -1714,6 +1723,7 @@ public static partial class OpenIddictClientHandlers CreateTokenEntry = false, PersistTokenPayload = false, Principal = context.ClientAssertionTokenPrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.ClientAssertionToken }; @@ -1741,7 +1751,13 @@ public static partial class OpenIddictClientHandlers } context.ClientAssertionToken = notification.Token; - context.ClientAssertionTokenType = ClientAssertionTypes.JwtBearer; + context.ClientAssertionTokenType = notification.TokenFormat switch + { + TokenFormats.Jwt => ClientAssertionTypes.JwtBearer, + TokenFormats.Saml2 => ClientAssertionTypes.Saml2Bearer, + + _ => null + }; } } @@ -4124,6 +4140,7 @@ public static partial class OpenIddictClientHandlers CreateTokenEntry = !context.Options.DisableTokenStorage, PersistTokenPayload = !context.Options.DisableTokenStorage, Principal = context.StateTokenPrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.StateToken }; diff --git a/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj b/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj index 496c54a6..1a02208b 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj +++ b/src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj @@ -33,6 +33,7 @@ + diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs index df131355..a95c78ec 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs @@ -36,6 +36,9 @@ public static class OpenIddictServerDataProtectionExtensions // Note: the order used here is not important, as the actual order is set in the options. builder.Services.TryAdd(OpenIddictServerDataProtectionHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + // Register the built-in filters used by the default OpenIddict Data Protection event handlers. + 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 new file mode 100644 index 00000000..a67aaa61 --- /dev/null +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs @@ -0,0 +1,30 @@ +/* + * 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.ComponentModel; + +namespace OpenIddict.Server.DataProtection; + +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictServerDataProtectionHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers if + /// the selected token format is not ASP.NET Core Data Protection. + /// + public class RequireDataProtectionTokenFormat : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.TokenFormat is TokenFormats.Private.DataProtection); + } + } +} diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs index 7eccd5f2..4755df32 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs @@ -29,6 +29,7 @@ public static partial class OpenIddictServerDataProtectionHandlers /* * Token generation: */ + OverrideGeneratedTokenFormat.Descriptor, GenerateDataProtectionToken.Descriptor); /// @@ -185,31 +186,38 @@ public static partial class OpenIddictServerDataProtectionHandlers ClaimsPrincipal? ValidateToken(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)) - }); + // + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + (type, context.TokenId) switch + { + (TokenTypeHints.AccessToken, { Length: not 0 }) + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.AccessToken, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, + + (TokenTypeHints.AuthorizationCode, { Length: not 0 }) + => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.AuthorizationCode, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server }, + + (TokenTypeHints.DeviceCode, { Length: not 0 }) + => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.DeviceCode, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server }, + + (TokenTypeHints.RefreshToken, { Length: not 0 }) + => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.RefreshToken, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server }, + + (TokenTypeHints.UserCode, { Length: not 0 }) + => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.UserCode, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.UserCode, Schemes.Server }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); try { @@ -231,6 +239,65 @@ public static partial class OpenIddictServerDataProtectionHandlers } } + /// + /// Contains the logic responsible for overriding the default token format + /// to generate ASP.NET Core Data Protection tokens instead of JSON Web Tokens. + /// + public class OverrideGeneratedTokenFormat : IOpenIddictServerHandler + { + private readonly IOptionsMonitor _options; + + public OverrideGeneratedTokenFormat(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachSecurityCredentials.Descriptor.Order + 500) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // ASP.NET Core Data Protection can be used to format certain types of tokens in lieu + // of the default token format (typically, JSON Web Token). By default, Data Protection + // is automatically used for all the supported token types once the integration is enabled + // but the default token format can be re-enabled in the options. Alternatively, the token + // format can be overriden manually using a custom event handler registered after this one. + + context.TokenFormat = context.TokenType switch + { + TokenTypeHints.AccessToken when !_options.CurrentValue.PreferDefaultAccessTokenFormat + => TokenFormats.Private.DataProtection, + + TokenTypeHints.AuthorizationCode when !_options.CurrentValue.PreferDefaultAuthorizationCodeFormat + => TokenFormats.Private.DataProtection, + + TokenTypeHints.DeviceCode when !_options.CurrentValue.PreferDefaultDeviceCodeFormat + => TokenFormats.Private.DataProtection, + + TokenTypeHints.RefreshToken when !_options.CurrentValue.PreferDefaultRefreshTokenFormat + => TokenFormats.Private.DataProtection, + + TokenTypeHints.UserCode when !_options.CurrentValue.PreferDefaultUserCodeFormat + => TokenFormats.Private.DataProtection, + + _ => context.TokenFormat // Don't override the format if the token type is not supported. + }; + + return default; + } + } + /// /// Contains the logic responsible for generating a token using Data Protection. /// @@ -246,6 +313,7 @@ public static partial class OpenIddictServerDataProtectionHandlers /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() .UseSingletonHandler() .SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500) .SetType(OpenIddictServerHandlerType.BuiltIn) @@ -265,46 +333,39 @@ public static partial class OpenIddictServerDataProtectionHandlers 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 }, + // + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + (context.TokenType, context.PersistTokenPayload) switch + { + (TokenTypeHints.AccessToken, true) + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.AccessToken, false) + => 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.AuthorizationCode, true) + => new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.AuthorizationCode, false) + => 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.DeviceCode, true) + => new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.DeviceCode, false) + => 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.RefreshToken, true) + => new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.RefreshToken, false) + => 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 }, + (TokenTypeHints.UserCode, true) + => new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.UserCode, false) + => new[] { Handlers.Server, Formats.UserCode, Schemes.Server }, - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); using var buffer = new MemoryStream(); using var writer = new BinaryWriter(buffer); diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 4f0c9f2f..127e0b26 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -308,7 +308,7 @@ public class OpenIddictServerBuilder => AddEncryptionCredentials(new EncryptingCredentials(CreateRsaSecurityKey(2048), algorithm, SecurityAlgorithms.Aes256CbcHmacSha512)), - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)) }; static SymmetricSecurityKey CreateSymmetricSecurityKey(int size) @@ -750,7 +750,7 @@ public class OpenIddictServerBuilder => throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0069)), #endif - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)), + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0058)) }; [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs index 55c7cc06..06792665 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs @@ -77,6 +77,11 @@ public static partial class OpenIddictServerEvents /// public string? Token { get; set; } + /// + /// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create. + /// + public string TokenFormat { get; set; } = default!; + /// /// Gets or sets the type of the token to create. /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index d2f2cd24..6b1d11b6 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -59,6 +59,7 @@ public static class OpenIddictServerExtensions builder.Services.TryAddSingleton(); 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 baaa6126..b7354ca0 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -315,6 +315,22 @@ public static class OpenIddictServerHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token. + /// + public class RequireJsonWebTokenFormat : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.TokenFormat is TokenFormats.Jwt); + } + } + /// /// Represents a filter that excludes the associated handlers if the request is not a logout request. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index e451d50a..f69d60cd 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -90,8 +90,8 @@ public static partial class OpenIddictServerHandlers // For identity tokens, both "JWT" and "application/jwt" are valid. TokenTypeHints.IdToken => new[] { - JsonWebTokenTypes.JsonWebToken, - JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.JsonWebToken + JsonWebTokenTypes.Jwt, + JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt }, // For authorization codes, only the short "oi_auc+jwt" form is valid. @@ -369,7 +369,7 @@ public static partial class OpenIddictServerHandlers => TokenTypeHints.AccessToken, // Both JWT and application/JWT are supported for identity tokens. - JsonWebTokenTypes.JsonWebToken or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.JsonWebToken + JsonWebTokenTypes.Jwt or JsonWebTokenTypes.Prefixes.Application + JsonWebTokenTypes.Jwt => TokenTypeHints.IdToken, JsonWebTokenTypes.Private.AuthorizationCode => TokenTypeHints.AuthorizationCode, @@ -424,7 +424,7 @@ public static partial class OpenIddictServerHandlers // 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. + // Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information. var scopes = context.Principal.GetClaims(Claims.Scope); if (scopes.Length > 1) { @@ -1145,6 +1145,7 @@ public static partial class OpenIddictServerHandlers /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() .UseSingletonHandler() .SetOrder(CreateTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) @@ -1202,10 +1203,16 @@ public static partial class OpenIddictServerHandlers } } - // 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 + // For access tokens, set the public scope claim using the private scope + // claims from the principal and add a jti claim containing a random identifier + // (separate from the token identifier used by OpenIddict to attach a database + // entry to the token) that can be used by the resource servers to determine + // whether an access token has already been used or blacklist them if necessary. + // + // 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. + // + // See https://datatracker.ietf.org/doc/html/rfc9068 for more information. if (context.TokenType is TokenTypeHints.AccessToken) { var scopes = context.Principal.GetScopes(); @@ -1213,6 +1220,8 @@ public static partial class OpenIddictServerHandlers { claims.Add(Claims.Scope, string.Join(" ", scopes)); } + + claims.Add(Claims.JwtId, Guid.NewGuid().ToString()); } // For authorization/device/user codes and refresh tokens, @@ -1241,7 +1250,7 @@ public static partial class OpenIddictServerHandlers null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0025)), TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, - TokenTypeHints.IdToken => JsonWebTokenTypes.JsonWebToken, + TokenTypeHints.IdToken => JsonWebTokenTypes.Jwt, TokenTypeHints.AuthorizationCode => JsonWebTokenTypes.Private.AuthorizationCode, TokenTypeHints.DeviceCode => JsonWebTokenTypes.Private.DeviceCode, TokenTypeHints.RefreshToken => JsonWebTokenTypes.Private.RefreshToken, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index aa5cbf1e..e770b4eb 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -1661,7 +1661,7 @@ public static partial class OpenIddictServerHandlers principal.SetAudiences(context.Principal.GetResources()); // Store the client identifier in the public client_id claim, if available. - // See https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-04 for more information. + // See https://datatracker.ietf.org/doc/html/rfc9068 for more information. principal.SetClaim(Claims.ClientId, context.ClientId); // When receiving a grant_type=refresh_token request, determine whether the client application @@ -2223,6 +2223,7 @@ public static partial class OpenIddictServerHandlers // corresponding option was enabled in the server options. PersistTokenPayload = context.Options.UseReferenceAccessTokens, Principal = context.AccessTokenPrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.AccessToken }; @@ -2288,6 +2289,7 @@ public static partial class OpenIddictServerHandlers CreateTokenEntry = !context.Options.DisableTokenStorage, PersistTokenPayload = !context.Options.DisableTokenStorage, Principal = context.AuthorizationCodePrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.AuthorizationCode }; @@ -2360,6 +2362,7 @@ public static partial class OpenIddictServerHandlers _ => !context.Options.DisableTokenStorage }, Principal = context.DeviceCodePrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.DeviceCode }; @@ -2427,6 +2430,7 @@ public static partial class OpenIddictServerHandlers // corresponding option was enabled in the server options. PersistTokenPayload = context.Options.UseReferenceRefreshTokens, Principal = context.RefreshTokenPrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.RefreshToken }; @@ -2731,6 +2735,7 @@ public static partial class OpenIddictServerHandlers CreateTokenEntry = !context.Options.DisableTokenStorage, PersistTokenPayload = !context.Options.DisableTokenStorage, Principal = context.UserCodePrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.UserCode }; @@ -2797,6 +2802,7 @@ public static partial class OpenIddictServerHandlers // Identity tokens cannot never be reference tokens. PersistTokenPayload = false, Principal = context.IdentityTokenPrincipal!, + TokenFormat = TokenFormats.Jwt, TokenType = TokenTypeHints.IdToken }; diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 1dcfbdf6..4478fe94 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -143,7 +143,7 @@ public class OpenIddictServerOptions type = usage switch { TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, - TokenTypeHints.IdToken => JsonWebTokenTypes.JsonWebToken, + TokenTypeHints.IdToken => JsonWebTokenTypes.Jwt, _ => throw new NotSupportedException(SR.GetResourceString(SR.ID0269)) }; diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs index 15bfd330..0c347bcb 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs @@ -95,15 +95,18 @@ public static partial class OpenIddictValidationDataProtectionHandlers ClaimsPrincipal? ValidateToken(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 }, - - _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) - }); + // + // Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens. + var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector( + (type, context.TokenId) switch + { + (TokenTypeHints.AccessToken, { Length: not 0 }) + => new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server }, + (TokenTypeHints.AccessToken, null or { Length: 0 }) + => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server }, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) + }); try { diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index 8a2c7ae8..abf5182f 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -413,7 +413,7 @@ public static partial class OpenIddictValidationHandlers // 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. + // Visit https://datatracker.ietf.org/doc/html/rfc9068 for more information. var scopes = context.Principal.GetClaims(Claims.Scope); if (scopes.Length > 1) { diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 2a48ae9c..76be8091 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -128,7 +128,7 @@ public class OpenIddictValidationOptions type = usage switch { TokenTypeHints.AccessToken => JsonWebTokenTypes.AccessToken, - TokenTypeHints.IdToken => JsonWebTokenTypes.JsonWebToken, + TokenTypeHints.IdToken => JsonWebTokenTypes.Jwt, _ => throw new NotSupportedException(SR.GetResourceString(SR.ID0269)) };