Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
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.
 
 
 
 
 
 

608 lines
30 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.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace OpenIddict.Server
{
public static partial class OpenIddictServerHandlers
{
public static class Serialization
{
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Access token serialization:
*/
AttachAccessTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeAccessTokenContext>.Descriptor,
/*
* Authorization code serialization:
*/
AttachAuthorizationCodeSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeAuthorizationCodeContext>.Descriptor,
/*
* Identity token serialization:
*/
AttachIdentityTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeIdentityTokenContext>.Descriptor,
/*
* Refresh token serialization:
*/
AttachRefreshTokenSerializationParameters.Descriptor,
SerializeJwtBearerToken<SerializeRefreshTokenContext>.Descriptor,
/*
* Access token deserialization:
*/
AttachAccessTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeAccessTokenContext>.Descriptor,
/*
* Authorization code deserialization:
*/
AttachAuthorizationCodeDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeAuthorizationCodeContext>.Descriptor,
/*
* Identity token deserialization:
*/
AttachIdentityTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeIdentityTokenContext>.Descriptor,
/*
* Authorization code deserialization:
*/
AttachRefreshTokenDeserializationParameters.Descriptor,
DeserializeJwtBearerToken<DeserializeRefreshTokenContext>.Descriptor);
/// <summary>
/// Contains the logic responsible of generating a JWT bearer token using IdentityModel.
/// </summary>
public class SerializeJwtBearerToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseSerializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<SerializeJwtBearerToken<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.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] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.TokenUsage))
{
throw new InvalidOperationException("The token usage cannot be null or empty.");
}
var claims = new Dictionary<string, object>(StringComparer.Ordinal)
{
[Claims.Private.TokenUsage] = context.TokenUsage
};
var destinations = new Dictionary<string, string[]>(StringComparer.Ordinal);
foreach (var group in context.Principal.Claims.GroupBy(claim => claim.Type))
{
var collection = group.ToList();
// Note: destinations are attached to claims as special CLR properties. Such properties can't be serialized
// as part of classic JWT tokens. To work around this limitation, claim destinations are added to a special
// claim named oi_cl_dstn that contains a map of all the claims and their attached destinations, if any.
var set = new HashSet<string>(collection[0].GetDestinations(), StringComparer.OrdinalIgnoreCase);
if (set.Count != 0)
{
// Ensure the other claims of the same type use the same exact destinations.
for (var index = 0; index < collection.Count; index++)
{
if (!set.SetEquals(collection[index].GetDestinations()))
{
throw new InvalidOperationException($"Conflicting destinations for the claim '{group.Key}' were specified.");
}
}
destinations[group.Key] = set.ToArray();
}
}
// Unless at least one claim was added to the claim destinations map,
// don't add the special claim to avoid adding a useless empty claim.
if (destinations.Count != 0)
{
claims[Claims.Private.ClaimDestinations] = destinations;
}
context.Token = context.SecurityTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Subject = (ClaimsIdentity) context.Principal.Identity,
Claims = new ReadOnlyDictionary<string, object>(claims),
EncryptingCredentials = context.EncryptingCredentials,
Issuer = context.Issuer?.AbsoluteUri,
SigningCredentials = context.SigningCredentials
});
context.HandleSerialization();
return default;
}
}
/// <summary>
/// Contains the logic responsible of unprotecting a JWT bearer token using IdentityModel.
/// </summary>
public class DeserializeJwtBearerToken<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseDeserializingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseSingletonHandler<DeserializeJwtBearerToken<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.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] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.SecurityTokenHandler.CanReadToken(context.Token))
{
context.Logger.LogTrace("The token '{Token}' was not compatible with the JWT format.", context.Token);
return default;
}
try
{
var result = context.SecurityTokenHandler.ValidateToken(context.Token, context.TokenValidationParameters);
if (result == null || !result.IsValid)
{
if (result?.Exception != null)
{
context.Logger.LogTrace(result.Exception, "The JWT token '{Token}' could not be validated.", context.Token);
}
else
{
context.Logger.LogTrace("The token '{Token}' could not be validated.", context.Token);
}
}
var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken;
if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) ||
!string.Equals(usage, context.TokenUsage, StringComparison.OrdinalIgnoreCase))
{
context.Logger.LogDebug("The token usage associated to the token {Token} does not match the expected type.");
context.HandleDeserialization();
return default;
}
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity);
// Restore the claim destinations from the special oi_cl_dstn claim (represented as a dictionary/JSON object).
if (assertion.TryGetPayloadValue(Claims.Private.ClaimDestinations, out IDictionary<string, string[]> definitions))
{
foreach (var definition in definitions)
{
foreach (var claim in context.Principal.Claims.Where(claim => claim.Type == definition.Key))
{
claim.SetDestinations(definition.Value);
}
}
}
context.HandleDeserialization();
return default;
}
catch (Exception exception)
{
context.Logger.LogDebug(exception, "An exception occured while deserializing a token.");
context.HandleDeserialization();
return default;
}
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an access token.
/// </summary>
public class AttachAccessTokenSerializationParameters : IOpenIddictServerHandler<SerializeAccessTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] SerializeAccessTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Options.SigningCredentials.Count == 0)
{
throw new InvalidOperationException("No suitable signing credentials could be found.");
}
context.EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault(
credentials => credentials.Key is SymmetricSecurityKey);
context.SecurityTokenHandler = context.Options.AccessTokenHandler;
context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(
credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First();
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an authorization code.
/// </summary>
public class AttachAuthorizationCodeSerializationParameters : IOpenIddictServerHandler<SerializeAuthorizationCodeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] SerializeAuthorizationCodeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Options.EncryptionCredentials.Count == 0)
{
throw new InvalidOperationException("No suitable encryption credentials could be found.");
}
if (context.Options.SigningCredentials.Count == 0)
{
throw new InvalidOperationException("No suitable signing credentials could be found.");
}
context.EncryptingCredentials = context.Options.EncryptionCredentials[0];
context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler;
context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(
credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First();
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate an identity token.
/// </summary>
public class AttachIdentityTokenSerializationParameters : IOpenIddictServerHandler<SerializeIdentityTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeIdentityTokenContext>()
.UseSingletonHandler<AttachIdentityTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] SerializeIdentityTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey))
{
throw new InvalidOperationException("No suitable signing credentials could be found.");
}
context.SecurityTokenHandler = context.Options.IdentityTokenHandler;
context.SigningCredentials = context.Options.SigningCredentials.First(
credentials => credentials.Key is AsymmetricSecurityKey);
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the serialization parameters needed to generate a refresh token.
/// </summary>
public class AttachRefreshTokenSerializationParameters : IOpenIddictServerHandler<SerializeRefreshTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenSerializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] SerializeRefreshTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Options.EncryptionCredentials.Count == 0)
{
throw new InvalidOperationException("No suitable encryption credentials could be found.");
}
if (context.Options.SigningCredentials.Count == 0)
{
throw new InvalidOperationException("No suitable signing credentials could be found.");
}
context.EncryptingCredentials = context.Options.EncryptionCredentials[0];
context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler;
context.SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(
credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First();
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an access token.
/// </summary>
public class AttachAccessTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeAccessTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAccessTokenContext>()
.UseSingletonHandler<AttachAccessTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] DeserializeAccessTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SecurityTokenHandler = context.Options.AccessTokenHandler;
context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials
.Select(credentials => credentials.Key);
context.TokenValidationParameters.NameClaimType = Claims.Name;
context.TokenValidationParameters.RoleClaimType = Claims.Role;
context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials
.Select(credentials => credentials.Key)
.Where(key => key is SymmetricSecurityKey);
context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri;
context.TokenValidationParameters.ValidateAudience = false;
context.TokenValidationParameters.ValidateLifetime = false;
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an authorization code.
/// </summary>
public class AttachAuthorizationCodeDeserializationParameters : IOpenIddictServerHandler<DeserializeAuthorizationCodeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAuthorizationCodeContext>()
.UseSingletonHandler<AttachAuthorizationCodeDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] DeserializeAuthorizationCodeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler;
context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials
.Select(credentials => credentials.Key);
context.TokenValidationParameters.NameClaimType = Claims.Name;
context.TokenValidationParameters.RoleClaimType = Claims.Role;
context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials
.Select(credentials => credentials.Key);
context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri;
context.TokenValidationParameters.ValidateAudience = false;
context.TokenValidationParameters.ValidateLifetime = false;
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect an identity token.
/// </summary>
public class AttachIdentityTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeIdentityTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeIdentityTokenContext>()
.UseSingletonHandler<AttachIdentityTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] DeserializeIdentityTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SecurityTokenHandler = context.Options.IdentityTokenHandler;
context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials
.Select(credentials => credentials.Key)
.OfType<AsymmetricSecurityKey>();
context.TokenValidationParameters.NameClaimType = Claims.Name;
context.TokenValidationParameters.RoleClaimType = Claims.Role;
context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri;
context.TokenValidationParameters.ValidateAudience = false;
context.TokenValidationParameters.ValidateLifetime = false;
return default;
}
}
/// <summary>
/// Contains the logic responsible of populating the deserialization parameters needed to unprotect a refresh token.
/// </summary>
public class AttachRefreshTokenDeserializationParameters : IOpenIddictServerHandler<DeserializeRefreshTokenContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeRefreshTokenContext>()
.UseSingletonHandler<AttachRefreshTokenDeserializationParameters>()
.SetOrder(int.MinValue + 100_000)
.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] DeserializeRefreshTokenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.SecurityTokenHandler = context.Options.AuthorizationCodeHandler;
context.TokenValidationParameters.IssuerSigningKeys = context.Options.SigningCredentials
.Select(credentials => credentials.Key);
context.TokenValidationParameters.NameClaimType = Claims.Name;
context.TokenValidationParameters.RoleClaimType = Claims.Role;
context.TokenValidationParameters.TokenDecryptionKeys = context.Options.EncryptionCredentials
.Select(credentials => credentials.Key);
context.TokenValidationParameters.ValidIssuer = context.Issuer?.AbsoluteUri;
context.TokenValidationParameters.ValidateAudience = false;
context.TokenValidationParameters.ValidateLifetime = false;
return default;
}
}
}
}
}