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