ASP.NET
Core Identity
Server 4 Signing
Credential Key rollover
Signing
Credential in Identity Server 4 is an asymmetric key pair which is used for
signing/validating JWTs.
In
development mode, we can easily create/reuse the Signing Credential by
var builder = services.AddIdentityServer();
// Signing credential
if (this.env.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
The above
codes will check if there is tempkey.rsa
(Development Signing Credential) in your Idsrv4’s root path and create a
new one if it doesn’t exists.
However, when the Development
Signing Credential changes, we will encounter the problem that the signed JWTs
by old (private) key will be invalid when being validated by the new (public)
key. So
it’s recommended to use the SSL/TLS certificate or use your own RSA key pair,
and do the key rollover.
Key rollover means:
1. Create a new Signing Credential that is used to ISSUE/VALIDATE
JWT.
2. Add old Signing Credentials as the Validation Keys to
ONLY VALIDATE JWT.
3. Retire the old Signing Credentials if we don’t need to
validate the JWTs signed by them anymore. (This wont be discussed in this
article).
|
In this
tutorial, we will replace the Development Signing Credential in these ways:
From
|
Ho to do key rollover
|
Note
|
Certificate
|
Manually update the certificate
|
Some cert provider can renew cert automatically, like
Let’s encrypt.
You can combine the renew process to your CI.
|
File
|
Manually/Automatically
|
The key can be renewed automatically when having a
Registry Server and integrate in your CI. This tutorial will only has the
manually way.
|
Redis
|
Automatically
|
You can store the key in Database instead of Redis.
|
▋Docker 18.05.0-ce
▋.NET Core SDK 3.1.201
▋IdentityServer4 3.1.2
▋IdentityModel 4.0.0
The
source code is on my Github.
▋Signing Credential from certificate (Linux container)
Notice that the Audience is “MyBackendApi2”.
First we have to create the certificate by OpenSSL,
skip this step if you already have one.
▋(Optional)
Create certificate
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/Docker.key -x509 -days 3650 -out certs/Docker.crt
$ openssl pkcs12 -export -out certs/Docker.pfx -inkey certs/Docker.key -in certs/Docker.crt
▋Startup.cs: ConfigureServices
Add the certificate as Signing Credential as
following, notice that I put the cert under certs/.
public void ConfigureServices(IServiceCollection services)
{
// …
skip other codes
var builder = services.AddIdentityServer(options =>
{
// … set options
here
});
var rootPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Certs");
var cert = new X509Certificate2(Path.Combine(rootPath, "Docker.pfx"), string.Empty);
builder.AddSigningCredential(cert);
}
We can also add the old certificate as the
validation key like this,
// Add validation keys if any
builder.AddValidationKey(old_cert);
▋Signing Credential from certificate (Windows)
First, lets import the certificate into Windows
Certificate Store by Certificate Management Store.
After importing the cert, we need to know the Thumbprint of it to get the right
certificate in our code.
Right click on the cert -> Properties
Copy the value of Thumbprint in [Details] tab,
Now we can use it as Signing Credential in Idsrv.
▋Startup.cs: ConfigureServices
Add the certificate as Signing Credential from
Windows Certificate Store as following,
public void ConfigureServices(IServiceCollection services)
{
// …
skip other codes
var builder = services.AddIdentityServer(options =>
{
// … set options
here
});
using (var certStore = new X509Store(StoreName.My, StoreLocation.LocalMachine))
{
certStore.Open(OpenFlags.ReadOnly);
var certCollection = certStore.Certificates.Find(
X509FindType.FindByThumbprint,
"3c99282xxxxxxxxxxxxxxxxxxxxxxxxx", // Change this with the thumbprint of your certifiacte
validOnly: false);
if (certCollection.Count > 0)
{
cert = certCollection[0];
builder.AddSigningCredential(cert);
}
}
// Add validation keys if any
//
builder.AddValidationKey(old_cert);
}
▋Signing Credential from Redis
We will
use 2 Redis keys to store the keys (Signing credentials):
Redis Key
|
Type
|
Description
|
Content format
|
SigningCredential
|
Strings
|
The Signing Credential for issuing and validating JWT
|
Object in JSON
|
SigningCredentialDeprecated
|
Strings
|
Deprecated Signing Credentials for validating JWT
|
Array in JSON
|
The
Signing Credential(s) will be stored as object or array of custom type: SigningCredential,
▋Model: SigningCredential.cs
public class SigningCredential
{
/// <summary>
/// Key ID
/// </summary>
public string KeyId { get; set; }
/// <summary>
/// RSA parameters
/// </summary>
public RSAParameters Parameters { get; set; }
/// <summary>
/// Expire on
/// </summary>
public DateTimeOffset ExpireOn { get; set; }
}
This
model is similar to the private class: TemporaryRsaKey
in Crypto.cs
from Idsrv4’s source code.
The only
difference is we have the new property ExpireOn, which is used to check
if the key needs to be renewed.
PS. The TemporaryRsaKey class is the text
format in tempkey.rsa (Development Signing Credential).
※ How to create a new
RSA Key pair as the Signing Credential?
We can
use the same logic of IdentityServerBuilderExtensionsCrypto.AddDeveloperSigningCredential()
in Crypto.cs
from Idsrv4’s source code,
using IdentityServer4;
using IdentityServer4.Configuration;
using IdentityServer4.Models;
var key = CryptoHelper.CreateRsaSecurityKey();
RSAParameters parameters = key.Rsa == null ?
parameters = key.Parameters :
key.Rsa.ExportParameters(includePrivateParameters: true);
var expireOn = DateTimeOffset.UtcNow.AddYears(1);
var credential = new SigningCredential
{
Parameters = parameters,
KeyId = key.KeyId,
ExpireOn = expireOn
};
Now we
can implement the full logics with these steps:
1. Check Redis’s current Signing Credential (from Redis key: SigningCredential)
is valid or expired.s
2. If current Signing Credential is valid, go to step 4.
3. If current Signing Credential is missing or expired, move
the old one to Redis key: SigningCredentialDeprecated and create a new
one.
4. Register the Signing Credential (from Redis key: SigningCredential)
to Idsrv4.
5. Register the Signing Credentials (from Redis key: SigningCredentialDeprecated)
as the validation keys to Idsrv4.
▋IdentityServerBuilderExtensions.cs
public static IIdentityServerBuilder AddSigningCredentialByRedis(
this IIdentityServerBuilder builder, AppSettings appSettings)
{
// Variables
const int DEFAULT_EXPIRY_YEAR = 1;
var utcNow = DateTimeOffset.UtcNow;
var redisKeyWorkingSk = CacheKeyFactory.SigningCredential();
var redisKeyDeprecatedSk = CacheKeyFactory.SigningCredential(isDeprecated: true);
// RSA key object
Microsoft.IdentityModel.Tokens.RsaSecurityKey key = null; // The Key for Idsrv
// Signing credetial object from Redis
SigningCredential credential = null; // The Signing credential stored in Redis
List<SigningCredential> deprecatedCredentials = null; // The Deprecated Signing credentials stored in Redis
using (ICacheService redis = new RedisService(appSettings))
{
bool isSigningCredentialExists = redis.GetCache(redisKeyWorkingSk, out credential);
if (isSigningCredentialExists && credential.ExpireOn >= utcNow)
{
// Use the RSA key pair stored in redis
key = CryptoHelper.CreateRsaSecurityKey(credential.Parameters, credential.KeyId);
}
else if (isSigningCredentialExists && credential.ExpireOn < utcNow)
{
#region Move the expired Signing credential to Redis's Decprecated-Signing-Credential key
_ = redis.GetCache(redisKeyDeprecatedSk, out deprecatedCredentials);
if (deprecatedCredentials == null)
{
deprecatedCredentials = new List<SigningCredential>();
}
deprecatedCredentials.Add(credential);
redis.SaveCache(redisKeyDeprecatedSk, deprecatedCredentials);
#endregion
#region Clear the expired Signing credential from Redis's Signing-Credential key
redis.ClearCache(redisKeyWorkingSk);
#endregion
// Set flag as False
isSigningCredentialExists = false;
}
#region If no available Signing credial, create a new one
if (!isSigningCredentialExists)
{
key = CryptoHelper.CreateRsaSecurityKey();
RSAParameters parameters = key.Rsa == null ?
parameters = key.Parameters :
key.Rsa.ExportParameters(includePrivateParameters: true);
var expireOn = appSettings.Global?.SigningCredential?.DefaultExpiry == null ?
utcNow.AddYears(DEFAULT_EXPIRY_YEAR) :
appSettings.Global.SigningCredential.DefaultExpiry.GetExpireOn();
credential = new SigningCredential
{
Parameters = parameters,
KeyId = key.KeyId,
ExpireOn = expireOn
};
// Save to Redis
redis.SaveCache(redisKeyWorkingSk, credential);
}
#endregion
// Add the key as the Signing credential for Idsrv
builder.AddSigningCredential(key, IdentityServerConstants.RsaSigningAlgorithm.RS256);
// Also add the expired key for clients' old tokens
if (redis.GetCache(redisKeyDeprecatedSk, out deprecatedCredentials))
{
IList<SecurityKeyInfo> deprecatedKeyInfos = new List<SecurityKeyInfo>();
deprecatedCredentials.ForEach(dc =>
{
var deprecatedKeyInfo = new SecurityKeyInfo
{
Key = CryptoHelper.CreateRsaSecurityKey(dc.Parameters, dc.KeyId),
SigningAlgorithm = SecurityAlgorithms.RsaSha256
};
deprecatedKeyInfos.Add(deprecatedKeyInfo);
});
builder.AddValidationKey(deprecatedKeyInfos.ToArray());
}
}
return builder;
}
And don’t
forget to update Startup: ConfigureServices!
▋Startup.cs:
ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
// …
skip other codes
var builder = services.AddIdentityServer(options =>
{
// … set options
here
});
builder.AddSigningCredentialByRedis(this.appSettings);
}
▋Demo
After starting the Auth Server, we will find that a
new Signing Credential is stored in Redis.
Next time we restart the Auth Server, the expired
Signing Credential will be removed and create a new one.
▋Signing Credential from Files
We will
use 2 files to store the keys (Signing credentials):
Redis Key
|
Path
|
Description
|
Content format
|
SigningCredential.rsa
|
Keys/
|
The Signing Credential for issuing and validating JWT
|
Object in JSON
|
DeprecatedSigningCredentials.rsa
|
Keys/
|
Deprecated Signing Credentials for validating JWT
|
Array in JSON
|
The way to generate/store Signing
Credential is almost as same as using Redis, except that it cannot renew key
automatically.
▋IdentityServerBuilderExtensions.cs
public static class IdentityServerBuilderExtensions
{
public static IIdentityServerBuilder AddSigningCredentialsByFile(
this IIdentityServerBuilder builder, AppSettings appSettings)
{
// Variables
const int DEFAULT_EXPIRY_YEAR = 1;
const string DIR_NAME_KEYS = "Keys";
const string FILE_NAME_WORKING_SC = "SigningCredential.rsa";
const string FILE_NAME_DEPRECATED_SC = "DeprecatedSigningCredentials.rsa";
var rootDir = Path.Combine(AppContext.BaseDirectory, DIR_NAME_KEYS);
var workingScDir = Path.Combine(rootDir, FILE_NAME_WORKING_SC);
var deprecatedScDir = Path.Combine(rootDir, FILE_NAME_DEPRECATED_SC);
var utcNow = DateTimeOffset.UtcNow;
// RSA key object
Microsoft.IdentityModel.Tokens.RsaSecurityKey key = null; // The Key for Idsrv
// Signing credetial object
SigningCredential credential = null; // The Signing credential stored in file
List<SigningCredential> deprecatedCredentials = null; // The Deprecated Signing credentials stored in file
#region Add exist or new Signing Credentials
var strWorkingSc = FileUtils.ReadFileAsync(workingScDir).Result;
var strDeprecatedScs = FileUtils.ReadFileAsync(deprecatedScDir).Result;
if (!string.IsNullOrEmpty(strWorkingSc))
{
// Use the RSA key pair stored in file
credential = JsonConvert.DeserializeObject<SigningCredential>(strWorkingSc);
key = CryptoHelper.CreateRsaSecurityKey(credential.Parameters, credential.KeyId);
// Warning if key expires
if (credential.ExpireOn < utcNow)
{
logger.Warn($"Auth Server's Signing Credential (ID: {credential.KeyId}) is expired at {credential.ExpireOn.ToLocalTime()}!");
}
}
else
{
// Create new RSA key pair
key = CryptoHelper.CreateRsaSecurityKey();
RSAParameters parameters = key.Rsa == null ?
parameters = key.Parameters :
key.Rsa.ExportParameters(includePrivateParameters: true);
var expireOn = appSettings.Global?.SigningCredential?.DefaultExpiry == null ?
utcNow.AddYears(DEFAULT_EXPIRY_YEAR) :
appSettings.Global.SigningCredential.DefaultExpiry.GetExpireOn();
credential = new SigningCredential
{
Parameters = parameters,
KeyId = key.KeyId,
ExpireOn = expireOn
};
// Save to fiile
FileUtils.SaveFileAsync(workingScDir, JsonConvert.SerializeObject(credential)).Wait();
}
// Add the key as the Signing credential for Idsrv
builder.AddSigningCredential(key, IdentityServerConstants.RsaSigningAlgorithm.RS256);
#endregion
#region Add Deprecated Signing Credentials for clients' old tokens
deprecatedCredentials = string.IsNullOrEmpty(strDeprecatedScs) ? new List<SigningCredential>() : JsonConvert.DeserializeObject<List<SigningCredential>>(strDeprecatedScs);
IList<SecurityKeyInfo> deprecatedKeyInfos = new List<SecurityKeyInfo>();
deprecatedCredentials.ForEach(dc =>
{
var deprecatedKeyInfo = new SecurityKeyInfo
{
Key = CryptoHelper.CreateRsaSecurityKey(dc.Parameters, dc.KeyId),
SigningAlgorithm = SecurityAlgorithms.RsaSha256
};
deprecatedKeyInfos.Add(deprecatedKeyInfo);
});
builder.AddValidationKey(deprecatedKeyInfos.ToArray());
#endregion
return builder;
}
}
▋Startup.cs:
ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
// …
skip other codes
var builder = services.AddIdentityServer(options =>
{
// … set options
here
});
builder.AddSigningCredentialsByFile(this.appSettings);
}
▋Demo
After starting the Auth Server, we will find that a
new Signing Credential is stored in Keys/SigningCredential.rsa.
We can put the expired Signing Credential(s) as the
validation key(s) as following.
沒有留言:
張貼留言