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