You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
484 lines
25 KiB
484 lines
25 KiB
/*
|
|
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|
* the license and the contributors participating to this project.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.IO;
|
|
using System.Security.Claims;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using OpenIddict.Abstractions;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes;
|
|
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
|
|
using static OpenIddict.Server.OpenIddictServerEvents;
|
|
using static OpenIddict.Server.OpenIddictServerHandlerFilters;
|
|
using static OpenIddict.Server.OpenIddictServerHandlers;
|
|
using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties;
|
|
using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes;
|
|
|
|
namespace OpenIddict.Server.DataProtection
|
|
{
|
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
public static partial class OpenIddictServerDataProtectionHandlers
|
|
{
|
|
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
|
|
/*
|
|
* Authentication processing:
|
|
*/
|
|
ValidateDataProtectionToken.Descriptor,
|
|
|
|
/*
|
|
* Sign-in processing:
|
|
*/
|
|
GenerateDataProtectionAccessToken.Descriptor,
|
|
GenerateDataProtectionAuthorizationCode.Descriptor,
|
|
GenerateDataProtectionDeviceCode.Descriptor,
|
|
GenerateDataProtectionRefreshToken.Descriptor,
|
|
GenerateDataProtectionUserCode.Descriptor);
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of validating tokens generated using Data Protection.
|
|
/// </summary>
|
|
public class ValidateDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public ValidateDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
|
|
.UseSingletonHandler<ValidateDataProtectionToken>()
|
|
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If a principal was already attached, don't overwrite it.
|
|
if (context.Principal != null)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds
|
|
// to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads.
|
|
// As an optimization, always ignore tokens that don't start with the "CfDJ8" string.
|
|
if (string.IsNullOrEmpty(context.Token) || !context.Token.StartsWith("CfDJ8", StringComparison.Ordinal))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// If the token cannot be validated, don't return an error to allow another handle to validate it.
|
|
var principal = !string.IsNullOrEmpty(context.TokenType) ?
|
|
ValidateToken(context.Token, context.TokenType) :
|
|
ValidateToken(context.Token, TokenTypeHints.AccessToken) ??
|
|
ValidateToken(context.Token, TokenTypeHints.RefreshToken) ??
|
|
ValidateToken(context.Token, TokenTypeHints.AuthorizationCode) ??
|
|
ValidateToken(context.Token, TokenTypeHints.DeviceCode) ??
|
|
ValidateToken(context.Token, TokenTypeHints.UserCode);
|
|
if (principal == null)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
context.Principal = principal;
|
|
|
|
context.Logger.LogTrace("The DP token '{Token}' was successfully validated and the following claims " +
|
|
"could be extracted: {Claims}.", context.Token, context.Principal.Claims);
|
|
|
|
return default;
|
|
|
|
ClaimsPrincipal ValidateToken(string token, string type)
|
|
{
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch
|
|
{
|
|
TokenTypeHints.AccessToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
|
|
=> new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
|
|
|
|
TokenTypeHints.AuthorizationCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
|
|
=> new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
|
|
|
|
TokenTypeHints.DeviceCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
|
|
=> new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
|
|
|
|
TokenTypeHints.RefreshToken when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
|
|
=> new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
|
|
|
|
TokenTypeHints.UserCode when context.Transaction.Properties.ContainsKey(Properties.ReferenceTokenIdentifier)
|
|
=> new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server },
|
|
|
|
TokenTypeHints.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
|
|
TokenTypeHints.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
|
|
TokenTypeHints.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
|
|
TokenTypeHints.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
|
|
TokenTypeHints.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
|
|
|
|
_ => throw new InvalidOperationException("The specified token type is not supported.")
|
|
});
|
|
|
|
try
|
|
{
|
|
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token)));
|
|
using var reader = new BinaryReader(buffer);
|
|
|
|
// Note: since the data format relies on a data protector using different "purposes" strings
|
|
// per token type, the token processed at this stage is guaranteed to be of the expected type.
|
|
return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type);
|
|
}
|
|
|
|
catch (Exception exception)
|
|
{
|
|
context.Logger.LogTrace(exception, "An exception occured while deserializing the token '{Token}'.", token);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of generating an access token using Data Protection.
|
|
/// </summary>
|
|
public class GenerateDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSignInContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public GenerateDataProtectionAccessToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
|
|
.AddFilter<RequireAccessTokenIncluded>()
|
|
.AddFilter<RequireDataProtectionFormatEnabled>()
|
|
.UseSingletonHandler<GenerateDataProtectionAccessToken>()
|
|
.SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessSignInContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If an access token was already attached by another handler, don't overwrite it.
|
|
if (!string.IsNullOrEmpty(context.Response.AccessToken))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = context.Options.UseReferenceTokens ?
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server) :
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.AccessToken, Schemes.Server);
|
|
|
|
using var buffer = new MemoryStream();
|
|
using var writer = new BinaryWriter(buffer);
|
|
|
|
_options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal);
|
|
|
|
context.Response.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
|
|
|
|
context.Logger.LogTrace("The access token '{Identifier}' was successfully created: {Payload}. " +
|
|
"The principal used to create the token contained the following claims: {Claims}.",
|
|
context.AccessTokenPrincipal.GetClaim(Claims.JwtId),
|
|
context.Response.AccessToken, context.AccessTokenPrincipal.Claims);
|
|
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of generating an authorization code using Data Protection.
|
|
/// </summary>
|
|
public class GenerateDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSignInContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public GenerateDataProtectionAuthorizationCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
|
|
.AddFilter<RequireAuthorizationCodeIncluded>()
|
|
.AddFilter<RequireDataProtectionFormatEnabled>()
|
|
.UseSingletonHandler<GenerateDataProtectionAuthorizationCode>()
|
|
.SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessSignInContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If an authorization code was already attached by another handler, don't overwrite it.
|
|
if (!string.IsNullOrEmpty(context.Response.Code))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = context.Options.UseReferenceTokens ?
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server) :
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.AuthorizationCode, Schemes.Server);
|
|
|
|
using var buffer = new MemoryStream();
|
|
using var writer = new BinaryWriter(buffer);
|
|
|
|
_options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal);
|
|
|
|
context.Response.Code = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
|
|
|
|
context.Logger.LogTrace("The authorization code '{Identifier}' was successfully created: {Payload}. " +
|
|
"The principal used to create the token contained the following claims: {Claims}.",
|
|
context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId),
|
|
context.Response.Code, context.AuthorizationCodePrincipal.Claims);
|
|
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of generating a device code using Data Protection.
|
|
/// </summary>
|
|
public class GenerateDataProtectionDeviceCode : IOpenIddictServerHandler<ProcessSignInContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public GenerateDataProtectionDeviceCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
|
|
.AddFilter<RequireDeviceCodeIncluded>()
|
|
.AddFilter<RequireDataProtectionFormatEnabled>()
|
|
.UseSingletonHandler<GenerateDataProtectionDeviceCode>()
|
|
.SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessSignInContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If a device code was already attached by another handler, don't overwrite it.
|
|
if (!string.IsNullOrEmpty(context.Response.DeviceCode))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = !context.Options.DisableTokenStorage ?
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server) :
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.DeviceCode, Schemes.Server);
|
|
|
|
using var buffer = new MemoryStream();
|
|
using var writer = new BinaryWriter(buffer);
|
|
|
|
_options.CurrentValue.Formatter.WriteToken(writer, context.DeviceCodePrincipal);
|
|
|
|
context.Response.DeviceCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
|
|
|
|
context.Logger.LogTrace("The device code '{Identifier}' was successfully created: {Payload}. " +
|
|
"The principal used to create the token contained the following claims: {Claims}.",
|
|
context.DeviceCodePrincipal.GetClaim(Claims.JwtId),
|
|
context.Response.DeviceCode, context.DeviceCodePrincipal.Claims);
|
|
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of generating a refresh token using Data Protection.
|
|
/// </summary>
|
|
public class GenerateDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSignInContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public GenerateDataProtectionRefreshToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
|
|
.AddFilter<RequireRefreshTokenIncluded>()
|
|
.AddFilter<RequireDataProtectionFormatEnabled>()
|
|
.UseSingletonHandler<GenerateDataProtectionRefreshToken>()
|
|
.SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessSignInContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If a refresh token was already attached by another handler, don't overwrite it.
|
|
if (!string.IsNullOrEmpty(context.Response.RefreshToken))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = context.Options.UseReferenceTokens ?
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server) :
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.RefreshToken, Schemes.Server);
|
|
|
|
using var buffer = new MemoryStream();
|
|
using var writer = new BinaryWriter(buffer);
|
|
|
|
_options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal);
|
|
|
|
context.Response.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
|
|
|
|
context.Logger.LogTrace("The refresh token '{Identifier}' was successfully created: {Payload}. " +
|
|
"The principal used to create the token contained the following claims: {Claims}.",
|
|
context.RefreshTokenPrincipal.GetClaim(Claims.JwtId),
|
|
context.Response.RefreshToken, context.RefreshTokenPrincipal.Claims);
|
|
|
|
return default;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of generating a user code using Data Protection.
|
|
/// </summary>
|
|
public class GenerateDataProtectionUserCode : IOpenIddictServerHandler<ProcessSignInContext>
|
|
{
|
|
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
|
|
|
|
public GenerateDataProtectionUserCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
|
|
=> _options = options;
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
|
|
.AddFilter<RequireUserCodeIncluded>()
|
|
.AddFilter<RequireDataProtectionFormatEnabled>()
|
|
.UseSingletonHandler<GenerateDataProtectionUserCode>()
|
|
.SetOrder(GenerateIdentityModelUserCode.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
/// <summary>
|
|
/// Processes the event.
|
|
/// </summary>
|
|
/// <param name="context">The context associated with the event to process.</param>
|
|
/// <returns>
|
|
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
|
|
/// </returns>
|
|
public ValueTask HandleAsync([NotNull] ProcessSignInContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// If a user code was already attached by another handler, don't overwrite it.
|
|
if (!string.IsNullOrEmpty(context.Response.UserCode))
|
|
{
|
|
return default;
|
|
}
|
|
|
|
// Create a Data Protection protector using the provider registered in the options.
|
|
var protector = !context.Options.DisableTokenStorage ?
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server) :
|
|
_options.CurrentValue.DataProtectionProvider.CreateProtector(
|
|
Handlers.Server, Formats.UserCode, Schemes.Server);
|
|
|
|
using var buffer = new MemoryStream();
|
|
using var writer = new BinaryWriter(buffer);
|
|
|
|
_options.CurrentValue.Formatter.WriteToken(writer, context.UserCodePrincipal);
|
|
|
|
context.Response.UserCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
|
|
|
|
context.Logger.LogTrace("The user code '{Identifier}' was successfully created: {Payload}. " +
|
|
"The principal used to create the token contained the following claims: {Claims}.",
|
|
context.UserCodePrincipal.GetClaim(Claims.JwtId),
|
|
context.Response.UserCode, context.UserCodePrincipal.Claims);
|
|
|
|
return default;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|