40 changed files with 1749 additions and 167 deletions
@ -0,0 +1,23 @@ |
|||
using System.Data.Entity; |
|||
|
|||
namespace OpenIddict.Sandbox.AspNetCore.Server.Models |
|||
{ |
|||
public class ApplicationDbContext : DbContext |
|||
{ |
|||
public ApplicationDbContext() |
|||
: base("DefaultConnection") |
|||
{ |
|||
} |
|||
|
|||
protected override void OnModelCreating(DbModelBuilder modelBuilder) |
|||
{ |
|||
modelBuilder.UseOpenIddict(); |
|||
|
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
// Customize the ASP.NET Identity model and override the defaults if needed.
|
|||
// For example, you can rename the ASP.NET Identity table names and more.
|
|||
// Add your customizations after calling base.OnModelCreating(builder);
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
using Microsoft.EntityFrameworkCore; |
|||
|
|||
namespace OpenIddict.Sandbox.AspNetCore.Client.Models; |
|||
|
|||
public class ApplicationDbContext : DbContext |
|||
{ |
|||
public ApplicationDbContext(DbContextOptions options) |
|||
: base(options) |
|||
{ |
|||
} |
|||
|
|||
protected override void OnModelCreating(ModelBuilder modelBuilder) |
|||
{ |
|||
base.OnModelCreating(modelBuilder); |
|||
|
|||
// Customize the ASP.NET Identity model and override the defaults if needed.
|
|||
// For example, you can rename the ASP.NET Identity table names and more.
|
|||
// Add your customizations after calling base.OnModelCreating(modelBuilder);
|
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using OpenIddict.Sandbox.AspNetCore.Client.Models; |
|||
|
|||
namespace OpenIddict.Sandbox.AspNetCore.Client; |
|||
|
|||
public class Worker : IHostedService |
|||
{ |
|||
private readonly IServiceProvider _serviceProvider; |
|||
|
|||
public Worker(IServiceProvider serviceProvider) |
|||
=> _serviceProvider = serviceProvider; |
|||
|
|||
public async Task StartAsync(CancellationToken cancellationToken) |
|||
{ |
|||
await using var scope = _serviceProvider.CreateAsyncScope(); |
|||
|
|||
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); |
|||
await context.Database.EnsureCreatedAsync(cancellationToken); |
|||
} |
|||
|
|||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<configuration> |
|||
|
|||
<!-- To customize the asp.net core module uncomment and edit the following section. |
|||
For more info see https://go.microsoft.com/fwlink/?linkid=838655 --> |
|||
<!-- |
|||
<system.webServer> |
|||
<handlers> |
|||
<remove name="aspNetCore"/> |
|||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> |
|||
</handlers> |
|||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" /> |
|||
</system.webServer> |
|||
--> |
|||
|
|||
<system.webServer> |
|||
<security> |
|||
<requestFiltering> |
|||
<requestLimits maxQueryString="32768"/> |
|||
</requestFiltering> |
|||
</security> |
|||
</system.webServer> |
|||
|
|||
</configuration> |
|||
@ -1,24 +0,0 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<configuration> |
|||
|
|||
<!-- To customize the asp.net core module uncomment and edit the following section. |
|||
For more info see https://go.microsoft.com/fwlink/?linkid=838655 --> |
|||
<!-- |
|||
<system.webServer> |
|||
<handlers> |
|||
<remove name="aspNetCore"/> |
|||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/> |
|||
</handlers> |
|||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" /> |
|||
</system.webServer> |
|||
--> |
|||
|
|||
<system.webServer> |
|||
<security> |
|||
<requestFiltering> |
|||
<requestLimits maxQueryString="32768"/> |
|||
</requestFiltering> |
|||
</security> |
|||
</system.webServer> |
|||
|
|||
</configuration> |
|||
@ -0,0 +1,15 @@ |
|||
/* |
|||
* 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.Security.Claims; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
public interface IOpenIddictClientDataProtectionFormatter |
|||
{ |
|||
ClaimsPrincipal ReadToken(BinaryReader reader); |
|||
void WriteToken(BinaryWriter writer, ClaimsPrincipal principal); |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net461;netcoreapp3.1;net5.0;net6.0;netstandard2.0;netstandard2.1</TargetFrameworks> |
|||
</PropertyGroup> |
|||
|
|||
<PropertyGroup> |
|||
<Description>ASP.NET Core Data Protection integration package for the OpenIddict client services.</Description> |
|||
<PackageTags>$(PackageTags);client;dataprotection</PackageTags> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup |
|||
Condition=" '$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '3.0')) "> |
|||
<FrameworkReference Include="Microsoft.AspNetCore.App" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup |
|||
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '3.0'))) Or |
|||
('$(TargetFrameworkIdentifier)' == '.NETFramework') Or |
|||
('$(TargetFrameworkIdentifier)' == '.NETStandard') "> |
|||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<Using Include="OpenIddict.Abstractions" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" /> |
|||
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientEvents" Static="true" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" /> |
|||
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" /> |
|||
<Using Include="OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionHandlers" Static="true" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -0,0 +1,99 @@ |
|||
/* |
|||
* 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.ComponentModel; |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using OpenIddict.Client.DataProtection; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes the necessary methods required to configure the
|
|||
/// OpenIddict ASP.NET Core Data Protection integration.
|
|||
/// </summary>
|
|||
public class OpenIddictClientDataProtectionBuilder |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="OpenIddictClientDataProtectionBuilder"/>.
|
|||
/// </summary>
|
|||
/// <param name="services">The services collection.</param>
|
|||
public OpenIddictClientDataProtectionBuilder(IServiceCollection services) |
|||
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the services collection.
|
|||
/// </summary>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public IServiceCollection Services { get; } |
|||
|
|||
/// <summary>
|
|||
/// Amends the default OpenIddict client ASP.NET Core Data Protection configuration.
|
|||
/// </summary>
|
|||
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientDataProtectionBuilder"/>.</returns>
|
|||
public OpenIddictClientDataProtectionBuilder Configure(Action<OpenIddictClientDataProtectionOptions> configuration) |
|||
{ |
|||
if (configuration is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
Services.Configure(configuration); |
|||
|
|||
return this; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures OpenIddict to use a specific data protection provider
|
|||
/// instead of relying on the default instance provided by the DI container.
|
|||
/// </summary>
|
|||
/// <param name="provider">The data protection provider used to create token protectors.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientDataProtectionBuilder"/>.</returns>
|
|||
public OpenIddictClientDataProtectionBuilder UseDataProtectionProvider(IDataProtectionProvider provider) |
|||
{ |
|||
if (provider is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(provider)); |
|||
} |
|||
|
|||
return Configure(options => options.DataProtectionProvider = provider); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures OpenIddict to use a specific formatter instead of relying on the default instance.
|
|||
/// </summary>
|
|||
/// <param name="formatter">The formatter used to read and write tokens.</param>
|
|||
/// <returns>The <see cref="OpenIddictClientDataProtectionBuilder"/>.</returns>
|
|||
public OpenIddictClientDataProtectionBuilder UseFormatter(IOpenIddictClientDataProtectionFormatter formatter) |
|||
{ |
|||
if (formatter is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(formatter)); |
|||
} |
|||
|
|||
return Configure(options => options.Formatter = formatter); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configures OpenIddict to use the default token format (JWT) when issuing new state tokens.
|
|||
/// </summary>
|
|||
/// <returns>The <see cref="OpenIddictClientDataProtectionBuilder"/>.</returns>
|
|||
public OpenIddictClientDataProtectionBuilder PreferDefaultStateTokenFormat() |
|||
=> Configure(options => options.PreferDefaultStateTokenFormat = true); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override bool Equals(object? obj) => base.Equals(obj); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override int GetHashCode() => base.GetHashCode(); |
|||
|
|||
/// <inheritdoc/>
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public override string? ToString() => base.ToString(); |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
/* |
|||
* 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 Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.Extensions.Options; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
/// <summary>
|
|||
/// Contains the methods required to ensure that the OpenIddict ASP.NET Core Data Protection configuration is valid.
|
|||
/// </summary>
|
|||
public class OpenIddictClientDataProtectionConfiguration : IConfigureOptions<OpenIddictClientOptions>, |
|||
IPostConfigureOptions<OpenIddictClientDataProtectionOptions> |
|||
{ |
|||
private readonly IDataProtectionProvider _dataProtectionProvider; |
|||
|
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="OpenIddictClientDataProtectionConfiguration"/> class.
|
|||
/// </summary>
|
|||
/// <param name="dataProtectionProvider">The ASP.NET Core Data Protection provider.</param>
|
|||
public OpenIddictClientDataProtectionConfiguration(IDataProtectionProvider dataProtectionProvider) |
|||
=> _dataProtectionProvider = dataProtectionProvider; |
|||
|
|||
public void Configure(OpenIddictClientOptions options) |
|||
{ |
|||
if (options is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
// Register the built-in event handlers used by the OpenIddict Data Protection server components.
|
|||
options.Handlers.AddRange(OpenIddictClientDataProtectionHandlers.DefaultHandlers); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Populates the default OpenIddict ASP.NET Core Data Protection server options
|
|||
/// and ensures that the configuration is in a consistent and valid state.
|
|||
/// </summary>
|
|||
/// <param name="name">The name of the options instance to configure, if applicable.</param>
|
|||
/// <param name="options">The options instance to initialize.</param>
|
|||
public void PostConfigure(string name, OpenIddictClientDataProtectionOptions options) |
|||
{ |
|||
if (options is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(options)); |
|||
} |
|||
|
|||
options.DataProtectionProvider ??= _dataProtectionProvider; |
|||
} |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
/* |
|||
* 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. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
public static class OpenIddictClientDataProtectionConstants |
|||
{ |
|||
public static class Properties |
|||
{ |
|||
public const string Audiences = ".audiences"; |
|||
public const string CodeVerifier = ".code_verifier"; |
|||
public const string Expires = ".expires"; |
|||
public const string InternalTokenId = ".internal_token_id"; |
|||
public const string Issued = ".issued"; |
|||
public const string Nonce = ".nonce"; |
|||
public const string OriginalRedirectUri = ".original_redirect_uri"; |
|||
public const string Presenters = ".presenters"; |
|||
public const string Resources = ".resources"; |
|||
public const string Scopes = ".scopes"; |
|||
public const string StateTokenLifetime = ".state_token_lifetime"; |
|||
} |
|||
|
|||
public static class Purposes |
|||
{ |
|||
public static class Features |
|||
{ |
|||
public const string ReferenceTokens = "UseReferenceTokens"; |
|||
} |
|||
|
|||
public static class Formats |
|||
{ |
|||
public const string StateToken = "StateTokenFormat"; |
|||
} |
|||
|
|||
public static class Handlers |
|||
{ |
|||
public const string Client = "OpenIdConnectClientHandler"; |
|||
} |
|||
|
|||
public static class Schemes |
|||
{ |
|||
public const string Server = "ASOC"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
/* |
|||
* 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 Microsoft.Extensions.DependencyInjection.Extensions; |
|||
using Microsoft.Extensions.Options; |
|||
using OpenIddict.Client; |
|||
using OpenIddict.Client.DataProtection; |
|||
|
|||
namespace Microsoft.Extensions.DependencyInjection; |
|||
|
|||
/// <summary>
|
|||
/// Exposes extensions allowing to register the OpenIddict ASP.NET Core Data Protection client services.
|
|||
/// </summary>
|
|||
public static class OpenIddictClientDataProtectionExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Registers the OpenIddict ASP.NET Core Data Protection client services in the DI container
|
|||
/// and configures OpenIddict to validate and issue ASP.NET Data Protection-based tokens.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
|
|||
public static OpenIddictClientDataProtectionBuilder UseDataProtection(this OpenIddictClientBuilder builder) |
|||
{ |
|||
if (builder is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
builder.Services.AddDataProtection(); |
|||
|
|||
// Register the built-in server event handlers used by the OpenIddict Data Protection components.
|
|||
// Note: the order used here is not important, as the actual order is set in the options.
|
|||
builder.Services.TryAdd(OpenIddictClientDataProtectionHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); |
|||
|
|||
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
|
|||
builder.Services.TryAddEnumerable(new[] |
|||
{ |
|||
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientDataProtectionConfiguration>(), |
|||
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientDataProtectionOptions>, OpenIddictClientDataProtectionConfiguration>() |
|||
}); |
|||
|
|||
return new OpenIddictClientDataProtectionBuilder(builder.Services); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Registers the OpenIddict ASP.NET Core Data Protection client services in the DI container
|
|||
/// and configures OpenIddict to validate and issue ASP.NET Data Protection-based tokens.
|
|||
/// </summary>
|
|||
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
|
|||
/// <param name="configuration">The configuration delegate used to configure the client services.</param>
|
|||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|||
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
|
|||
public static OpenIddictClientBuilder UseDataProtection( |
|||
this OpenIddictClientBuilder builder, Action<OpenIddictClientDataProtectionBuilder> configuration) |
|||
{ |
|||
if (builder is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(builder)); |
|||
} |
|||
|
|||
if (configuration is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(configuration)); |
|||
} |
|||
|
|||
configuration(builder.UseDataProtection()); |
|||
|
|||
return builder; |
|||
} |
|||
} |
|||
@ -0,0 +1,383 @@ |
|||
/* |
|||
* 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.Collections.Immutable; |
|||
using System.Security.Claims; |
|||
using System.Text; |
|||
using System.Text.Encodings.Web; |
|||
using System.Text.Json; |
|||
using Properties = OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionConstants.Properties; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
public class OpenIddictClientDataProtectionFormatter : IOpenIddictClientDataProtectionFormatter |
|||
{ |
|||
public ClaimsPrincipal ReadToken(BinaryReader reader) |
|||
{ |
|||
if (reader is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(reader)); |
|||
} |
|||
|
|||
var (principal, properties) = Read(reader); |
|||
|
|||
// 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)) |
|||
.SetPresenters(GetArrayProperty(properties, Properties.Presenters)) |
|||
.SetResources(GetArrayProperty(properties, Properties.Resources)) |
|||
.SetScopes(GetArrayProperty(properties, Properties.Scopes)) |
|||
|
|||
.SetClaim(Claims.Private.CodeVerifier, GetProperty(properties, Properties.CodeVerifier)) |
|||
.SetClaim(Claims.Private.CreationDate, GetProperty(properties, Properties.Issued)) |
|||
.SetClaim(Claims.Private.ExpirationDate, GetProperty(properties, Properties.Expires)) |
|||
.SetClaim(Claims.Private.Nonce, GetProperty(properties, Properties.Nonce)) |
|||
.SetClaim(Claims.Private.RedirectUri, GetProperty(properties, Properties.OriginalRedirectUri)) |
|||
.SetClaim(Claims.Private.StateTokenLifetime, GetProperty(properties, Properties.StateTokenLifetime)) |
|||
.SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)); |
|||
|
|||
static (ClaimsPrincipal principal, IReadOnlyDictionary<string, string> properties) Read(BinaryReader reader) |
|||
{ |
|||
// Read the version of the format used to serialize the ticket.
|
|||
var version = reader.ReadInt32(); |
|||
if (version != 5) |
|||
{ |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0287)); |
|||
} |
|||
|
|||
// Read the authentication scheme associated to the ticket.
|
|||
_ = reader.ReadString(); |
|||
|
|||
// Read the number of identities stored in the serialized payload.
|
|||
var count = reader.ReadInt32(); |
|||
|
|||
var identities = new ClaimsIdentity[count]; |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
identities[index] = ReadIdentity(reader); |
|||
} |
|||
|
|||
var properties = ReadProperties(reader); |
|||
|
|||
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 IReadOnlyDictionary<string, string> ReadProperties(BinaryReader reader) |
|||
{ |
|||
// Read the version of the format used to serialize the properties.
|
|||
var version = reader.ReadInt32(); |
|||
if (version != 1) |
|||
{ |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0287)); |
|||
} |
|||
|
|||
var count = reader.ReadInt32(); |
|||
var properties = new Dictionary<string, string>(count, StringComparer.Ordinal); |
|||
for (var index = 0; index != count; ++index) |
|||
{ |
|||
properties.Add(reader.ReadString(), reader.ReadString()); |
|||
} |
|||
|
|||
return properties; |
|||
} |
|||
|
|||
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 ImmutableArray<string> GetArrayProperty(IReadOnlyDictionary<string, string> properties, string name) |
|||
{ |
|||
if (properties.TryGetValue(name, out var value)) |
|||
{ |
|||
using var document = JsonDocument.Parse(value); |
|||
var builder = ImmutableArray.CreateBuilder<string>(document.RootElement.GetArrayLength()); |
|||
|
|||
foreach (var element in document.RootElement.EnumerateArray()) |
|||
{ |
|||
var item = element.GetString(); |
|||
if (string.IsNullOrEmpty(item)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
builder.Add(item); |
|||
} |
|||
|
|||
return builder.ToImmutable(); |
|||
} |
|||
|
|||
return ImmutableArray.Create<string>(); |
|||
} |
|||
} |
|||
|
|||
public void WriteToken(BinaryWriter writer, ClaimsPrincipal principal) |
|||
{ |
|||
if (writer is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(writer)); |
|||
} |
|||
|
|||
if (principal is 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 (1.x/2.x), well-known claims are manually mapped to their properties equivalents.
|
|||
|
|||
SetProperty(properties, Properties.Issued, principal.GetClaim(Claims.Private.CreationDate)); |
|||
SetProperty(properties, Properties.Expires, principal.GetClaim(Claims.Private.ExpirationDate)); |
|||
|
|||
SetProperty(properties, Properties.StateTokenLifetime, principal.GetClaim(Claims.Private.StateTokenLifetime)); |
|||
|
|||
SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId()); |
|||
|
|||
SetProperty(properties, Properties.CodeVerifier, principal.GetClaim(Claims.Private.CodeVerifier)); |
|||
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.Resources, principal.GetResources()); |
|||
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 is not ( |
|||
Claims.Private.Audience or |
|||
Claims.Private.CodeVerifier or |
|||
Claims.Private.CreationDate or |
|||
Claims.Private.ExpirationDate or |
|||
Claims.Private.Nonce or |
|||
Claims.Private.Presenter or |
|||
Claims.Private.RedirectUri or |
|||
Claims.Private.Resource or |
|||
Claims.Private.Scope or |
|||
Claims.Private.StateTokenLifetime or |
|||
Claims.Private.TokenId)); |
|||
|
|||
Write(writer, 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, string? scheme, ClaimsPrincipal principal, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
// Write the version of the format used to serialize the ticket.
|
|||
writer.Write(/* version: */ 5); |
|||
writer.Write(scheme ?? string.Empty); |
|||
|
|||
// 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, 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 is not null) |
|||
{ |
|||
writer.Write(true); |
|||
WriteIdentity(writer, identity.Actor); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
writer.Write(false); |
|||
} |
|||
} |
|||
|
|||
static void WriteClaim(BinaryWriter writer, Claim claim) |
|||
{ |
|||
if (writer is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(writer)); |
|||
} |
|||
|
|||
if (claim is 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, IReadOnlyDictionary<string, string> properties) |
|||
{ |
|||
// Write the version of the format used to serialize the properties.
|
|||
writer.Write(/* version: */ 1); |
|||
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, ImmutableArray<string> values) |
|||
{ |
|||
if (values.IsDefaultOrEmpty) |
|||
{ |
|||
properties.Remove(name); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
using var stream = new MemoryStream(); |
|||
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions |
|||
{ |
|||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, |
|||
Indented = false |
|||
}); |
|||
|
|||
writer.WriteStartArray(); |
|||
|
|||
foreach (var value in values) |
|||
{ |
|||
writer.WriteStringValue(value); |
|||
} |
|||
|
|||
writer.WriteEndArray(); |
|||
writer.Flush(); |
|||
|
|||
properties[name] = Encoding.UTF8.GetString(stream.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,201 @@ |
|||
/* |
|||
* 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.Collections.Immutable; |
|||
using System.Security.Claims; |
|||
using Microsoft.AspNetCore.DataProtection; |
|||
using Microsoft.Extensions.Logging; |
|||
using Microsoft.Extensions.Options; |
|||
using Microsoft.IdentityModel.Tokens; |
|||
using static OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionConstants.Purposes; |
|||
using static OpenIddict.Client.OpenIddictClientHandlers.Protection; |
|||
using Schemes = OpenIddict.Client.DataProtection.OpenIddictClientDataProtectionConstants.Purposes.Schemes; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
public static partial class OpenIddictClientDataProtectionHandlers |
|||
{ |
|||
public static class Protection |
|||
{ |
|||
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
|||
/* |
|||
* Token validation: |
|||
*/ |
|||
ValidateDataProtectionToken.Descriptor, |
|||
|
|||
/* |
|||
* Token generation: |
|||
*/ |
|||
GenerateDataProtectionToken.Descriptor); |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible for validating tokens generated using Data Protection.
|
|||
/// </summary>
|
|||
public class ValidateDataProtectionToken : IOpenIddictClientHandler<ValidateTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictClientDataProtectionOptions> _options; |
|||
|
|||
public ValidateDataProtectionToken(IOptionsMonitor<OpenIddictClientDataProtectionOptions> options) |
|||
=> _options = options ?? throw new ArgumentNullException(nameof(options)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<ValidateTokenContext>() |
|||
.UseSingletonHandler<ValidateDataProtectionToken>() |
|||
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(ValidateTokenContext context) |
|||
{ |
|||
// If a principal was already attached, don't overwrite it.
|
|||
if (context.Principal is not null) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
// Note: ASP.NET Core Data Protection tokens always start with "CfDJ8", that corresponds
|
|||
// to the base64 representation of the magic "09 F0 C9 F0" header identifying DP payloads.
|
|||
if (!context.Token.StartsWith("CfDJ8", StringComparison.Ordinal)) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
// Note: unlike the equivalent handler in the server stack, the logic used here
|
|||
// is simpler as only state tokens are currently supported by the client stack.
|
|||
var principal = context.ValidTokenTypes.Count switch |
|||
{ |
|||
// If no valid token type was set, all supported token types are allowed.
|
|||
0 => ValidateToken(TokenTypeHints.StateToken), |
|||
|
|||
_ when context.ValidTokenTypes.Contains(TokenTypeHints.StateToken) |
|||
=> ValidateToken(TokenTypeHints.StateToken), |
|||
|
|||
_ => null // The token type is not supported by the Data Protection integration (e.g identity tokens).
|
|||
}; |
|||
|
|||
if (principal is null) |
|||
{ |
|||
context.Reject( |
|||
error: Errors.InvalidToken, |
|||
description: SR.GetResourceString(SR.ID2004), |
|||
uri: SR.FormatID8000(SR.ID2004)); |
|||
|
|||
return default; |
|||
} |
|||
|
|||
context.Principal = principal; |
|||
|
|||
context.Logger.LogTrace(SR.GetResourceString(SR.ID6152), context.Token, context.Principal.Claims); |
|||
|
|||
return default; |
|||
|
|||
ClaimsPrincipal? ValidateToken(string type) |
|||
{ |
|||
// Create a Data Protection protector using the provider registered in the options.
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch |
|||
{ |
|||
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
|
|||
TokenTypeHints.StateToken when !string.IsNullOrEmpty(context.TokenId) |
|||
=> new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, |
|||
TokenTypeHints.StateToken => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, |
|||
|
|||
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) |
|||
}); |
|||
|
|||
try |
|||
{ |
|||
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(context.Token))); |
|||
using var reader = new BinaryReader(buffer); |
|||
|
|||
// Note: since the data format relies on a data protector using different "purposes" strings
|
|||
// per token type, the token processed at this stage is guaranteed to be of the expected type.
|
|||
return _options.CurrentValue.Formatter.ReadToken(reader)?.SetTokenType(type); |
|||
} |
|||
|
|||
catch (Exception exception) |
|||
{ |
|||
context.Logger.LogTrace(exception, SR.GetResourceString(SR.ID6153), context.Token); |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible for generating a token using Data Protection.
|
|||
/// </summary>
|
|||
public class GenerateDataProtectionToken : IOpenIddictClientHandler<GenerateTokenContext> |
|||
{ |
|||
private readonly IOptionsMonitor<OpenIddictClientDataProtectionOptions> _options; |
|||
|
|||
public GenerateDataProtectionToken(IOptionsMonitor<OpenIddictClientDataProtectionOptions> options) |
|||
=> _options = options ?? throw new ArgumentNullException(nameof(options)); |
|||
|
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<GenerateTokenContext>() |
|||
.UseSingletonHandler<GenerateDataProtectionToken>() |
|||
.SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(GenerateTokenContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
// If an access token was already attached by another handler, don't overwrite it.
|
|||
if (!string.IsNullOrEmpty(context.Token)) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
if (context.TokenType switch |
|||
{ |
|||
TokenTypeHints.StateToken => _options.CurrentValue.PreferDefaultStateTokenFormat, |
|||
|
|||
_ => true // The token type is not supported by the Data Protection integration (e.g identity tokens).
|
|||
}) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
// Create a Data Protection protector using the provider registered in the options.
|
|||
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(context.TokenType switch |
|||
{ |
|||
// Note: reference tokens are encrypted using a different "purpose" string than non-reference tokens.
|
|||
TokenTypeHints.StateToken when !context.Options.DisableTokenStorage |
|||
=> new[] { Handlers.Client, Formats.StateToken, Features.ReferenceTokens, Schemes.Server }, |
|||
TokenTypeHints.StateToken => new[] { Handlers.Client, Formats.StateToken, Schemes.Server }, |
|||
|
|||
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0003)) |
|||
}); |
|||
|
|||
using var buffer = new MemoryStream(); |
|||
using var writer = new BinaryWriter(buffer); |
|||
|
|||
_options.CurrentValue.Formatter.WriteToken(writer, context.Principal); |
|||
|
|||
context.Token = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())); |
|||
|
|||
context.Logger.LogTrace(SR.GetResourceString(SR.ID6013), context.TokenType, |
|||
context.Token, context.Principal.Claims); |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
/* |
|||
* 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.Collections.Immutable; |
|||
using System.ComponentModel; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
[EditorBrowsable(EditorBrowsableState.Never)] |
|||
public static partial class OpenIddictClientDataProtectionHandlers |
|||
{ |
|||
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } |
|||
= ImmutableArray.CreateRange(Protection.DefaultHandlers); |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
/* |
|||
* 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 Microsoft.AspNetCore.DataProtection; |
|||
|
|||
namespace OpenIddict.Client.DataProtection; |
|||
|
|||
/// <summary>
|
|||
/// Provides various settings needed to configure the OpenIddict
|
|||
/// ASP.NET Core Data Protection server integration.
|
|||
/// </summary>
|
|||
public class OpenIddictClientDataProtectionOptions |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the data protection provider used to create the default
|
|||
/// data protectors used by the OpenIddict Data Protection client services.
|
|||
/// When this property is set to <see langword="null"/>, the data protection provider
|
|||
/// is directly retrieved from the dependency injection container.
|
|||
/// </summary>
|
|||
public IDataProtectionProvider DataProtectionProvider { get; set; } = default!; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the formatter used to read and write Data Protection tokens.
|
|||
/// </summary>
|
|||
public IOpenIddictClientDataProtectionFormatter Formatter { get; set; } |
|||
= new OpenIddictClientDataProtectionFormatter(); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a boolean indicating whether the default state token format should be
|
|||
/// used when issuing new state tokens. This property is set to <see langword="false"/> by default.
|
|||
/// </summary>
|
|||
public bool PreferDefaultStateTokenFormat { get; set; } |
|||
} |
|||
Loading…
Reference in new issue