committed by
GitHub
33 changed files with 4401 additions and 2905 deletions
@ -0,0 +1,18 @@ |
|||
/* |
|||
* 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.IO; |
|||
using System.Security.Claims; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace OpenIddict.Server.DataProtection |
|||
{ |
|||
public interface IOpenIddictServerDataProtectionFormatter |
|||
{ |
|||
ClaimsPrincipal ReadToken([NotNull] BinaryReader reader); |
|||
void WriteToken([NotNull] BinaryWriter writer, [NotNull] ClaimsPrincipal principal); |
|||
} |
|||
} |
|||
@ -0,0 +1,379 @@ |
|||
/* |
|||
* 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.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using JetBrains.Annotations; |
|||
using Newtonsoft.Json; |
|||
using Newtonsoft.Json.Linq; |
|||
using OpenIddict.Abstractions; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
using Properties = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Properties; |
|||
|
|||
namespace OpenIddict.Server.DataProtection |
|||
{ |
|||
public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProtectionFormatter |
|||
{ |
|||
public ClaimsPrincipal ReadToken(BinaryReader reader) |
|||
{ |
|||
if (reader == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(reader)); |
|||
} |
|||
|
|||
var (principal, properties) = Read(reader, version: 5); |
|||
if (principal == null) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
// Tokens serialized using the ASP.NET Core Data Protection stack are compound
|
|||
// of both claims and special authentication properties. To ensure existing tokens
|
|||
// can be reused, well-known properties are manually mapped to their claims equivalents.
|
|||
|
|||
return principal |
|||
.SetAudiences(GetArrayProperty(properties, Properties.Audiences)) |
|||
.SetCreationDate(GetDateProperty(properties, Properties.Issued)) |
|||
.SetExpirationDate(GetDateProperty(properties, Properties.Expires)) |
|||
.SetPresenters(GetArrayProperty(properties, Properties.Presenters)) |
|||
.SetScopes(GetArrayProperty(properties, Properties.Scopes)) |
|||
|
|||
.SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) |
|||
.SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime)) |
|||
.SetClaim(Claims.Private.AuthorizationId, GetProperty(properties, Properties.InternalAuthorizationId)) |
|||
.SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) |
|||
.SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) |
|||
.SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) |
|||
.SetClaim(Claims.Private.Nonce, GetProperty(properties, Properties.Nonce)) |
|||
.SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) |
|||
.SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) |
|||
.SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)); |
|||
|
|||
static (ClaimsPrincipal principal, ImmutableDictionary<string, string> properties) Read(BinaryReader reader, int version) |
|||
{ |
|||
if (version != reader.ReadInt32()) |
|||
{ |
|||
return (null, ImmutableDictionary.Create<string, string>()); |
|||
} |
|||
|
|||
// Read the authentication scheme associated to the ticket.
|
|||
_ = reader.ReadString(); |
|||
|
|||
// Read the number of identities stored in the serialized payload.
|
|||
var count = reader.ReadInt32(); |
|||
if (count < 0) |
|||
{ |
|||
return (null, ImmutableDictionary.Create<string, string>()); |
|||
} |
|||
|
|||
var identities = new ClaimsIdentity[count]; |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
identities[index] = ReadIdentity(reader); |
|||
} |
|||
|
|||
var properties = ReadProperties(reader, version); |
|||
|
|||
return (new ClaimsPrincipal(identities), properties); |
|||
} |
|||
|
|||
static ClaimsIdentity ReadIdentity(BinaryReader reader) |
|||
{ |
|||
var identity = new ClaimsIdentity( |
|||
authenticationType: reader.ReadString(), |
|||
nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType), |
|||
roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType)); |
|||
|
|||
// Read the number of claims contained in the serialized identity.
|
|||
var count = reader.ReadInt32(); |
|||
|
|||
for (int index = 0; index != count; ++index) |
|||
{ |
|||
var claim = ReadClaim(reader, identity); |
|||
|
|||
identity.AddClaim(claim); |
|||
} |
|||
|
|||
// Determine whether the identity has a bootstrap context attached.
|
|||
if (reader.ReadBoolean()) |
|||
{ |
|||
identity.BootstrapContext = reader.ReadString(); |
|||
} |
|||
|
|||
// Determine whether the identity has an actor identity attached.
|
|||
if (reader.ReadBoolean()) |
|||
{ |
|||
identity.Actor = ReadIdentity(reader); |
|||
} |
|||
|
|||
return identity; |
|||
} |
|||
|
|||
static Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) |
|||
{ |
|||
var type = ReadWithDefault(reader, identity.NameClaimType); |
|||
var value = reader.ReadString(); |
|||
var valueType = ReadWithDefault(reader, ClaimValueTypes.String); |
|||
var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); |
|||
var originalIssuer = ReadWithDefault(reader, issuer); |
|||
|
|||
var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); |
|||
|
|||
// Read the number of properties stored in the claim.
|
|||
var count = reader.ReadInt32(); |
|||
|
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
var key = reader.ReadString(); |
|||
var propertyValue = reader.ReadString(); |
|||
|
|||
claim.Properties.Add(key, propertyValue); |
|||
} |
|||
|
|||
return claim; |
|||
} |
|||
|
|||
static ImmutableDictionary<string, string> ReadProperties(BinaryReader reader, int version) |
|||
{ |
|||
if (version != reader.ReadInt32()) |
|||
{ |
|||
return ImmutableDictionary.Create<string, string>(); |
|||
} |
|||
|
|||
var properties = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal); |
|||
var count = reader.ReadInt32(); |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
properties.Add(reader.ReadString(), reader.ReadString()); |
|||
} |
|||
|
|||
return properties.ToImmutable(); |
|||
} |
|||
|
|||
static string ReadWithDefault(BinaryReader reader, string defaultValue) |
|||
{ |
|||
var value = reader.ReadString(); |
|||
|
|||
if (string.Equals(value, "\0", StringComparison.Ordinal)) |
|||
{ |
|||
return defaultValue; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
static string GetProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? value : null; |
|||
|
|||
static IEnumerable<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values<string>() : Enumerable.Empty<string>(); |
|||
|
|||
static DateTimeOffset? GetDateProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? (DateTimeOffset?) |
|||
DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; |
|||
} |
|||
|
|||
public void WriteToken([NotNull] BinaryWriter writer, [NotNull] ClaimsPrincipal principal) |
|||
{ |
|||
if (writer == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(writer)); |
|||
} |
|||
|
|||
if (principal == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(principal)); |
|||
} |
|||
|
|||
var properties = new Dictionary<string, string>(); |
|||
|
|||
// Unlike ASP.NET Core Data Protection-based tokens, tokens serialized using the new format
|
|||
// can't include authentication properties. To ensure tokens can be used with previous versions
|
|||
// of OpenIddict are issued, well-known claims are manually mapped to their properties equivalents.
|
|||
|
|||
SetProperty(properties, Properties.Issued, principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); |
|||
SetProperty(properties, Properties.Expires, principal.GetExpirationDate()?.ToString("r", CultureInfo.InvariantCulture)); |
|||
|
|||
SetProperty(properties, Properties.AccessTokenLifetime, principal.GetClaim(Claims.Private.AccessTokenLifetime)); |
|||
SetProperty(properties, Properties.AuthorizationCodeLifetime, principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); |
|||
SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime)); |
|||
SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime)); |
|||
|
|||
SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge)); |
|||
SetProperty(properties, Properties.CodeChallengeMethod, principal.GetClaim(Claims.Private.CodeChallengeMethod)); |
|||
|
|||
SetProperty(properties, Properties.InternalAuthorizationId, principal.GetInternalAuthorizationId()); |
|||
SetProperty(properties, Properties.InternalTokenId, principal.GetInternalTokenId()); |
|||
|
|||
SetProperty(properties, Properties.Nonce, principal.GetClaim(Claims.Private.Nonce)); |
|||
SetProperty(properties, Properties.OriginalRedirectUri, principal.GetClaim(Claims.Private.RedirectUri)); |
|||
|
|||
SetArrayProperty(properties, Properties.Audiences, principal.GetAudiences()); |
|||
SetArrayProperty(properties, Properties.Presenters, principal.GetPresenters()); |
|||
SetArrayProperty(properties, Properties.Scopes, principal.GetScopes()); |
|||
|
|||
// Copy the principal and exclude the claim that were mapped to authentication properties.
|
|||
principal = principal.Clone(claim => claim.Type switch |
|||
{ |
|||
Claims.Audience => false, |
|||
Claims.ExpiresAt => false, |
|||
Claims.IssuedAt => false, |
|||
|
|||
Claims.Private.AccessTokenLifetime => false, |
|||
Claims.Private.AuthorizationCodeLifetime => false, |
|||
Claims.Private.AuthorizationId => false, |
|||
Claims.Private.CodeChallenge => false, |
|||
Claims.Private.CodeChallengeMethod => false, |
|||
Claims.Private.IdentityTokenLifetime => false, |
|||
Claims.Private.Nonce => false, |
|||
Claims.Private.Presenters => false, |
|||
Claims.Private.RedirectUri => false, |
|||
Claims.Private.RefreshTokenLifetime => false, |
|||
Claims.Private.Scopes => false, |
|||
Claims.Private.TokenId => false, |
|||
|
|||
_ => true |
|||
}); |
|||
|
|||
Write(writer, version: 5, principal.Identity.AuthenticationType, principal, properties); |
|||
writer.Flush(); |
|||
|
|||
// Note: the following local methods closely matches the logic used by ASP.NET Core's
|
|||
// authentication stack and MUST NOT be modified to ensure tokens encrypted using
|
|||
// the OpenID Connect server middleware can be read by OpenIddict (and vice-versa).
|
|||
|
|||
static void Write(BinaryWriter writer, int version, string scheme, |
|||
ClaimsPrincipal principal, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
writer.Write(version); |
|||
writer.Write(scheme); |
|||
|
|||
// Write the number of identities contained in the principal.
|
|||
writer.Write(principal.Identities.Count()); |
|||
|
|||
foreach (var identity in principal.Identities) |
|||
{ |
|||
WriteIdentity(writer, identity); |
|||
} |
|||
|
|||
WriteProperties(writer, version, properties); |
|||
} |
|||
|
|||
static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) |
|||
{ |
|||
writer.Write(identity.AuthenticationType ?? string.Empty); |
|||
WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); |
|||
WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); |
|||
|
|||
// Write the number of claims contained in the identity.
|
|||
writer.Write(identity.Claims.Count()); |
|||
|
|||
foreach (var claim in identity.Claims) |
|||
{ |
|||
WriteClaim(writer, claim); |
|||
} |
|||
|
|||
var bootstrap = identity.BootstrapContext as string; |
|||
if (!string.IsNullOrEmpty(bootstrap)) |
|||
{ |
|||
writer.Write(true); |
|||
writer.Write(bootstrap); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
writer.Write(false); |
|||
} |
|||
|
|||
if (identity.Actor != null) |
|||
{ |
|||
writer.Write(true); |
|||
WriteIdentity(writer, identity.Actor); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
writer.Write(false); |
|||
} |
|||
} |
|||
|
|||
static void WriteClaim(BinaryWriter writer, Claim claim) |
|||
{ |
|||
if (writer == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(writer)); |
|||
} |
|||
|
|||
if (claim == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(claim)); |
|||
} |
|||
|
|||
WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); |
|||
writer.Write(claim.Value); |
|||
WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); |
|||
WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); |
|||
WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); |
|||
|
|||
// Write the number of properties contained in the claim.
|
|||
writer.Write(claim.Properties.Count); |
|||
|
|||
foreach (var property in claim.Properties) |
|||
{ |
|||
writer.Write(property.Key ?? string.Empty); |
|||
writer.Write(property.Value ?? string.Empty); |
|||
} |
|||
} |
|||
|
|||
static void WriteProperties(BinaryWriter writer, int version, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
writer.Write(version); |
|||
writer.Write(properties.Count); |
|||
|
|||
foreach (var property in properties) |
|||
{ |
|||
writer.Write(property.Key ?? string.Empty); |
|||
writer.Write(property.Value ?? string.Empty); |
|||
} |
|||
} |
|||
|
|||
static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) |
|||
=> writer.Write(string.Equals(value, defaultValue, StringComparison.Ordinal) ? "\0" : value); |
|||
|
|||
static void SetProperty(IDictionary<string, string> properties, string name, string value) |
|||
{ |
|||
if (string.IsNullOrEmpty(value)) |
|||
{ |
|||
properties.Remove(name); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
properties[name] = value; |
|||
} |
|||
} |
|||
|
|||
static void SetArrayProperty(IDictionary<string, string> properties, string name, IEnumerable<string> values) |
|||
{ |
|||
var array = new JArray(values); |
|||
if (array.Count == 0) |
|||
{ |
|||
properties.Remove(name); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
properties[name] = array.ToString(Formatting.None); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,836 +0,0 @@ |
|||
/* |
|||
* 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.Globalization; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Security.Claims; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using Newtonsoft.Json; |
|||
using Newtonsoft.Json.Linq; |
|||
using OpenIddict.Abstractions; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters; |
|||
using static OpenIddict.Server.OpenIddictServerEvents; |
|||
using static OpenIddict.Server.OpenIddictServerHandlers.Serialization; |
|||
using Properties = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Properties; |
|||
|
|||
namespace OpenIddict.Server.DataProtection |
|||
{ |
|||
public static partial class OpenIddictServerDataProtectionHandlers |
|||
{ |
|||
public static class Serialization |
|||
{ |
|||
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
|||
/* |
|||
* Access token serialization: |
|||
*/ |
|||
AttachAccessTokenSerializationProtector.Descriptor, |
|||
SerializeDataProtectionToken<SerializeAccessTokenContext>.Descriptor, |
|||
|
|||
/* |
|||
* Authorization code serialization: |
|||
*/ |
|||
AttachAuthorizationCodeSerializationProtector.Descriptor, |
|||
SerializeDataProtectionToken<SerializeAuthorizationCodeContext>.Descriptor, |
|||
|
|||
/* |
|||
* Refresh token serialization: |
|||
*/ |
|||
AttachRefreshTokenSerializationProtector.Descriptor, |
|||
SerializeDataProtectionToken<SerializeRefreshTokenContext>.Descriptor, |
|||
|
|||
/* |
|||
* Access token deserialization: |
|||
*/ |
|||
AttachAccessTokenDeserializationProtector.Descriptor, |
|||
DeserializeDataProtectionToken<DeserializeAccessTokenContext>.Descriptor, |
|||
|
|||
/* |
|||
* Authorization code deserialization: |
|||
*/ |
|||
AttachAuthorizationCodeDeserializationProtector.Descriptor, |
|||
DeserializeDataProtectionToken<DeserializeAuthorizationCodeContext>.Descriptor, |
|||
|
|||
/* |
|||
* Refresh token deserialization: |
|||
*/ |
|||
AttachRefreshTokenDeserializationProtector.Descriptor, |
|||
DeserializeDataProtectionToken<DeserializeRefreshTokenContext>.Descriptor); |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of generating a Data Protection token.
|
|||
/// </summary>
|
|||
public class SerializeDataProtectionToken<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>() |
|||
.AddFilter<RequirePreferDataProtectionFormatEnabled>() |
|||
.UseSingletonHandler<SerializeDataProtectionToken<TContext>>() |
|||
.SetOrder(SerializeJwtBearerToken<TContext>.Descriptor.Order - 5000) |
|||
.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.Properties.TryGetValue(Properties.DataProtector, out var property) || |
|||
!(property is IDataProtector protector)) |
|||
{ |
|||
throw new InvalidOperationException(new StringBuilder() |
|||
.Append("No suitable data protector was found for the specified token type.") |
|||
.Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") |
|||
.ToString()); |
|||
} |
|||
|
|||
var properties = new Dictionary<string, string>(); |
|||
|
|||
// Unlike ASP.NET Core Data Protection-based tokens, tokens serialized using the new format
|
|||
// can't include authentication properties. To ensure tokens can be used with previous versions
|
|||
// of OpenIddict are issued, well-known claims are manually mapped to their properties equivalents.
|
|||
|
|||
SetProperty(properties, Properties.AccessTokenLifetime, |
|||
context.Principal.GetClaim(Claims.Private.AccessTokenLifetime)); |
|||
|
|||
SetProperty(properties, Properties.AuthorizationCodeLifetime, |
|||
context.Principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); |
|||
|
|||
SetProperty(properties, Properties.CodeChallenge, |
|||
context.Principal.GetClaim(Claims.Private.CodeChallenge)); |
|||
|
|||
SetProperty(properties, Properties.CodeChallengeMethod, |
|||
context.Principal.GetClaim(Claims.Private.CodeChallengeMethod)); |
|||
|
|||
SetProperty(properties, Properties.Expires, |
|||
context.Principal.GetExpirationDate()?.ToString("r", CultureInfo.InvariantCulture)); |
|||
|
|||
SetProperty(properties, Properties.IdentityTokenLifetime, |
|||
context.Principal.GetClaim(Claims.Private.IdentityTokenLifetime)); |
|||
|
|||
SetProperty(properties, Properties.InternalAuthorizationId, context.Principal.GetInternalAuthorizationId()); |
|||
SetProperty(properties, Properties.InternalTokenId, context.Principal.GetInternalTokenId()); |
|||
|
|||
SetProperty(properties, Properties.Issued, |
|||
context.Principal.GetCreationDate()?.ToString("r", CultureInfo.InvariantCulture)); |
|||
|
|||
SetProperty(properties, Properties.OriginalRedirectUri, |
|||
context.Principal.GetClaim(Claims.Private.RedirectUri)); |
|||
|
|||
SetProperty(properties, Properties.RefreshTokenLifetime, |
|||
context.Principal.GetClaim(Claims.Private.RefreshTokenLifetime)); |
|||
|
|||
SetArrayProperty(properties, Properties.Audiences, context.Principal.GetAudiences()); |
|||
SetArrayProperty(properties, Properties.Presenters, context.Principal.GetPresenters()); |
|||
SetArrayProperty(properties, Properties.Scopes, context.Principal.GetScopes()); |
|||
|
|||
using var buffer = new MemoryStream(); |
|||
using var writer = new BinaryWriter(buffer); |
|||
|
|||
Write(writer, version: 5, context.Principal.Identity.AuthenticationType, context.Principal, properties); |
|||
writer.Flush(); |
|||
|
|||
context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); |
|||
context.HandleSerialization(); |
|||
|
|||
return default; |
|||
|
|||
// Note: the following local methods closely matches the logic used by ASP.NET Core's
|
|||
// authentication stack and MUST NOT be modified to ensure tokens encrypted using
|
|||
// the OpenID Connect server middleware can be read by OpenIddict (and vice-versa).
|
|||
|
|||
static void Write(BinaryWriter writer, int version, string scheme, |
|||
ClaimsPrincipal principal, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
writer.Write(version); |
|||
writer.Write(scheme); |
|||
|
|||
// Write the number of identities contained in the principal.
|
|||
writer.Write(principal.Identities.Count()); |
|||
|
|||
foreach (var identity in principal.Identities) |
|||
{ |
|||
WriteIdentity(writer, identity); |
|||
} |
|||
|
|||
WriteProperties(writer, version, properties); |
|||
} |
|||
|
|||
static void WriteIdentity(BinaryWriter writer, ClaimsIdentity identity) |
|||
{ |
|||
writer.Write(identity.AuthenticationType ?? string.Empty); |
|||
WriteWithDefault(writer, identity.NameClaimType, ClaimsIdentity.DefaultNameClaimType); |
|||
WriteWithDefault(writer, identity.RoleClaimType, ClaimsIdentity.DefaultRoleClaimType); |
|||
|
|||
// Write the number of claims contained in the identity.
|
|||
writer.Write(identity.Claims.Count()); |
|||
|
|||
foreach (var claim in identity.Claims) |
|||
{ |
|||
WriteClaim(writer, claim); |
|||
} |
|||
|
|||
var bootstrap = identity.BootstrapContext as string; |
|||
if (!string.IsNullOrEmpty(bootstrap)) |
|||
{ |
|||
writer.Write(true); |
|||
writer.Write(bootstrap); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
writer.Write(false); |
|||
} |
|||
|
|||
if (identity.Actor != null) |
|||
{ |
|||
writer.Write(true); |
|||
WriteIdentity(writer, identity.Actor); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
writer.Write(false); |
|||
} |
|||
} |
|||
|
|||
static void WriteClaim(BinaryWriter writer, Claim claim) |
|||
{ |
|||
if (writer == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(writer)); |
|||
} |
|||
|
|||
if (claim == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(claim)); |
|||
} |
|||
|
|||
WriteWithDefault(writer, claim.Type, claim.Subject?.NameClaimType ?? ClaimsIdentity.DefaultNameClaimType); |
|||
writer.Write(claim.Value); |
|||
WriteWithDefault(writer, claim.ValueType, ClaimValueTypes.String); |
|||
WriteWithDefault(writer, claim.Issuer, ClaimsIdentity.DefaultIssuer); |
|||
WriteWithDefault(writer, claim.OriginalIssuer, claim.Issuer); |
|||
|
|||
// Write the number of properties contained in the claim.
|
|||
writer.Write(claim.Properties.Count); |
|||
|
|||
foreach (var property in claim.Properties) |
|||
{ |
|||
writer.Write(property.Key ?? string.Empty); |
|||
writer.Write(property.Value ?? string.Empty); |
|||
} |
|||
} |
|||
|
|||
static void WriteProperties(BinaryWriter writer, int version, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
writer.Write(version); |
|||
writer.Write(properties.Count); |
|||
|
|||
foreach (var property in properties) |
|||
{ |
|||
writer.Write(property.Key ?? string.Empty); |
|||
writer.Write(property.Value ?? string.Empty); |
|||
} |
|||
} |
|||
|
|||
static void WriteWithDefault(BinaryWriter writer, string value, string defaultValue) |
|||
=> writer.Write(string.Equals(value, defaultValue, StringComparison.Ordinal) ? "\0" : value); |
|||
|
|||
static void SetProperty(IDictionary<string, string> properties, string name, string value) |
|||
{ |
|||
if (string.IsNullOrEmpty(value)) |
|||
{ |
|||
properties.Remove(name); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
properties[name] = value; |
|||
} |
|||
} |
|||
|
|||
static void SetArrayProperty(IDictionary<string, string> properties, string name, IEnumerable<string> values) |
|||
{ |
|||
var array = new JArray(values); |
|||
if (array.Count == 0) |
|||
{ |
|||
properties.Remove(name); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
properties[name] = array.ToString(Formatting.None); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of unprotecting a Data Protection token.
|
|||
/// </summary>
|
|||
public class DeserializeDataProtectionToken<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<DeserializeDataProtectionToken<TContext>>() |
|||
.SetOrder(DeserializeJwtBearerToken<TContext>.Descriptor.Order - 5000) |
|||
.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.Properties.TryGetValue(Properties.DataProtector, out var property) || |
|||
!(property is IDataProtector protector)) |
|||
{ |
|||
throw new InvalidOperationException(new StringBuilder() |
|||
.Append("No suitable data protector was found for the specified token type.") |
|||
.Append("This may indicate that the OpenIddict Data Protection services were not correctly registered.") |
|||
.ToString()); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(context.Token))); |
|||
using var reader = new BinaryReader(buffer); |
|||
|
|||
var (principal, properties) = Read(reader, version: 5); |
|||
if (principal == null) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
context.Principal = principal; |
|||
|
|||
// Tokens serialized using the ASP.NET Core Data Protection stack are compound
|
|||
// of both claims and special authentication properties. To ensure existing tokens
|
|||
// can be reused, well-known properties are manually mapped to their claims equivalents.
|
|||
|
|||
context.Principal |
|||
.SetAudiences(GetArrayProperty(properties, Properties.Audiences)) |
|||
.SetCreationDate(GetDateProperty(properties, Properties.Issued)) |
|||
.SetExpirationDate(GetDateProperty(properties, Properties.Expires)) |
|||
.SetPresenters(GetArrayProperty(properties, Properties.Presenters)) |
|||
.SetScopes(GetArrayProperty(properties, Properties.Scopes)) |
|||
|
|||
.SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) |
|||
.SetClaim(Claims.Private.AuthorizationCodeLifetime, GetProperty(properties, Properties.AuthorizationCodeLifetime)) |
|||
.SetClaim(Claims.Private.AuthorizationId, GetProperty(properties, Properties.InternalAuthorizationId)) |
|||
.SetClaim(Claims.Private.CodeChallenge, GetProperty(properties, Properties.CodeChallenge)) |
|||
.SetClaim(Claims.Private.CodeChallengeMethod, GetProperty(properties, Properties.CodeChallengeMethod)) |
|||
.SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) |
|||
.SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) |
|||
.SetClaim(Claims.Private.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) |
|||
.SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) |
|||
|
|||
// 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.
|
|||
.SetClaim(Claims.Private.TokenUsage, (string) context.Properties[Properties.TokenUsage]); |
|||
|
|||
context.HandleDeserialization(); |
|||
|
|||
return default; |
|||
} |
|||
|
|||
catch (Exception exception) |
|||
{ |
|||
context.Logger.LogTrace(exception, "An exception occured while deserializing a token."); |
|||
|
|||
return default; |
|||
} |
|||
|
|||
static (ClaimsPrincipal principal, ImmutableDictionary<string, string> properties) Read(BinaryReader reader, int version) |
|||
{ |
|||
if (version != reader.ReadInt32()) |
|||
{ |
|||
return (null, ImmutableDictionary.Create<string, string>()); |
|||
} |
|||
|
|||
// Read the authentication scheme associated to the ticket.
|
|||
_ = reader.ReadString(); |
|||
|
|||
// Read the number of identities stored in the serialized payload.
|
|||
var count = reader.ReadInt32(); |
|||
if (count < 0) |
|||
{ |
|||
return (null, ImmutableDictionary.Create<string, string>()); |
|||
} |
|||
|
|||
var identities = new ClaimsIdentity[count]; |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
identities[index] = ReadIdentity(reader); |
|||
} |
|||
|
|||
var properties = ReadProperties(reader, version); |
|||
|
|||
return (new ClaimsPrincipal(identities), properties); |
|||
} |
|||
|
|||
static ClaimsIdentity ReadIdentity(BinaryReader reader) |
|||
{ |
|||
var identity = new ClaimsIdentity( |
|||
authenticationType: reader.ReadString(), |
|||
nameType: ReadWithDefault(reader, ClaimsIdentity.DefaultNameClaimType), |
|||
roleType: ReadWithDefault(reader, ClaimsIdentity.DefaultRoleClaimType)); |
|||
|
|||
// Read the number of claims contained in the serialized identity.
|
|||
var count = reader.ReadInt32(); |
|||
|
|||
for (int index = 0; index != count; ++index) |
|||
{ |
|||
var claim = ReadClaim(reader, identity); |
|||
|
|||
identity.AddClaim(claim); |
|||
} |
|||
|
|||
// Determine whether the identity has a bootstrap context attached.
|
|||
if (reader.ReadBoolean()) |
|||
{ |
|||
identity.BootstrapContext = reader.ReadString(); |
|||
} |
|||
|
|||
// Determine whether the identity has an actor identity attached.
|
|||
if (reader.ReadBoolean()) |
|||
{ |
|||
identity.Actor = ReadIdentity(reader); |
|||
} |
|||
|
|||
return identity; |
|||
} |
|||
|
|||
static Claim ReadClaim(BinaryReader reader, ClaimsIdentity identity) |
|||
{ |
|||
var type = ReadWithDefault(reader, identity.NameClaimType); |
|||
var value = reader.ReadString(); |
|||
var valueType = ReadWithDefault(reader, ClaimValueTypes.String); |
|||
var issuer = ReadWithDefault(reader, ClaimsIdentity.DefaultIssuer); |
|||
var originalIssuer = ReadWithDefault(reader, issuer); |
|||
|
|||
var claim = new Claim(type, value, valueType, issuer, originalIssuer, identity); |
|||
|
|||
// Read the number of properties stored in the claim.
|
|||
var count = reader.ReadInt32(); |
|||
|
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
var key = reader.ReadString(); |
|||
var propertyValue = reader.ReadString(); |
|||
|
|||
claim.Properties.Add(key, propertyValue); |
|||
} |
|||
|
|||
return claim; |
|||
} |
|||
|
|||
static ImmutableDictionary<string, string> ReadProperties(BinaryReader reader, int version) |
|||
{ |
|||
if (version != reader.ReadInt32()) |
|||
{ |
|||
return ImmutableDictionary.Create<string, string>(); |
|||
} |
|||
|
|||
var properties = ImmutableDictionary.CreateBuilder<string, string>(StringComparer.Ordinal); |
|||
var count = reader.ReadInt32(); |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
properties.Add(reader.ReadString(), reader.ReadString()); |
|||
} |
|||
|
|||
return properties.ToImmutable(); |
|||
} |
|||
|
|||
static string ReadWithDefault(BinaryReader reader, string defaultValue) |
|||
{ |
|||
var value = reader.ReadString(); |
|||
|
|||
if (string.Equals(value, "\0", StringComparison.Ordinal)) |
|||
{ |
|||
return defaultValue; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
static string GetProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? value : null; |
|||
|
|||
static IEnumerable<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? JArray.Parse(value).Values<string>() : Enumerable.Empty<string>(); |
|||
|
|||
static DateTimeOffset? GetDateProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
=> properties.TryGetValue(name, out var value) ? (DateTimeOffset?) |
|||
DateTimeOffset.ParseExact(value, "r", CultureInfo.InvariantCulture) : null; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to generate an access token.
|
|||
/// </summary>
|
|||
public class AttachAccessTokenSerializationProtector : IOpenIddictServerHandler<SerializeAccessTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachAccessTokenSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAccessTokenContext>() |
|||
.UseSingletonHandler<AttachAccessTokenSerializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"AccessTokenFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to generate an authorization code.
|
|||
/// </summary>
|
|||
public class AttachAuthorizationCodeSerializationProtector : IOpenIddictServerHandler<SerializeAuthorizationCodeContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachAuthorizationCodeSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeAuthorizationCodeContext>() |
|||
.UseSingletonHandler<AttachAuthorizationCodeSerializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"AuthorizationCodeFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to generate a refresh token.
|
|||
/// </summary>
|
|||
public class AttachRefreshTokenSerializationProtector : IOpenIddictServerHandler<SerializeRefreshTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachRefreshTokenSerializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<SerializeRefreshTokenContext>() |
|||
.UseSingletonHandler<AttachRefreshTokenSerializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"RefreshTokenFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to unprotect an access token.
|
|||
/// </summary>
|
|||
public class AttachAccessTokenDeserializationProtector : IOpenIddictServerHandler<DeserializeAccessTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachAccessTokenDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAccessTokenContext>() |
|||
.UseSingletonHandler<AttachAccessTokenDeserializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"AccessTokenFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.AccessToken; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to unprotect an authorization code.
|
|||
/// </summary>
|
|||
public class AttachAuthorizationCodeDeserializationProtector : IOpenIddictServerHandler<DeserializeAuthorizationCodeContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachAuthorizationCodeDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeAuthorizationCodeContext>() |
|||
.UseSingletonHandler<AttachAuthorizationCodeDeserializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"AuthorizationCodeFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.AuthorizationCode; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of populating the data protector needed to unprotect a refresh token.
|
|||
/// </summary>
|
|||
public class AttachRefreshTokenDeserializationProtector : IOpenIddictServerHandler<DeserializeRefreshTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options; |
|||
|
|||
public AttachRefreshTokenDeserializationProtector([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options) |
|||
=> _options = options; |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictServerHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictServerHandlerDescriptor.CreateBuilder<DeserializeRefreshTokenContext>() |
|||
.UseSingletonHandler<AttachRefreshTokenDeserializationProtector>() |
|||
.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)); |
|||
} |
|||
|
|||
// Note: the protector MUST be created with the same purposes used by the
|
|||
// OpenID Connect server middleware (aka ASOS) to guarantee compatibility.
|
|||
var purposes = new List<string>(capacity: 4) |
|||
{ |
|||
"OpenIdConnectServerHandler", |
|||
"RefreshTokenFormat", |
|||
"ASOS" |
|||
}; |
|||
|
|||
if (context.Options.UseReferenceTokens) |
|||
{ |
|||
purposes.Insert(index: 2, "UseReferenceTokens"); |
|||
} |
|||
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(purposes); |
|||
context.Properties[Properties.DataProtector] = protector; |
|||
context.Properties[Properties.TokenUsage] = TokenUsages.RefreshToken; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,229 +0,0 @@ |
|||
/* |
|||
* 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.Security.Claims; |
|||
using JetBrains.Annotations; |
|||
using Microsoft.IdentityModel.JsonWebTokens; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
|
|||
namespace OpenIddict.Server |
|||
{ |
|||
public static partial class OpenIddictServerEvents |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an abstract base class used for certain event contexts.
|
|||
/// </summary>
|
|||
public abstract class BaseSerializingContext : BaseContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="BaseSerializingContext"/> class.
|
|||
/// </summary>
|
|||
public BaseSerializingContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the security principal containing the claims to serialize.
|
|||
/// </summary>
|
|||
public ClaimsPrincipal Principal { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encrypting credentials used to encrypt the token.
|
|||
/// </summary>
|
|||
public EncryptingCredentials EncryptingCredentials { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the signing credentials used to sign the token.
|
|||
/// </summary>
|
|||
public SigningCredentials SigningCredentials { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the security token handler used to serialize the token.
|
|||
/// </summary>
|
|||
public JsonWebTokenHandler SecurityTokenHandler { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the token returned to the client application.
|
|||
/// </summary>
|
|||
public string Token { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the token usage.
|
|||
/// </summary>
|
|||
public string TokenUsage { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a boolean indicating whether the
|
|||
/// <see cref="HandleSerialization()"/> method was called.
|
|||
/// </summary>
|
|||
public bool IsHandled { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Marks the serialization process as handled by the application code.
|
|||
/// </summary>
|
|||
public void HandleSerialization() => IsHandled = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an abstract base class used for certain event contexts.
|
|||
/// </summary>
|
|||
public abstract class BaseDeserializingContext : BaseContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="BaseDeserializingContext"/> class.
|
|||
/// </summary>
|
|||
public BaseDeserializingContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the security principal containing the deserialized claims.
|
|||
/// </summary>
|
|||
public ClaimsPrincipal Principal { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the validation parameters used to verify the authenticity of access tokens.
|
|||
/// Note: this property is only used when <see cref="SecurityTokenHandler"/> is not <c>null</c>.
|
|||
/// </summary>
|
|||
public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the security token handler used to
|
|||
/// deserialize the authentication ticket.
|
|||
/// </summary>
|
|||
public JsonWebTokenHandler SecurityTokenHandler { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the token used by the client application.
|
|||
/// </summary>
|
|||
public string Token { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the token usage.
|
|||
/// </summary>
|
|||
public string TokenUsage { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a boolean indicating whether the
|
|||
/// <see cref="HandleDeserialization()"/> method was called.
|
|||
/// </summary>
|
|||
public bool IsHandled { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Marks the deserialization process as handled by the application code.
|
|||
/// </summary>
|
|||
public void HandleDeserialization() => IsHandled = true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when serializing an access token.
|
|||
/// </summary>
|
|||
public class SerializeAccessTokenContext : BaseSerializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="SerializeAccessTokenContext"/> class.
|
|||
/// </summary>
|
|||
public SerializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.AccessToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when serializing an authorization code.
|
|||
/// </summary>
|
|||
public class SerializeAuthorizationCodeContext : BaseSerializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="SerializeAuthorizationCodeContext"/> class.
|
|||
/// </summary>
|
|||
public SerializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.AuthorizationCode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when serializing an identity token.
|
|||
/// </summary>
|
|||
public class SerializeIdentityTokenContext : BaseSerializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="SerializeIdentityTokenContext"/> class.
|
|||
/// </summary>
|
|||
public SerializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.IdToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when serializing a refresh token.
|
|||
/// </summary>
|
|||
public class SerializeRefreshTokenContext : BaseSerializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="SerializeRefreshTokenContext"/> class.
|
|||
/// </summary>
|
|||
public SerializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.RefreshToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when deserializing an access token.
|
|||
/// </summary>
|
|||
public class DeserializeAccessTokenContext : BaseDeserializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="DeserializeAccessTokenContext"/> class.
|
|||
/// </summary>
|
|||
public DeserializeAccessTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.AccessToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when deserializing an authorization code.
|
|||
/// </summary>
|
|||
public class DeserializeAuthorizationCodeContext : BaseDeserializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="DeserializeAuthorizationCodeContext"/> class.
|
|||
/// </summary>
|
|||
public DeserializeAuthorizationCodeContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.AuthorizationCode; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when deserializing an identity token.
|
|||
/// </summary>
|
|||
public class DeserializeIdentityTokenContext : BaseDeserializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="DeserializeIdentityTokenContext"/> class.
|
|||
/// </summary>
|
|||
public DeserializeIdentityTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.IdToken; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called when deserializing a refresh token.
|
|||
/// </summary>
|
|||
public class DeserializeRefreshTokenContext : BaseDeserializingContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="DeserializeRefreshTokenContext"/> class.
|
|||
/// </summary>
|
|||
public DeserializeRefreshTokenContext([NotNull] OpenIddictServerTransaction transaction) |
|||
: base(transaction) |
|||
=> TokenUsage = TokenUsages.RefreshToken; |
|||
} |
|||
} |
|||
} |
|||
@ -1,610 +0,0 @@ |
|||
/* |
|||
* 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); |
|||
} |
|||
|
|||
return default; |
|||
} |
|||
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
@ -0,0 +1,147 @@ |
|||
/* |
|||
* 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.Linq; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.IdentityModel.JsonWebTokens; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using OpenIddict.Abstractions; |
|||
using static OpenIddict.Abstractions.OpenIddictConstants; |
|||
|
|||
namespace OpenIddict.Server |
|||
{ |
|||
public class OpenIddictServerTokenHandler : JsonWebTokenHandler |
|||
{ |
|||
public ValueTask<string> CreateTokenFromDescriptorAsync(SecurityTokenDescriptor descriptor) |
|||
{ |
|||
if (descriptor == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(descriptor)); |
|||
} |
|||
|
|||
if (descriptor.Subject == null) |
|||
{ |
|||
throw new ArgumentException("The subject associated with a descriptor cannot be null.", nameof(descriptor)); |
|||
} |
|||
|
|||
if (descriptor.Claims == null) |
|||
{ |
|||
throw new InvalidOperationException("The claims collection cannot be null or empty."); |
|||
} |
|||
|
|||
if (!descriptor.Claims.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type)) |
|||
{ |
|||
throw new InvalidOperationException("The token usage cannot be null or empty."); |
|||
} |
|||
|
|||
var destinations = new Dictionary<string, string[]>(StringComparer.Ordinal); |
|||
foreach (var group in descriptor.Subject.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) |
|||
{ |
|||
descriptor.Claims[Claims.Private.ClaimDestinations] = destinations; |
|||
} |
|||
|
|||
return new ValueTask<string>(base.CreateToken(descriptor)); |
|||
} |
|||
|
|||
public ValueTask<TokenValidationResult> ValidateTokenStringAsync(string token, TokenValidationParameters parameters) |
|||
{ |
|||
if (parameters == null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(parameters)); |
|||
} |
|||
|
|||
if (!parameters.PropertyBag.TryGetValue(Claims.Private.TokenUsage, out var type) || string.IsNullOrEmpty((string) type)) |
|||
{ |
|||
throw new InvalidOperationException("The token usage cannot be null or empty."); |
|||
} |
|||
|
|||
if (!CanReadToken(token)) |
|||
{ |
|||
return new ValueTask<TokenValidationResult>(new TokenValidationResult |
|||
{ |
|||
Exception = new SecurityTokenException("The token was not compatible with the JWT format."), |
|||
IsValid = false |
|||
}); |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var result = base.ValidateToken(token, parameters); |
|||
if (result == null || !result.IsValid) |
|||
{ |
|||
return new ValueTask<TokenValidationResult>(new TokenValidationResult |
|||
{ |
|||
Exception = result?.Exception, |
|||
IsValid = false |
|||
}); |
|||
} |
|||
|
|||
var assertion = ((JsonWebToken) result.SecurityToken)?.InnerToken ?? (JsonWebToken) result.SecurityToken; |
|||
|
|||
if (!assertion.TryGetPayloadValue(Claims.Private.TokenUsage, out string usage) || |
|||
!string.Equals(usage, (string) type, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
return new ValueTask<TokenValidationResult>(new TokenValidationResult |
|||
{ |
|||
Exception = new SecurityTokenException("The token usage associated to the token does not match the expected type."), |
|||
IsValid = false |
|||
}); |
|||
} |
|||
|
|||
// 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 result.ClaimsIdentity.Claims.Where(claim => claim.Type == definition.Key)) |
|||
{ |
|||
claim.SetDestinations(definition.Value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new ValueTask<TokenValidationResult>(result); |
|||
} |
|||
|
|||
catch (Exception exception) |
|||
{ |
|||
return new ValueTask<TokenValidationResult>(new TokenValidationResult |
|||
{ |
|||
Exception = exception, |
|||
IsValid = false |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue