/* * 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 DefaultHandlers { get; } = ImmutableArray.Create( /* * Access token serialization: */ AttachAccessTokenSerializationParameters.Descriptor, SerializeJwtBearerToken.Descriptor, /* * Authorization code serialization: */ AttachAuthorizationCodeSerializationParameters.Descriptor, SerializeJwtBearerToken.Descriptor, /* * Identity token serialization: */ AttachIdentityTokenSerializationParameters.Descriptor, SerializeJwtBearerToken.Descriptor, /* * Refresh token serialization: */ AttachRefreshTokenSerializationParameters.Descriptor, SerializeJwtBearerToken.Descriptor, /* * Access token deserialization: */ AttachAccessTokenDeserializationParameters.Descriptor, DeserializeJwtBearerToken.Descriptor, /* * Authorization code deserialization: */ AttachAuthorizationCodeDeserializationParameters.Descriptor, DeserializeJwtBearerToken.Descriptor, /* * Identity token deserialization: */ AttachIdentityTokenDeserializationParameters.Descriptor, DeserializeJwtBearerToken.Descriptor, /* * Authorization code deserialization: */ AttachRefreshTokenDeserializationParameters.Descriptor, DeserializeJwtBearerToken.Descriptor); /// /// Contains the logic responsible of generating a JWT bearer token using IdentityModel. /// public class SerializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseSerializingContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler>() .SetOrder(int.MaxValue - 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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(StringComparer.Ordinal) { [Claims.Private.TokenUsage] = context.TokenUsage }; var destinations = new Dictionary(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(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(claims), EncryptingCredentials = context.EncryptingCredentials, Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.SigningCredentials }); context.HandleSerialization(); return default; } } /// /// Contains the logic responsible of unprotecting a JWT bearer token using IdentityModel. /// public class DeserializeJwtBearerToken : IOpenIddictServerHandler where TContext : BaseDeserializingContext { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler>() .SetOrder(int.MaxValue - 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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 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; } } } /// /// Contains the logic responsible of populating the serialization parameters needed to generate an access token. /// public class AttachAccessTokenSerializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the serialization parameters needed to generate an authorization code. /// public class AttachAuthorizationCodeSerializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the serialization parameters needed to generate an identity token. /// public class AttachIdentityTokenSerializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the serialization parameters needed to generate a refresh token. /// public class AttachRefreshTokenSerializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an access token. /// public class AttachAccessTokenDeserializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an authorization code. /// public class AttachAuthorizationCodeDeserializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } /// /// Contains the logic responsible of populating the deserialization parameters needed to unprotect an identity token. /// public class AttachIdentityTokenDeserializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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(); 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; } } /// /// Contains the logic responsible of populating the deserialization parameters needed to unprotect a refresh token. /// public class AttachRefreshTokenDeserializationParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); /// /// Processes the event. /// /// The context associated with the event to process. /// /// A that can be used to monitor the asynchronous operation. /// 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; } } } } }