Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/migrating/from_duende.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Migration Steps

.. _refReadOnlyKeyStore:

If you were using automatic key management, you will now need to register read-only key store or add a signing key explicitly:
If you were using automatic key management, you will now need to register the read-only key store and/or add a signing key(s) explicitly:

.. code-block:: c#

Expand All @@ -113,6 +113,12 @@ Migration Steps
// Explicit key registration
builder.Services.AddIdentityServer()
.AddSigningCredential(certificate);

.. warning::
If configuring the read-only key store, all keys created within a 90 days will be considered valid, and the newest
key within this timespan will be registered as a signing key. The extension method used to register the
compatibility store allows you to configure this ``.AddCompatibilityKeyStores(opt => opt.MaxLifetime = TimeSpan.FromDays(120))``.


5. **Migrate the database schema (if applicable)**

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,17 @@ public static IIdentityServerBuilder AddValidationKey(
/// Add the signing and validation key stores that target the IdentityServer compatibility key stores
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="configure">Action to configure compatibility key store options</param>
/// <returns></returns>
public static IIdentityServerBuilder AddCompatibilityKeyStores(this IIdentityServerBuilder builder)
public static IIdentityServerBuilder AddCompatibilityKeyStores(this IIdentityServerBuilder builder, Action<CompatibilityKeyStoreOptions> configure = null)
{
var options = new CompatibilityKeyStoreOptions();
configure?.Invoke(options);

builder.Services.AddSingleton(options);
builder.Services.AddTransient<DataProtectedIdentityServerKeyMaterialConverter>();
builder.Services.AddScoped<ISigningCredentialStore, IdentityServerSigningCredentialStore>();
builder.Services.AddScoped<IValidationKeysStore, IdentityServerValidationKeysStore>();
builder.Services.AddScoped<ISigningCredentialStore, IdentityServerSigningCredentialStore>();

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;

namespace Open.IdentityServer.Configuration;
Comment thread
jhbritton-RSK marked this conversation as resolved.

/// <summary>
/// Compatibility key store options.
/// </summary>
public class CompatibilityKeyStoreOptions
{
/// <summary>
/// Maximum lifetime for keys read from the key store, defaults to 90 days.
/// </summary>
public TimeSpan MaxLifetime { get; set; } = TimeSpan.FromDays(90);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Open.IdentityServer.DataProtection;
/// <param name="dataProtectionProvider">The data protection provider used to create a protector for unprotecting encrypted key material.</param>
public class DataProtectedIdentityServerKeyMaterialConverter(IDataProtectionProvider dataProtectionProvider)
{
private IDataProtector dataProtector = dataProtectionProvider.CreateProtector("DataProtectionKeyProtector");
private IDataProtector dataProtector = dataProtectionProvider.CreateProtector(DataProtectionConstants.KeyProtectorPurpose);

/// <summary>
/// Json serializer settings
Expand Down Expand Up @@ -58,8 +58,6 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial)
{
var keyData = JsonSerializer.Deserialize<EcIdentityServerKeyData>(unprotectedData, Settings);

signingKey.Created = keyData.Created;

ECCurve curve = keyMaterial.Algorithm switch
{
"ES256" => CryptoHelper.GetCurveFromCrvValue("P-256"),
Expand All @@ -70,6 +68,7 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial)

var ecdsa = ECDsa.Create(new ECParameters { Curve = curve, D = keyData.D, Q = keyData.Q });

signingKey.Created = keyData.Created;
signingKey.Credentials = new SigningCredentials(new ECDsaSecurityKey(ecdsa) { KeyId = keyData.Id }, keyData.Algorithm);
}

Expand All @@ -79,7 +78,8 @@ public SigningKey Convert(IdentityServerKeyMaterial keyMaterial)

var cert = X509CertificateLoader.LoadPkcs12(System.Convert.FromBase64String(keyData.CertificateRawData), null);

signingKey.Credentials = new SigningCredentials(new X509SecurityKey(cert), keyData.Algorithm);
signingKey.Created = keyData.Created;
signingKey.Credentials = new SigningCredentials(new X509SecurityKey(cert) { KeyId = keyData.Id }, keyData.Algorithm);
}

return signingKey;
Expand Down
13 changes: 13 additions & 0 deletions src/Open.IdentityServer/src/DataProtectionConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace Open.IdentityServer;

/// <summary>
/// Constants for DataProtection in Open.IdentityServer.
/// </summary>
public static class DataProtectionConstants
{
/// <summary>Purpose used when creating key material data protector.</summary>
public const string KeyProtectorPurpose = "DataProtectionKeyProtector";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
Comment thread
jhbritton-RSK marked this conversation as resolved.
using Open.IdentityServer.Configuration;
using Open.IdentityServer.DataProtection;
using Open.IdentityServer.Models;

namespace Open.IdentityServer.Stores;

/// <summary>
/// This class is the base implementation of the IdentityServer compatibility signing key store that retrieves legacy
/// from the existing IdentityServer read-only key store.
/// </summary>
/// <param name="identityServerKeyStore">The key store used to retrieve signing keys.</param>
/// <param name="dataProtectedIdentityServerKeyMaterialConverter">Converter used to decrypt and convert data-protected key material into usable credentials.</param>
/// <param name="timeProvider">Time provider implementation</param>
/// <param name="options">Compatibility key store options</param>
public abstract class IdentityServerSigningKeyStore(
IIdentityServerKeyStore identityServerKeyStore,
DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter,
TimeProvider timeProvider,
CompatibilityKeyStoreOptions options)
{
private readonly IIdentityServerKeyStore identityServerKeyStore = identityServerKeyStore;
private readonly DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter = dataProtectedIdentityServerKeyMaterialConverter;
private readonly CompatibilityKeyStoreOptions options = options;

/// <summary>
/// Get all signing keys from the store, filtering based on the compatibility key store configuration and use
/// </summary>
/// <returns></returns>
protected IEnumerable<SigningKey> GetKeys()
{
Comment thread
jhbritton-RSK marked this conversation as resolved.
var results = identityServerKeyStore.GetKeys()
.Where(x => x.Use == "signing")
.Select(dataProtectedIdentityServerKeyMaterialConverter.Convert)
.Where(x => x.Created.Add(options.MaxLifetime) > timeProvider.GetUtcNow().UtcDateTime)
.OrderByDescending(x => x.Created);

return results;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using Open.IdentityServer.Configuration;
using Open.IdentityServer.DataProtection;

namespace Open.IdentityServer.Stores;
Expand All @@ -13,20 +15,25 @@ namespace Open.IdentityServer.Stores;
/// </summary>
/// <param name="identityServerKeyStore">The key store used to retrieve signing keys.</param>
/// <param name="dataProtectedIdentityServerKeyMaterialConverter">Converter used to decrypt and convert data-protected key material into usable credentials.</param>
/// <param name="timeProvider">Time provider implementation</param>
/// <param name="options">Compatibility key store options</param>
public class IdentityServerSigningCredentialStore(
IIdentityServerKeyStore identityServerKeyStore,
DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter): ISigningCredentialStore
DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter,
TimeProvider timeProvider,
CompatibilityKeyStoreOptions options
):
IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options),
ISigningCredentialStore
{

/// <summary>
/// Gets the key to be used for signing from the key store, will be the newest key
/// </summary>
/// <returns>a key to be used for signing</returns>
public async Task<SigningCredentials> GetSigningCredentialsAsync()
{
return identityServerKeyStore.GetKeys()
.Where(x => x.Use == "signing")
.Select(dataProtectedIdentityServerKeyMaterialConverter.Convert)
.OrderByDescending(x => x.Created)
return GetKeys()
.Select(x => x.Credentials)
.FirstOrDefault();
Comment thread
jhbritton-RSK marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Open.IdentityServer.Configuration;
using Open.IdentityServer.DataProtection;
using Open.IdentityServer.Models;

Expand All @@ -14,9 +16,16 @@ namespace Open.IdentityServer.Stores;
/// </summary>
/// <param name="identityServerKeyStore">The key store used to retrieve all available signing keys.</param>
/// <param name="dataProtectedIdentityServerKeyMaterialConverter">Converter used to decrypt and convert data-protected key material into usable credentials.</param>
/// <param name="timeProvider">Time provider implementation</param>
/// <param name="options">Compatibility key store options</param>
public class IdentityServerValidationKeysStore(
IIdentityServerKeyStore identityServerKeyStore,
DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter): IValidationKeysStore
DataProtectedIdentityServerKeyMaterialConverter dataProtectedIdentityServerKeyMaterialConverter,
TimeProvider timeProvider,
CompatibilityKeyStoreOptions options
):
IdentityServerSigningKeyStore(identityServerKeyStore, dataProtectedIdentityServerKeyMaterialConverter, timeProvider, options),
IValidationKeysStore
{
/// <summary>
/// Gets all keys to be used for validation in the Duende key store, will be any other keys that are not retired, or the
Expand All @@ -25,10 +34,7 @@ public class IdentityServerValidationKeysStore(
/// <returns>list of validation keys info</returns>
public async Task<IEnumerable<SecurityKeyInfo>> GetValidationKeysAsync()
{
return identityServerKeyStore.GetKeys()
.Where(x => x.Use == "signing")
.Select(dataProtectedIdentityServerKeyMaterialConverter.Convert)
.OrderByDescending(x => x.Created)
return GetKeys()
.Select(x => new SecurityKeyInfo { Key = x.Credentials.Key, SigningAlgorithm = x.Credentials.Algorithm })
.ToList();
Comment thread
jhbritton-RSK marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
Comment thread
jhbritton-RSK marked this conversation as resolved.
using System.Collections.Generic;
using Open.IdentityServer.Models;
using Open.IdentityServer.Stores;
Expand All @@ -6,6 +10,8 @@ namespace IdentityServer.IntegrationTests.Endpoints.Discovery;

public class FakeIdentityServerKeyStore: IIdentityServerKeyStore
{
public static readonly DateTime FakeNow = new(2026, 02, 01, 12, 00, 00, DateTimeKind.Utc);

Comment thread
jhbritton-RSK marked this conversation as resolved.
public IEnumerable<IdentityServerKeyMaterial> GetKeys()
{
return
Expand All @@ -15,6 +21,7 @@ public IEnumerable<IdentityServerKeyMaterial> GetKeys()
new IdentityServerKeyMaterial { Id = "FakeRSA_ES256", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES256", Data = FakeES256KeyData },
new IdentityServerKeyMaterial { Id = "FakeRSA_ES384", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES384", Data = FakeES384KeyData },
new IdentityServerKeyMaterial { Id = "FakeRSA_ES521", Version = 1, Use = "signing", DataProtected = false, Algorithm = "ES521", Data = FakeES521KeyData },
new IdentityServerKeyMaterial { Id = "FakeRSA_PS512_Expired", Version = 1, Use = "signing", DataProtected = false, Algorithm = "PS512", Data = FakePS512KeyData },
];
Comment thread
jhbritton-RSK marked this conversation as resolved.
}

Expand Down Expand Up @@ -97,4 +104,23 @@ public IEnumerable<IdentityServerKeyMaterial> GetKeys()
"Created": "2026-01-01T12:00:00"
}
""";

private const string FakePS512KeyData = """
{
"Parameters": {
"D": "Bj8nuAP4kWK6Bh5u7JM1SlkENF4MmaCDhptjtoG3mo+Blh7EfRb3IGCNP4E81xmWTBAmnNdb6/H9e0YSublgXVHnXcsEwLCiIucWCa9YmwuUeBw4cMzNIHVsWss2wv7XpY18SVLzZs9PW99zgZwREquQFbYcjVGfEQqEYNGe891Lz+AKTo5Qpsyx3laZLTOUL+VBb5bJGJxl2HIeFWDbrn/bqltBRUcjayYiX563IwtHBwLBWBAEbrBBurGPaIk9PjC4WVAvVdvWYZQgksfePsV7/1zWhi6K5VElAn0ohrpmImHWh6ob+nIV6oUJG/gXpNAmB2ZbMDopeRn1QRZKAQ==",
"DP": "QUvY4w84l8MoqhAcxoXA7H6DaPZRpDnoNuTPfMVJCjnMxBLrX8k2Yx8vPk2KhFXSklG9K0PrLWsp8EO/dHVplbUXqhluYbeAJ6UZXNXW1cmjw6AXiYn+SwibP7KY6HhElxTVqhMqm+66e6Coei11otsweRMWT3dSmPBMlYoxc/M=",
"DQ": "2HtvEUst8qBP7yl1n2cMoEBNo6X6Hv8On2oFBvAtsUgzfHO9BKRdrrwo7fGH6t6ZGoGYB+uE9tbenRpr58ZEHHE65bKwZ6z/Us6ogKq/QOsfroQCU509U0x5aXn3DfYwzNGXDDlw7RESh89L9YWf7j9UsMeITPpw1nINf06jIAE=",
"Exponent": "AQAB",
"InverseQ": "PUaNIRe81tN+RmxtA4OrfKupweBWPfGrKXosT9IbIlP3aqEGsR0+rLtWzs+5fw0bMyFSLcVUmEQAfso/RirlskqtO0A5Zg2qfQjSVZkTCvLrPYBnu94JwNhA6K/MawkoRcqsM682FVOlJvSqkhbCZuG/6AcF+Lu3To4oDjsm5y4=",
"Modulus": "zYRdc59usklvJRIgQhK+6jEp0P8YohIj3vYEWyv3S/dHqa8dSGloyHDJhG6RAdqnR8S2nhGTcdokqRE4hSdWMaQSd5FKgC2ZpKe0BM3qt4q7lzid8kezkUqkhqCV39ITEiMxMkeMDK+SZqiH2OB5J6n+G4b5PnroshElcNXRaWultIm+vnAWkPXJyhc0mvD+uAlFd9TMbTU5BZE7NZ0tn8Y0Ny2MAPLWW0UVmkGeSwS5rCROiaJTS3P50BJD5eAJrq4OAilVjciBWWt68+6uwxmUGZye9foqAc98P3jLM7ccLrMKklyejTBNIcisyNEY+3+WM1NkA3mMy0qVS0cbXw==",
"P": "7khqnAiMQG17a5daoEmL2zcB6rpnJ5t7cVqJd12UUCbfVzhP+pdxvjG5vt4Dpb5/lN29GlC+sJeL9WsmBSwtCENb1No3uUQ8QbQYCFA4gBInck5q2RBRuPeqP4RHJWglR+9rDSs+hcGvNaDPBL70BCyUQKcrlaBSy4NDwBCFZb8=",
"Q": "3MxE5TRElLXPnY0miYxqGc3zpjKayu/c7yRaM/kR+16+fRi1AWhscVBpusA27xHdS5SZiTGEeLdqiKQ+OVC/oNmXS2aXDaGOyb6014jsil43sBQVQekeiTfkKslPE03DlOr6w6KqeuiKTF+yQBHvH3yybDlXy3wqpiGLq7az8mE="
},
"Id": "FakeRSA_PS512_Expired",
"Algorithm": "PS512",
"HasX509Certificate": false,
"Created": "2025-10-01T12:00:00"
}
""";
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// Copyright (c) 2026, Rock Solid Knowledge Ltd
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

using System;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using AwesomeAssertions;
using IdentityServer.IntegrationTests.Common;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Time.Testing;
using Microsoft.IdentityModel.Tokens;
using Open.IdentityServer;
using Open.IdentityServer.Configuration;
Expand Down Expand Up @@ -157,9 +159,12 @@ public async Task Jwks_with_two_key_using_different_algs_expect_different_alg_va
public async Task Jwks_WhenCompatibilityStoreConfigured_ShouldGetKeysFromStore()
{
IdentityServerPipeline pipeline = new IdentityServerPipeline();
FakeTimeProvider timeProvider = new FakeTimeProvider();
timeProvider.SetUtcNow(FakeIdentityServerKeyStore.FakeNow);

pipeline.OnPostConfigureServices += services =>
{
services.AddSingleton<TimeProvider>(timeProvider);
services.AddScoped<IIdentityServerKeyStore, FakeIdentityServerKeyStore>();
services.AddIdentityServerBuilder()
.AddCompatibilityKeyStores();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Microsoft.AspNetCore.TestHost"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
Expand Down
Loading
Loading