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)
{