Browse Source

Allow determining the token format dynamically and automatically add a jti claim to JWT access tokens

pull/1449/head
Kévin Chalet 4 years ago
parent
commit
7bb02a43bd
  1. 14
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  2. 1
      src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj
  3. 3
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs
  4. 30
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlerFilters.cs
  5. 98
      src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs
  6. 4
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  7. 5
      src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs
  8. 1
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  9. 16
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  10. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs
  11. 21
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  12. 1
      src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj
  13. 3
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionExtensions.cs
  14. 30
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlerFilters.cs
  15. 179
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs
  16. 4
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  17. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs
  18. 1
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  19. 16
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  20. 25
      src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
  21. 8
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  22. 2
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  23. 21
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.Protection.cs
  24. 2
      src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs
  25. 2
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs

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

1
src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj

@ -33,6 +33,7 @@
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" />
<Using Include="OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionHandlers" Static="true" />
<Using Include="OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionHandlerFilters" Static="true" />
</ItemGroup>
</Project>

3
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<RequireDataProtectionTokenFormat>();
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{

30
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
{
/// <summary>
/// Represents a filter that excludes the associated handlers if
/// the selected token format is not ASP.NET Core Data Protection.
/// </summary>
public class RequireDataProtectionTokenFormat : IOpenIddictClientHandlerFilter<GenerateTokenContext>
{
public ValueTask<bool> IsActiveAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.TokenFormat is TokenFormats.Private.DataProtection);
}
}
}

98
src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs

@ -29,6 +29,7 @@ public static partial class OpenIddictClientDataProtectionHandlers
/*
* Token generation:
*/
OverrideGeneratedTokenFormat.Descriptor,
GenerateDataProtectionToken.Descriptor);
/// <summary>
@ -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
}
}
/// <summary>
/// Contains the logic responsible for overriding the default token format
/// to generate ASP.NET Core Data Protection tokens instead of JSON Web Tokens.
/// </summary>
public class OverrideGeneratedTokenFormat : IOpenIddictClientHandler<GenerateTokenContext>
{
private readonly IOptionsMonitor<OpenIddictClientDataProtectionOptions> _options;
public OverrideGeneratedTokenFormat(IOptionsMonitor<OpenIddictClientDataProtectionOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.UseSingletonHandler<OverrideGeneratedTokenFormat>()
.SetOrder(AttachSecurityCredentials.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible for generating a token using Data Protection.
/// </summary>
@ -145,6 +196,7 @@ public static partial class OpenIddictClientDataProtectionHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireDataProtectionTokenFormat>()
.UseSingletonHandler<GenerateDataProtectionToken>()
.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);

4
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",

5
src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs

@ -71,6 +71,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? Token { get; set; }
/// <summary>
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create.
/// </summary>
public string TokenFormat { get; set; } = default!;
/// <summary>
/// Gets or sets the type of the token to create.
/// </summary>

1
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -45,6 +45,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireFrontchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();
builder.Services.TryAddSingleton<RequireStateTokenGenerated>();

16
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -156,6 +156,22 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
/// </summary>
public class RequireJsonWebTokenFormat : IOpenIddictClientHandlerFilter<GenerateTokenContext>
{
public ValueTask<bool> IsActiveAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.TokenFormat is TokenFormats.Jwt);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a redirection request.
/// </summary>

3
src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs

@ -707,6 +707,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireJsonWebTokenFormat>()
.UseSingletonHandler<GenerateIdentityModelToken>()
.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,

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

1
src/OpenIddict.Server.DataProtection/OpenIddict.Server.DataProtection.csproj

@ -33,6 +33,7 @@
<Using Include="OpenIddict.Server.OpenIddictServerHandlers" Static="true" />
<Using Include="OpenIddict.Server.OpenIddictServerHandlerFilters" Static="true" />
<Using Include="OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlers" Static="true" />
<Using Include="OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters" Static="true" />
</ItemGroup>
</Project>

3
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<RequireDataProtectionTokenFormat>();
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{

30
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
{
/// <summary>
/// Represents a filter that excludes the associated handlers if
/// the selected token format is not ASP.NET Core Data Protection.
/// </summary>
public class RequireDataProtectionTokenFormat : IOpenIddictServerHandlerFilter<GenerateTokenContext>
{
public ValueTask<bool> IsActiveAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.TokenFormat is TokenFormats.Private.DataProtection);
}
}
}

179
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.Protection.cs

@ -29,6 +29,7 @@ public static partial class OpenIddictServerDataProtectionHandlers
/*
* Token generation:
*/
OverrideGeneratedTokenFormat.Descriptor,
GenerateDataProtectionToken.Descriptor);
/// <summary>
@ -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
}
}
/// <summary>
/// Contains the logic responsible for overriding the default token format
/// to generate ASP.NET Core Data Protection tokens instead of JSON Web Tokens.
/// </summary>
public class OverrideGeneratedTokenFormat : IOpenIddictServerHandler<GenerateTokenContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public OverrideGeneratedTokenFormat(IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.UseSingletonHandler<OverrideGeneratedTokenFormat>()
.SetOrder(AttachSecurityCredentials.Descriptor.Order + 500)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
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;
}
}
/// <summary>
/// Contains the logic responsible for generating a token using Data Protection.
/// </summary>
@ -246,6 +313,7 @@ public static partial class OpenIddictServerDataProtectionHandlers
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireDataProtectionTokenFormat>()
.UseSingletonHandler<GenerateDataProtectionToken>()
.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);

4
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",

5
src/OpenIddict.Server/OpenIddictServerEvents.Protection.cs

@ -77,6 +77,11 @@ public static partial class OpenIddictServerEvents
/// </summary>
public string? Token { get; set; }
/// <summary>
/// Gets or sets the format of the token (e.g JWT or ASP.NET Core Data Protection) to create.
/// </summary>
public string TokenFormat { get; set; } = default!;
/// <summary>
/// Gets or sets the type of the token to create.
/// </summary>

1
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -59,6 +59,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireIdentityTokenGenerated>();
builder.Services.TryAddSingleton<RequireIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireLogoutRequest>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
builder.Services.TryAddSingleton<RequireReferenceAccessTokensEnabled>();

16
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -315,6 +315,22 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
/// </summary>
public class RequireJsonWebTokenFormat : IOpenIddictServerHandlerFilter<GenerateTokenContext>
{
public ValueTask<bool> IsActiveAsync(GenerateTokenContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.TokenFormat is TokenFormats.Jwt);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a logout request.
/// </summary>

25
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
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<GenerateTokenContext>()
.AddFilter<RequireJsonWebTokenFormat>()
.UseSingletonHandler<GenerateIdentityModelToken>()
.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,

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

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

21
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
{

2
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)
{

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

Loading…
Cancel
Save