From 5758c4a6d951f450befb242dcd780b595cb5457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 23 Apr 2022 16:14:39 +0200 Subject: [PATCH] Make the OpenIddict client stack stateful by default and introduce OpenIddict.Client.DataProtection --- OpenIddict.sln | 7 + .../Models/ApplicationDbContext.cs | 23 ++ .../OpenIddict.Sandbox.AspNet.Client.csproj | 2 + .../Startup.cs | 52 ++- .../Web.config | 40 +- .../Web.config | 9 +- .../Models/ApplicationDbContext.cs | 20 + ...penIddict.Sandbox.AspNetCore.Client.csproj | 8 + .../Startup.cs | 84 +++- .../Worker.cs | 21 + .../appsettings.json | 4 + .../web.config | 24 -- .../Models/ApplicationDbContext.cs | 4 +- ...penIddict.Sandbox.AspNetCore.Server.csproj | 1 + .../appsettings.json | 2 +- .../web.config | 24 -- .../OpenIddictResources.resx | 35 ++ .../OpenIddict.AspNetCore.csproj | 1 + ...OpenIddictClientDataProtectionFormatter.cs | 15 + .../OpenIddict.Client.DataProtection.csproj | 38 ++ .../OpenIddictClientDataProtectionBuilder.cs | 99 +++++ ...IddictClientDataProtectionConfiguration.cs | 53 +++ ...OpenIddictClientDataProtectionConstants.cs | 48 +++ ...penIddictClientDataProtectionExtensions.cs | 74 ++++ ...OpenIddictClientDataProtectionFormatter.cs | 383 ++++++++++++++++++ ...ClientDataProtectionHandlers.Protection.cs | 201 +++++++++ .../OpenIddictClientDataProtectionHandlers.cs | 17 + .../OpenIddictClientDataProtectionOptions.cs | 36 ++ .../OpenIddictClientBuilder.cs | 10 + .../OpenIddictClientEvents.Protection.cs | 12 + .../OpenIddictClientExtensions.cs | 3 + .../OpenIddictClientHandlerFilters.cs | 48 +++ .../OpenIddictClientHandlers.Protection.cs | 323 ++++++++++++++- .../OpenIddictClientHandlers.cs | 59 ++- .../OpenIddictClientOptions.cs | 8 + .../OpenIddictClientService.cs | 10 +- ...OpenIddictServerDataProtectionConstants.cs | 1 - ...OpenIddictServerDataProtectionFormatter.cs | 88 ++-- ...IddictValidationDataProtectionConstants.cs | 1 - ...IddictValidationDataProtectionFormatter.cs | 28 +- 40 files changed, 1749 insertions(+), 167 deletions(-) create mode 100644 sandbox/OpenIddict.Sandbox.AspNet.Client/Models/ApplicationDbContext.cs create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Models/ApplicationDbContext.cs create mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/Worker.cs delete mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config delete mode 100644 sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config create mode 100644 src/OpenIddict.Client.DataProtection/IOpenIddictClientDataProtectionFormatter.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionBuilder.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.cs create mode 100644 src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionOptions.cs diff --git a/OpenIddict.sln b/OpenIddict.sln index 6ccfc6c0..0afed57b 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -124,6 +124,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.AspNet.S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.AspNet.Client", "sandbox\OpenIddict.Sandbox.AspNet.Client\OpenIddict.Sandbox.AspNet.Client.csproj", "{BFF5B862-D5FB-4019-8647-C43E5E7EF97D}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.DataProtection", "src\OpenIddict.Client.DataProtection\OpenIddict.Client.DataProtection.csproj", "{E4D77737-4C73-4520-99E8-8A9E586C69A1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -294,6 +296,10 @@ Global {BFF5B862-D5FB-4019-8647-C43E5E7EF97D}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFF5B862-D5FB-4019-8647-C43E5E7EF97D}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFF5B862-D5FB-4019-8647-C43E5E7EF97D}.Release|Any CPU.Build.0 = Release|Any CPU + {E4D77737-4C73-4520-99E8-8A9E586C69A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4D77737-4C73-4520-99E8-8A9E586C69A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4D77737-4C73-4520-99E8-8A9E586C69A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4D77737-4C73-4520-99E8-8A9E586C69A1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -340,6 +346,7 @@ Global {3385BC80-7EBF-4581-8FC8-E18E05EECFA2} = {D544447C-D701-46BB-9A5B-C76C612A596B} {0DFA4EC2-035A-46D3-AAF6-4BF1DFBC1040} = {F47D1283-0EE9-4728-8026-58405C29B786} {BFF5B862-D5FB-4019-8647-C43E5E7EF97D} = {F47D1283-0EE9-4728-8026-58405C29B786} + {E4D77737-4C73-4520-99E8-8A9E586C69A1} = {D544447C-D701-46BB-9A5B-C76C612A596B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Models/ApplicationDbContext.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Models/ApplicationDbContext.cs new file mode 100644 index 00000000..d9e16fe4 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Models/ApplicationDbContext.cs @@ -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); + } + } +} diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj index fe3ec52c..eb3c3fd8 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj @@ -17,6 +17,8 @@ + + diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs index 0b777bfa..ef1a8b16 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Owin.Security.Cookies; using OpenIddict.Client; using OpenIddict.Client.Owin; +using OpenIddict.Sandbox.AspNetCore.Server.Models; using Owin; using static OpenIddict.Abstractions.OpenIddictConstants; @@ -33,6 +34,13 @@ namespace OpenIddict.Sandbox.AspNet.Client // Configure ASP.NET MVC 5.2 to use Autofac when activating controller instances. DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); + + // Create the database used by the OpenIddict client stack to store tokens. + // Note: in a real world application, this step should be part of a setup script. + using var scope = container.BeginLifetimeScope(); + + var context = scope.Resolve(); + context.Database.CreateIfNotExists(); } private static IContainer CreateContainer() @@ -40,33 +48,49 @@ namespace OpenIddict.Sandbox.AspNet.Client var services = new ServiceCollection(); services.AddOpenIddict() - .AddClient(options => + + // Register the OpenIddict core components. + .AddCore(options => { - // Add a client registration matching the client application definition in the server project. - options.AddRegistration(new OpenIddictClientRegistration - { - Issuer = new Uri("https://localhost:44349/", UriKind.Absolute), + // Configure OpenIddict to use the Entity Framework 6.x stores and models. + // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities. + options.UseEntityFramework() + .UseDbContext(); - ClientId = "mvc", - ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", - RedirectUri = new Uri("https://localhost:44378/signin-oidc", UriKind.Absolute), - Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } - }); + // Developers who prefer using MongoDB can remove the previous lines + // and configure OpenIddict to use the specified MongoDB database: + // options.UseMongoDb() + // .UseDatabase(new MongoClient().GetDatabase("openiddict")); + }) + // Register the OpenIddict client components. + .AddClient(options => + { // Enable the redirection endpoint needed to handle the callback stage. options.SetRedirectionEndpointUris("/signin-oidc"); - // Register the OWIN host and configure the OWIN-specific options. - options.UseOwin() - .EnableRedirectionEndpointPassthrough(); - // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); + // Register the OWIN host and configure the OWIN-specific options. + options.UseOwin() + .EnableRedirectionEndpointPassthrough(); + // Register the System.Net.Http integration. options.UseSystemNetHttp(); + + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(new OpenIddictClientRegistration + { + Issuer = new Uri("https://localhost:44349/", UriKind.Absolute), + + ClientId = "mvc", + ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", + RedirectUri = new Uri("https://localhost:44378/signin-oidc", UriKind.Absolute), + Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } + }); }); // Create a new Autofac container and import the OpenIddict services. diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config index c691c4b1..7d1994f5 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config @@ -4,6 +4,13 @@ https://go.microsoft.com/fwlink/?LinkId=301880 --> + + +
+ + + + @@ -12,15 +19,8 @@ - + - - - - - - - @@ -106,6 +106,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config b/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config index 136e09ee..00bc9860 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config @@ -9,7 +9,7 @@
- + @@ -20,17 +20,12 @@ - + - - - - - diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Models/ApplicationDbContext.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Models/ApplicationDbContext.cs new file mode 100644 index 00000000..92697166 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Models/ApplicationDbContext.cs @@ -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); + } +} diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj index 9452163a..cef16706 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj @@ -8,7 +8,15 @@ + + + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs index 310f024d..884a37a4 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs @@ -1,13 +1,32 @@ using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.EntityFrameworkCore; using OpenIddict.Client; +using OpenIddict.Sandbox.AspNetCore.Client.Models; +using Quartz; using static OpenIddict.Abstractions.OpenIddictConstants; namespace OpenIddict.Sandbox.AspNetCore.Client; public class Startup { + public Startup(IConfiguration configuration) + => Configuration = configuration; + + public IConfiguration Configuration { get; } + public void ConfigureServices(IServiceCollection services) { + services.AddDbContext(options => + { + // Configure the context to use Microsoft SQL Server. + options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); + + // Register the entity sets needed by OpenIddict. + // Note: use the generic overload if you need + // to replace the default OpenIddict entities. + options.UseOpenIddict(); + }); + services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; @@ -21,40 +40,75 @@ public class Startup options.SlidingExpiration = false; }); + // OpenIddict offers native integration with Quartz.NET to perform scheduled tasks + // (like pruning orphaned authorizations from the database) at regular intervals. + services.AddQuartz(options => + { + options.UseMicrosoftDependencyInjectionJobFactory(); + options.UseSimpleTypeLoader(); + options.UseInMemoryStore(); + }); + + // Register the Quartz.NET service and configure it to block shutdown until jobs are complete. + services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true); + services.AddOpenIddict() - .AddClient(options => + + // Register the OpenIddict core components. + .AddCore(options => { - // Add a client registration matching the client application definition in the server project. - options.AddRegistration(new OpenIddictClientRegistration - { - Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), + // Configure OpenIddict to use the Entity Framework Core stores and models. + // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities. + options.UseEntityFrameworkCore() + .UseDbContext(); - ClientId = "mvc", - ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", - RedirectUri = new Uri("https://localhost:44381/signin-oidc", UriKind.Absolute), - Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } - }); + // Developers who prefer using MongoDB can remove the previous lines + // and configure OpenIddict to use the specified MongoDB database: + // options.UseMongoDb() + // .UseDatabase(new MongoClient().GetDatabase("openiddict")); + // Enable Quartz.NET integration. + options.UseQuartz(); + }) + + // Register the OpenIddict client components. + .AddClient(options => + { // Enable the redirection endpoint needed to handle the callback stage. options.SetRedirectionEndpointUris("/signin-oidc"); - // Register the ASP.NET Core host and configure the ASP.NET Core-specific options. - options.UseAspNetCore() - .EnableStatusCodePagesIntegration() - .EnableRedirectionEndpointPassthrough(); - // Register the signing and encryption credentials used to protect // sensitive data like the state tokens produced by OpenIddict. options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); + // Register the ASP.NET Core host and configure the ASP.NET Core-specific options. + options.UseAspNetCore() + .EnableStatusCodePagesIntegration() + .EnableRedirectionEndpointPassthrough(); + // Register the System.Net.Http integration. options.UseSystemNetHttp(); + + // Add a client registration matching the client application definition in the server project. + options.AddRegistration(new OpenIddictClientRegistration + { + Issuer = new Uri("https://localhost:44395/", UriKind.Absolute), + + ClientId = "mvc", + ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", + RedirectUri = new Uri("https://localhost:44381/signin-oidc", UriKind.Absolute), + Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } + }); }); services.AddHttpClient(); services.AddControllersWithViews(); + + // Register the worker responsible for creating the database used to store tokens. + // Note: in a real world application, this step should be part of a setup script. + services.AddHostedService(); } public void Configure(IApplicationBuilder app) diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Worker.cs new file mode 100644 index 00000000..98c9a9f2 --- /dev/null +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/Worker.cs @@ -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(); + await context.Database.EnsureCreatedAsync(cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json index 8983e0fc..65776f4d 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/appsettings.json @@ -1,4 +1,8 @@ { + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=openiddict-sandbox-aspnetcore-client;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { "LogLevel": { "Default": "Information", diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config b/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config deleted file mode 100644 index c128c496..00000000 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Client/web.config +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs index 2b361345..fb135fc2 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Models/ApplicationDbContext.cs @@ -6,7 +6,9 @@ namespace OpenIddict.Sandbox.AspNetCore.Server.Models; public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) - : base(options) { } + : base(options) + { + } protected override void OnModelCreating(ModelBuilder builder) { diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj index d4ad7607..5f291bde 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/OpenIddict.Sandbox.AspNetCore.Server.csproj @@ -13,6 +13,7 @@ + diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json index ea8f476f..91cab7ae 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=openiddict-aspnetcore-sandbox;Trusted_Connection=True;MultipleActiveResultSets=true" + "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=openiddict-sandbox-aspnetcore-server;Trusted_Connection=True;MultipleActiveResultSets=true" }, "Logging": { diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config deleted file mode 100644 index c128c496..00000000 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/web.config +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 608b68dd..2a2e4643 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1220,6 +1220,41 @@ Note: when using a dependency injection container supporting middleware resoluti The OpenIddict client services cannot be resolved from the DI container. To register the server services, use 'services.AddOpenIddict().AddClient()'. + + + The core services must be registered when enabling the OpenIddict client feature. +To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'. +Alternatively, you can disable the token storage feature by calling 'services.AddOpenIddict().AddClient().DisableTokenStorage()'. + + + An error occurred while refreshing tokens. + Error: {0} + Error description: {1} + Error URI: {2} + + + An error occurred while preparing the token request. + Error: {0} + Error description: {1} + Error URI: {2} + + + An error occurred while sending the token request. + Error: {0} + Error description: {1} + Error URI: {2} + + + An error occurred while extracting the token response. + Error: {0} + Error description: {1} + Error URI: {2} + + + An error occurred while handling the token response. + Error: {0} + Error description: {1} + Error URI: {2} The security token is missing. diff --git a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj index e73cdcdf..d0afbdd6 100644 --- a/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj +++ b/src/OpenIddict.AspNetCore/OpenIddict.AspNetCore.csproj @@ -14,6 +14,7 @@ + diff --git a/src/OpenIddict.Client.DataProtection/IOpenIddictClientDataProtectionFormatter.cs b/src/OpenIddict.Client.DataProtection/IOpenIddictClientDataProtectionFormatter.cs new file mode 100644 index 00000000..b1e7c750 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/IOpenIddictClientDataProtectionFormatter.cs @@ -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); +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj b/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj new file mode 100644 index 00000000..e4801053 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddict.Client.DataProtection.csproj @@ -0,0 +1,38 @@ + + + + net461;netcoreapp3.1;net5.0;net6.0;netstandard2.0;netstandard2.1 + + + + ASP.NET Core Data Protection integration package for the OpenIddict client services. + $(PackageTags);client;dataprotection + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionBuilder.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionBuilder.cs new file mode 100644 index 00000000..84c2f4b1 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionBuilder.cs @@ -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; + +/// +/// Exposes the necessary methods required to configure the +/// OpenIddict ASP.NET Core Data Protection integration. +/// +public class OpenIddictClientDataProtectionBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictClientDataProtectionBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict client ASP.NET Core Data Protection configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientDataProtectionBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Configures OpenIddict to use a specific data protection provider + /// instead of relying on the default instance provided by the DI container. + /// + /// The data protection provider used to create token protectors. + /// The . + public OpenIddictClientDataProtectionBuilder UseDataProtectionProvider(IDataProtectionProvider provider) + { + if (provider is null) + { + throw new ArgumentNullException(nameof(provider)); + } + + return Configure(options => options.DataProtectionProvider = provider); + } + + /// + /// Configures OpenIddict to use a specific formatter instead of relying on the default instance. + /// + /// The formatter used to read and write tokens. + /// The . + public OpenIddictClientDataProtectionBuilder UseFormatter(IOpenIddictClientDataProtectionFormatter formatter) + { + if (formatter is null) + { + throw new ArgumentNullException(nameof(formatter)); + } + + return Configure(options => options.Formatter = formatter); + } + + /// + /// Configures OpenIddict to use the default token format (JWT) when issuing new state tokens. + /// + /// The . + public OpenIddictClientDataProtectionBuilder PreferDefaultStateTokenFormat() + => Configure(options => options.PreferDefaultStateTokenFormat = true); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => base.Equals(obj); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() => base.ToString(); +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs new file mode 100644 index 00000000..40c60ead --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConfiguration.cs @@ -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; + +/// +/// Contains the methods required to ensure that the OpenIddict ASP.NET Core Data Protection configuration is valid. +/// +public class OpenIddictClientDataProtectionConfiguration : IConfigureOptions, + IPostConfigureOptions +{ + private readonly IDataProtectionProvider _dataProtectionProvider; + + /// + /// Creates a new instance of the class. + /// + /// The ASP.NET Core Data Protection provider. + 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); + } + + /// + /// Populates the default OpenIddict ASP.NET Core Data Protection server options + /// and ensures that the configuration is in a consistent and valid state. + /// + /// The name of the options instance to configure, if applicable. + /// The options instance to initialize. + public void PostConfigure(string name, OpenIddictClientDataProtectionOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.DataProtectionProvider ??= _dataProtectionProvider; + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs new file mode 100644 index 00000000..66784ba6 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionConstants.cs @@ -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"; + } + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs new file mode 100644 index 00000000..e06fc840 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionExtensions.cs @@ -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; + +/// +/// Exposes extensions allowing to register the OpenIddict ASP.NET Core Data Protection client services. +/// +public static class OpenIddictClientDataProtectionExtensions +{ + /// + /// 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. + /// + /// The services builder used by OpenIddict to register new services. + /// This extension can be safely called multiple times. + /// The . + 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, OpenIddictClientDataProtectionConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientDataProtectionConfiguration>() + }); + + return new OpenIddictClientDataProtectionBuilder(builder.Services); + } + + /// + /// 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. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the client services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictClientBuilder UseDataProtection( + this OpenIddictClientBuilder builder, Action configuration) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseDataProtection()); + + return builder; + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs new file mode 100644 index 00000000..688131a4 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionFormatter.cs @@ -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 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 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(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 properties, string name) + => properties.TryGetValue(name, out var value) ? value : null; + + static ImmutableArray GetArrayProperty(IReadOnlyDictionary properties, string name) + { + if (properties.TryGetValue(name, out var value)) + { + using var document = JsonDocument.Parse(value); + var builder = ImmutableArray.CreateBuilder(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(); + } + } + + 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(); + + // 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 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 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 properties, string name, string? value) + { + if (string.IsNullOrEmpty(value)) + { + properties.Remove(name); + } + + else + { + properties[name] = value; + } + } + + static void SetArrayProperty(IDictionary properties, string name, ImmutableArray 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()); + } + } + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs new file mode 100644 index 00000000..8e734be9 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.Protection.cs @@ -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 DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Token validation: + */ + ValidateDataProtectionToken.Descriptor, + + /* + * Token generation: + */ + GenerateDataProtectionToken.Descriptor); + + /// + /// Contains the logic responsible for validating tokens generated using Data Protection. + /// + public class ValidateDataProtectionToken : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public ValidateDataProtectionToken(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + } + } + + /// + /// Contains the logic responsible for generating a token using Data Protection. + /// + public class GenerateDataProtectionToken : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public GenerateDataProtectionToken(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelToken.Descriptor.Order - 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + } +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.cs new file mode 100644 index 00000000..1d0c3341 --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionHandlers.cs @@ -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 DefaultHandlers { get; } + = ImmutableArray.CreateRange(Protection.DefaultHandlers); +} diff --git a/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionOptions.cs b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionOptions.cs new file mode 100644 index 00000000..be1f9e9c --- /dev/null +++ b/src/OpenIddict.Client.DataProtection/OpenIddictClientDataProtectionOptions.cs @@ -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; + +/// +/// Provides various settings needed to configure the OpenIddict +/// ASP.NET Core Data Protection server integration. +/// +public class OpenIddictClientDataProtectionOptions +{ + /// + /// 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 , the data protection provider + /// is directly retrieved from the dependency injection container. + /// + public IDataProtectionProvider DataProtectionProvider { get; set; } = default!; + + /// + /// Gets or sets the formatter used to read and write Data Protection tokens. + /// + public IOpenIddictClientDataProtectionFormatter Formatter { get; set; } + = new OpenIddictClientDataProtectionFormatter(); + + /// + /// 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 by default. + /// + public bool PreferDefaultStateTokenFormat { get; set; } +} diff --git a/src/OpenIddict.Client/OpenIddictClientBuilder.cs b/src/OpenIddict.Client/OpenIddictClientBuilder.cs index dd5eac16..d073e6f8 100644 --- a/src/OpenIddict.Client/OpenIddictClientBuilder.cs +++ b/src/OpenIddict.Client/OpenIddictClientBuilder.cs @@ -960,6 +960,16 @@ public class OpenIddictClientBuilder return Configure(options => options.Registrations.Add(registration)); } + /// + /// Disables token storage, so that no database entry is created + /// for the tokens and codes returned by the OpenIddict client. + /// Using this option is generally NOT recommended as it prevents + /// the tokens from being revoked (if needed). + /// + /// The . + public OpenIddictClientBuilder DisableTokenStorage() + => Configure(options => options.DisableTokenStorage = true); + /// /// Sets the relative or absolute URLs associated to the redirection endpoint. /// If an empty array is specified, the endpoint will be considered disabled. diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs index 7b742f5a..f837accd 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Protection.cs @@ -34,6 +34,18 @@ public static partial class OpenIddictClientEvents set => Transaction.Request = value; } + /// + /// Gets or sets a boolean indicating whether a token entry + /// should be created to persist token metadata in a database. + /// + public bool CreateTokenEntry { get; set; } + + /// + /// Gets or sets a boolean indicating whether the token payload + /// should be persisted alongside the token metadata in the database. + /// + public bool PersistTokenPayload { get; set; } + /// /// Gets or sets the security principal used to create the token. /// diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs index c0a76d10..4fee0c75 100644 --- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs +++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs @@ -49,8 +49,11 @@ public static class OpenIddictClientExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs index 6e7498d3..e69132fe 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -220,6 +220,38 @@ public static class OpenIddictClientHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if no token entry is created in the database. + /// + public class RequireTokenEntryCreated : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.CreateTokenEntry); + } + } + + /// + /// Represents a filter that excludes the associated handlers if the token payload is not persisted in the database. + /// + public class RequireTokenPayloadPersisted : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.PersistTokenPayload); + } + } + /// /// Represents a filter that excludes the associated handlers if no token request is expected to be sent. /// @@ -252,6 +284,22 @@ public static class OpenIddictClientHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if token storage was not enabled. + /// + public class RequireTokenStorageEnabled : IOpenIddictClientHandlerFilter + { + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!context.Options.DisableTokenStorage); + } + } + /// /// Represents a filter that excludes the associated handlers if no userinfo request is expected to be sent. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs index d0170bdd..9a0b4083 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Protection.cs @@ -8,6 +8,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; using System.Security.Claims; +using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; @@ -23,16 +24,21 @@ public static partial class OpenIddictClientHandlers * Token validation: */ ResolveTokenValidationParameters.Descriptor, + ValidateReferenceTokenIdentifier.Descriptor, ValidateIdentityModelToken.Descriptor, MapInternalClaims.Descriptor, + RestoreReferenceTokenProperties.Descriptor, ValidatePrincipal.Descriptor, ValidateExpirationDate.Descriptor, + ValidateTokenEntry.Descriptor, /* - * Token generation: - */ + * Token generation: + */ AttachSecurityCredentials.Descriptor, - GenerateIdentityModelToken.Descriptor); + CreateTokenEntry.Descriptor, + GenerateIdentityModelToken.Descriptor, + ConvertReferenceToken.Descriptor); /// /// Contains the logic responsible for resolving the validation parameters used to validate tokens. @@ -146,6 +152,74 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for validating reference token identifiers. + /// Note: this handler is not used when token storage is disabled. + /// + public class ValidateReferenceTokenIdentifier : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318)); + + public ValidateReferenceTokenIdentifier(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If the reference token cannot be found, don't return an error to allow another handler to validate it. + var token = await _tokenManager.FindByReferenceIdAsync(context.Token); + if (token is null) + { + return; + } + + // If the type associated with the token entry doesn't match one of the expected types, return an error. + if (!(context.ValidTokenTypes.Count switch + { + 0 => true, // If no specific token type is expected, accept all token types at this stage. + 1 => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ElementAt(0)), + _ => await _tokenManager.HasTypeAsync(token, context.ValidTokenTypes.ToImmutableArray()) + })) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2004), + uri: SR.FormatID8000(SR.ID2004)); + + return; + } + + var payload = await _tokenManager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0026)); + } + + // Replace the token parameter by the payload resolved from the token entry + // and store the identifier of the reference token so it can be later + // used to restore the properties associated with the token. + context.Token = payload; + context.TokenId = await _tokenManager.GetIdAsync(token); + } + } + /// /// Contains the logic responsible for validating tokens generated using IdentityModel. /// @@ -157,7 +231,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ResolveTokenValidationParameters.Descriptor.Order + 1_000) + .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -319,6 +393,54 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for restoring the properties associated with a reference token entry. + /// Note: this handler is not used when token storage is disabled. + /// + public class RestoreReferenceTokenProperties : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RestoreReferenceTokenProperties() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318)); + + public RestoreReferenceTokenProperties(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Principal is null || string.IsNullOrEmpty(context.TokenId)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(context.TokenId) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + } + } + /// /// Contains the logic responsible for rejecting authentication demands for which no valid principal was resolved. /// @@ -330,7 +452,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(MapInternalClaims.Descriptor.Order + 1_000) + .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -411,6 +533,69 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for authentication demands a token whose + /// associated token entry is no longer valid (e.g was revoked). + /// Note: this handler is not used when token storage is disabled. + /// + public class ValidateTokenEntry : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ValidateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0139)); + + public ValidateTokenEntry(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateExpirationDate.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ValidateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var identifier = context.Principal.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token is null || !await _tokenManager.HasStatusAsync(token, Statuses.Valid)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6005), identifier); + + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2019), + uri: SR.FormatID8000(SR.ID2019)); + + return; + } + + // Restore the creation/expiration dates/identifiers from the token entry metadata. + context.Principal.SetCreationDate(await _tokenManager.GetCreationDateAsync(token)) + .SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token)) + .SetAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token)) + .SetTokenId(await _tokenManager.GetIdAsync(token)) + .SetTokenType(await _tokenManager.GetTypeAsync(token)); + } + } + /// /// Contains the logic responsible for resolving the signing and encryption credentials used to protect tokens. /// @@ -443,6 +628,64 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for creating a token entry. + /// Note: this handler is not used when token storage is disabled. + /// + public class CreateTokenEntry : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public CreateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318)); + + public CreateTokenEntry(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.Principal.GetAuthorizationId(), + CreationDate = context.Principal.GetCreationDate(), + ExpirationDate = context.Principal.GetExpirationDate(), + Principal = context.Principal, + Status = Statuses.Valid, + Subject = null, + Type = context.TokenType + }; + + // Tokens produced by the client stack cannot have an application attached. + + var token = await _tokenManager.CreateAsync(descriptor) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0019)); + + var identifier = await _tokenManager.GetIdAsync(token); + + // Attach the token identifier to the principal so that it can be stored in the token. + context.Principal.SetTokenId(identifier); + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6012), context.TokenType, identifier); + } + } + /// /// Contains the logic responsible for generating a token using IdentityModel. /// @@ -454,7 +697,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(AttachSecurityCredentials.Descriptor.Order + 1_000) + .SetOrder(CreateTokenEntry.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -531,5 +774,73 @@ public static partial class OpenIddictClientHandlers return default; } } + + /// + /// Contains the logic responsible for converting the token to a reference token. + /// Note: this handler is not used when token storage is disabled. + /// + public class ConvertReferenceToken : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public ConvertReferenceToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318)); + + public ConvertReferenceToken(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateIdentityModelToken.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(GenerateTokenContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var identifier = context.Principal.GetTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0009)); + } + + var token = await _tokenManager.FindByIdAsync(identifier) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0021)); + + var descriptor = new OpenIddictTokenDescriptor(); + await _tokenManager.PopulateAsync(descriptor, token); + + // Attach the generated token to the token entry. + descriptor.Payload = context.Token; + descriptor.Principal = context.Principal; + + var data = new byte[256 / 8]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + + descriptor.ReferenceId = Base64UrlEncoder.Encode(data); + + await _tokenManager.UpdateAsync(token, descriptor); + + // Replace the returned token by the reference identifier. + context.Token = descriptor.ReferenceId; + + context.Logger.LogTrace(SR.GetResourceString(SR.ID6014), context.TokenType, identifier, descriptor.ReferenceId); + } + } } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 004382dc..23a53554 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -80,6 +80,8 @@ public static partial class OpenIddictClientHandlers ValidateUserinfoTokenWellknownClaims.Descriptor, ValidateUserinfoTokenSubject.Descriptor, + RedeemStateTokenEntry.Descriptor, + /* * Challenge processing: */ @@ -1191,7 +1193,8 @@ public static partial class OpenIddictClientHandlers // Resolve the hash algorithm corresponding to the signing algorithm. If an // instance of the BCL hash algorithm cannot be resolved, throw an exception. - var algorithm = GetHashAlgorithm(name) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0293)); + using var algorithm = GetHashAlgorithm(name) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0293)); // If a frontchannel access token was returned in the authorization response, // ensure the at_hash claim matches the hash of the actual access token. @@ -2204,7 +2207,8 @@ public static partial class OpenIddictClientHandlers // Resolve the hash algorithm corresponding to the signing algorithm. If an // instance of the BCL hash algorithm cannot be resolved, throw an exception. - var algorithm = GetHashAlgorithm(name) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0295)); + using var algorithm = GetHashAlgorithm(name) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0295)); var hash = context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.AccessTokenHash); if (string.IsNullOrEmpty(hash)) @@ -2901,6 +2905,55 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for redeeming the token entry corresponding to the received state token. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RedeemStateTokenEntry : IOpenIddictClientHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RedeemStateTokenEntry() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0318)); + + public RedeemStateTokenEntry(IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateUserinfoTokenSubject.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Extract the token identifier from the state token principal. + // If no token identifier can be found, this indicates that the token has no backing database entry. + var identifier = context.StateTokenPrincipal.GetTokenId(); + if (!string.IsNullOrEmpty(identifier)) + { + // Mark the token as redeemed to prevent future reuses. + var token = await _tokenManager.FindByIdAsync(identifier); + if (token is not null) + { + await _tokenManager.TryRedeemAsync(token); + } + } + } + } + /// /// Contains the logic responsible for rejecting invalid challenge demands. /// @@ -3870,6 +3923,8 @@ public static partial class OpenIddictClientHandlers var notification = new GenerateTokenContext(context.Transaction) { + CreateTokenEntry = !context.Options.DisableTokenStorage, + PersistTokenPayload = !context.Options.DisableTokenStorage, Principal = context.StateTokenPrincipal!, TokenType = TokenTypeHints.StateToken }; diff --git a/src/OpenIddict.Client/OpenIddictClientOptions.cs b/src/OpenIddict.Client/OpenIddictClientOptions.cs index 68ae34c2..9159414e 100644 --- a/src/OpenIddict.Client/OpenIddictClientOptions.cs +++ b/src/OpenIddict.Client/OpenIddictClientOptions.cs @@ -98,4 +98,12 @@ public class OpenIddictClientOptions ValidateAudience = false, ValidateLifetime = false }; + + /// + /// Gets or sets a boolean indicating whether token storage should be disabled. + /// When disabled, no database entry is created for the tokens created by the + /// OpenIddict client services. Using this option is generally NOT recommended + /// as it prevents the tokens from being revoked (if needed). + /// + public bool DisableTokenStorage { get; set; } } diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index 2b857819..98d7c78f 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -358,7 +358,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new OpenIddictExceptions.GenericException( - SR.FormatID0163(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -487,7 +487,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new OpenIddictExceptions.GenericException( - SR.FormatID0152(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0320(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -509,7 +509,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new OpenIddictExceptions.GenericException( - SR.FormatID0153(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0321(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -531,7 +531,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new OpenIddictExceptions.GenericException( - SR.FormatID0154(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0322(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } @@ -556,7 +556,7 @@ public class OpenIddictClientService if (context.IsRejected) { throw new OpenIddictExceptions.GenericException( - SR.FormatID0155(context.Error, context.ErrorDescription, context.ErrorUri), + SR.FormatID0323(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs index 1fd777ed..30dfbea4 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs @@ -15,7 +15,6 @@ public static class OpenIddictServerDataProtectionConstants public const string Audiences = ".audiences"; public const string CodeChallenge = ".code_challenge"; public const string CodeChallengeMethod = ".code_challenge_method"; - public const string DataProtector = ".data_protector"; public const string DeviceCodeId = ".device_code_id"; public const string DeviceCodeLifetime = ".device_code_lifetime"; public const string Expires = ".expires"; diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs index 2768c1e5..8fbfe657 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -34,21 +34,21 @@ public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProt .SetResources(GetArrayProperty(properties, Properties.Resources)) .SetScopes(GetArrayProperty(properties, Properties.Scopes)) - .SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) + .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.CreationDate, GetProperty(properties, Properties.Issued)) - .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) - .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) - .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) - .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.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) - .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) - .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); + .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.CreationDate, GetProperty(properties, Properties.Issued)) + .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) + .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) + .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .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.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) + .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) + .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); static (ClaimsPrincipal principal, IReadOnlyDictionary properties) Read(BinaryReader reader) { @@ -209,51 +209,51 @@ public class OpenIddictServerDataProtectionFormatter : IOpenIddictServerDataProt // 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.Issued, principal.GetClaim(Claims.Private.CreationDate)); SetProperty(properties, Properties.Expires, principal.GetClaim(Claims.Private.ExpirationDate)); - SetProperty(properties, Properties.AccessTokenLifetime, principal.GetClaim(Claims.Private.AccessTokenLifetime)); + SetProperty(properties, Properties.AccessTokenLifetime, principal.GetClaim(Claims.Private.AccessTokenLifetime)); SetProperty(properties, Properties.AuthorizationCodeLifetime, principal.GetClaim(Claims.Private.AuthorizationCodeLifetime)); - SetProperty(properties, Properties.DeviceCodeLifetime, principal.GetClaim(Claims.Private.DeviceCodeLifetime)); - SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime)); - SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime)); - SetProperty(properties, Properties.UserCodeLifetime, principal.GetClaim(Claims.Private.UserCodeLifetime)); + SetProperty(properties, Properties.DeviceCodeLifetime, principal.GetClaim(Claims.Private.DeviceCodeLifetime)); + SetProperty(properties, Properties.IdentityTokenLifetime, principal.GetClaim(Claims.Private.IdentityTokenLifetime)); + SetProperty(properties, Properties.RefreshTokenLifetime, principal.GetClaim(Claims.Private.RefreshTokenLifetime)); + SetProperty(properties, Properties.UserCodeLifetime, principal.GetClaim(Claims.Private.UserCodeLifetime)); - SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge)); + SetProperty(properties, Properties.CodeChallenge, principal.GetClaim(Claims.Private.CodeChallenge)); SetProperty(properties, Properties.CodeChallengeMethod, principal.GetClaim(Claims.Private.CodeChallengeMethod)); SetProperty(properties, Properties.InternalAuthorizationId, principal.GetAuthorizationId()); - SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId()); + SetProperty(properties, Properties.InternalTokenId, principal.GetTokenId()); - SetProperty(properties, Properties.DeviceCodeId, principal.GetClaim(Claims.Private.DeviceCodeId)); - SetProperty(properties, Properties.Nonce, principal.GetClaim(Claims.Private.Nonce)); + SetProperty(properties, Properties.DeviceCodeId, principal.GetClaim(Claims.Private.DeviceCodeId)); + 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.Audiences, principal.GetAudiences()); SetArrayProperty(properties, Properties.Presenters, principal.GetPresenters()); - SetArrayProperty(properties, Properties.Resources, principal.GetResources()); - SetArrayProperty(properties, Properties.Scopes, principal.GetScopes()); + 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.AccessTokenLifetime or - Claims.Private.Audience or + Claims.Private.AccessTokenLifetime or + Claims.Private.Audience or Claims.Private.AuthorizationCodeLifetime or - Claims.Private.AuthorizationId or - Claims.Private.CodeChallenge or - Claims.Private.CodeChallengeMethod or - Claims.Private.CreationDate or - Claims.Private.DeviceCodeId or - Claims.Private.DeviceCodeLifetime or - Claims.Private.ExpirationDate or - Claims.Private.IdentityTokenLifetime or - Claims.Private.Nonce or - Claims.Private.Presenter or - Claims.Private.RedirectUri or - Claims.Private.RefreshTokenLifetime or - Claims.Private.Resource or - Claims.Private.Scope or - Claims.Private.TokenId or + Claims.Private.AuthorizationId or + Claims.Private.CodeChallenge or + Claims.Private.CodeChallengeMethod or + Claims.Private.CreationDate or + Claims.Private.DeviceCodeId or + Claims.Private.DeviceCodeLifetime or + Claims.Private.ExpirationDate or + Claims.Private.IdentityTokenLifetime or + Claims.Private.Nonce or + Claims.Private.Presenter or + Claims.Private.RedirectUri or + Claims.Private.RefreshTokenLifetime or + Claims.Private.Resource or + Claims.Private.Scope or + Claims.Private.TokenId or Claims.Private.UserCodeLifetime)); Write(writer, principal.Identity?.AuthenticationType, principal, properties); diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs index 9b6694a5..e20e3f06 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionConstants.cs @@ -15,7 +15,6 @@ public static class OpenIddictValidationDataProtectionConstants public const string Audiences = ".audiences"; public const string CodeChallenge = ".code_challenge"; public const string CodeChallengeMethod = ".code_challenge_method"; - public const string DataProtector = ".data_protector"; public const string DeviceCodeId = ".device_code_id"; public const string DeviceCodeLifetime = ".device_code_lifetime"; public const string Expires = ".expires"; diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs index 360831f0..c9fae7b4 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs +++ b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionFormatter.cs @@ -32,21 +32,21 @@ public class OpenIddictValidationDataProtectionFormatter : IOpenIddictValidation .SetResources(GetArrayProperty(properties, Properties.Resources)) .SetScopes(GetArrayProperty(properties, Properties.Scopes)) - .SetClaim(Claims.Private.AccessTokenLifetime, GetProperty(properties, Properties.AccessTokenLifetime)) + .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.CreationDate, GetProperty(properties, Properties.Issued)) - .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) - .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) - .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) - .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.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) - .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) - .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); + .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.CreationDate, GetProperty(properties, Properties.Issued)) + .SetClaim(Claims.Private.DeviceCodeId, GetProperty(properties, Properties.DeviceCodeId)) + .SetClaim(Claims.Private.DeviceCodeLifetime, GetProperty(properties, Properties.DeviceCodeLifetime)) + .SetClaim(Claims.Private.IdentityTokenLifetime, GetProperty(properties, Properties.IdentityTokenLifetime)) + .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.RefreshTokenLifetime, GetProperty(properties, Properties.RefreshTokenLifetime)) + .SetClaim(Claims.Private.TokenId, GetProperty(properties, Properties.InternalTokenId)) + .SetClaim(Claims.Private.UserCodeLifetime, GetProperty(properties, Properties.UserCodeLifetime)); static (ClaimsPrincipal principal, IReadOnlyDictionary properties) Read(BinaryReader reader) {