Skip to content

Instantly share code, notes, and snippets.

@mykeels
Last active January 26, 2024 03:47
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mykeels/408a26fb9411aff8fb7506f53c77c57a to your computer and use it in GitHub Desktop.
Save mykeels/408a26fb9411aff8fb7506f53c77c57a to your computer and use it in GitHub Desktop.
For IdentityServer4's AddSigningCredentials in production

AddSigningCredentials

The RsaKeyService.cs file is inspired by this repo.

In Startup.cs of an IdentityServer4 app written for dotnet core 2.x, you'll see code like:

if (Environment.IsDevelopment())
{
    builder.AddDeveloperSigningCredential();
}
else
{
    throw new Exception("need to configure key material");
}

An Exception will be thrown in production, because you're expected to specify a more secure signing credential in production.

I don't fully understand how signing credentials are used, so I am open to simple explanations on the subject, but considering that I spent quite a while coming up with this way to generate signing credentials for production, I thought to share.

Please give feedback if you know a better way, and be kind enough to explain why.


You can register the RsaKeyService class as a Singleton in your Startup.cs file like:

public void ConfigureServices(IServiceCollection services)
{
  var rsa = new RsaKeyService(Environment, TimeSpan.FromDays(30));
  services.AddSingleton<RsaKeyService>(provider => rsa);
}

This makes sure that at least 30 days passes before a new RSA key file is generated.

Next, rewrite the signing credential conditional snippet as:

if (Environment.IsDevelopment())
{
    builder.AddDeveloperSigningCredential();
}
else
{
    builder.AddSigningCredential(rsa.GetKey());
}

Holla in the comments, if this helps you.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.IdentityModel.Tokens;
namespace Mykeels.Services
{
public class RsaKeyService
{
/// <summary>
/// This points to a JSON file in the format:
/// {
/// "Modulus": "",
/// "Exponent": "",
/// "P": "",
/// "Q": "",
/// "DP": "",
/// "DQ": "",
/// "InverseQ": "",
/// "D": ""
/// }
/// </summary>
private string _file {
get {
return Path.Combine(_environment.ContentRootPath, "rsakey.json");
}
}
private readonly IHostingEnvironment _environment;
private readonly TimeSpan _timeSpan;
public RsaKeyService(IHostingEnvironment environment, TimeSpan timeSpan) {
_environment = environment;
_timeSpan = timeSpan;
}
public bool NeedsUpdate() {
if (File.Exists(_file)) {
var creationDate = File.GetCreationTime(_file);
return DateTime.Now.Subtract(creationDate) > _timeSpan;
}
return true;
}
public RSAParameters GetRandomKey()
{
using (var rsa = new RSACryptoServiceProvider(2048))
{
try
{
return rsa.ExportParameters(true);
}
finally
{
rsa.PersistKeyInCsp = false;
}
}
}
public RsaKeyService GenerateKeyAndSave(bool forceUpdate = false)
{
if (forceUpdate || NeedsUpdate()) {
var p = GetRandomKey();
RSAParametersWithPrivate t = new RSAParametersWithPrivate();
t.SetParameters(p);
File.WriteAllText(_file, JsonConvert.SerializeObject(t, Formatting.Indented));
}
return this;
}
/// <summary>
///
/// Generate
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public RSAParameters GetKeyParameters()
{
if (!File.Exists(_file)) throw new FileNotFoundException("Check configuration - cannot find auth key file: " + _file);
var keyParams = JsonConvert.DeserializeObject<RSAParametersWithPrivate>(File.ReadAllText(_file));
return keyParams.ToRSAParameters();
}
public RsaSecurityKey GetKey() {
if (NeedsUpdate()) GenerateKeyAndSave();
var provider = new System.Security.Cryptography.RSACryptoServiceProvider();
provider.ImportParameters(GetKeyParameters());
return new RsaSecurityKey(provider);
}
/// <summary>
/// Util class to allow restoring RSA parameters from JSON as the normal
/// RSA parameters class won't restore private key info.
/// </summary>
private class RSAParametersWithPrivate
{
public byte[] D { get; set; }
public byte[] DP { get; set; }
public byte[] DQ { get; set; }
public byte[] Exponent { get; set; }
public byte[] InverseQ { get; set; }
public byte[] Modulus { get; set; }
public byte[] P { get; set; }
public byte[] Q { get; set; }
public void SetParameters(RSAParameters p)
{
D = p.D;
DP = p.DP;
DQ = p.DQ;
Exponent = p.Exponent;
InverseQ = p.InverseQ;
Modulus = p.Modulus;
P = p.P;
Q = p.Q;
}
public RSAParameters ToRSAParameters()
{
return new RSAParameters()
{
D = this.D,
DP = this.DP,
DQ = this.DQ,
Exponent = this.Exponent,
InverseQ = this.InverseQ,
Modulus = this.Modulus,
P = this.P,
Q = this.Q
};
}
}
}
}
@pseudoramble
Copy link

I have a question about rotating the signing key. Does this code force IdentityServer4 to use the new signing key when it's generated? If it does, how do you ensure that both previously signed tokens keep working until they expire? If it doesn't, do you simply reload the new key each time the app is restarted?

@mykeels
Copy link
Author

mykeels commented Aug 29, 2019

I did some reading, and rotating keys helps keep your system secure. See this article and github issue:

The article helped give credence to key rotation, and the github issue discusses notifying consumers of the identity server with http headers about the lifetime of the current key.

@rodrigolira
Copy link

@mykeels by instantiating the RsaKeyService like that, wouldn't it cause issues if you want to scale and run multiple instances of the IdentityServer?

@mykeels
Copy link
Author

mykeels commented Oct 22, 2019

Probably.

I no longer use this method, because I couldn't figure out how to set it up in dotnet 3.0.

I use X509Certificates instead. I'm sure you could setup a shared certificate for your use case. @rodrigolira

@Karql
Copy link

Karql commented Jan 21, 2020

@mykeels Hi could you share solution with X509Certificates? ;)

@mykeels
Copy link
Author

mykeels commented Jan 21, 2020

I use a bash script like:

#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"

PRIVATE_PEM=$DIR/private.pem
PUBLIC_PEM=$DIR/public.pem
PFX=$DIR/mycert.pfx
PASSWD=$1

if [ -z "$PASSWD" ]
then
    PASSWD="HereIsMyDefaultPassword"
fi

echo "Creating Private Key"
openssl genrsa 2048 > $PRIVATE_PEM

echo "Creating Public Key"
echo """<Country-Code>
<State>
<City>
<Company-Name>
Dev
<Email-Address>
""" | openssl req -x509 -days 1000 -new -key $PRIVATE_PEM -out $PUBLIC_PEM

echo ""
echo "Creating Certificate"

openssl pkcs12 -export -in $PUBLIC_PEM -inkey $PRIVATE_PEM -out $PFX -password pass:$PASSWD

to generate the certificate. Be sure to replace <Country-Code> and others, appropriately.

@mykeels
Copy link
Author

mykeels commented Jan 21, 2020

In your Startup.cs, you can then have:

if (Environment.IsTestOrDevelopment())
{
  builder.AddDeveloperSigningCredential();
}
else
{
  string password = Configuration["Jwt:Secret"];
  Debug.Assert(!String.IsNullOrEmpty(password), "Jwt:Secret is missing from appsettings");
  string certificate = Configuration["Jwt:Certificate"];
  Debug.Assert(!String.IsNullOrEmpty(certificate), "Jwt:Certificate is missing from appsettings");
  
  var cert = new X509Certificate2(
    certificate,
    password,
    X509KeyStorageFlags.MachineKeySet |
    X509KeyStorageFlags.PersistKeySet |
    X509KeyStorageFlags.Exportable
  );
  builder.AddSigningCredential(cert);
}

The Jwt:Secret config variable can be any passphrase such as: HereIsMyDefaultPassword. The Jwt:Certificate config variable should be the file path of the generated*.pfxcertificate file. e.g.C:\Dev\Project\mycert.pfx`

NB: Environment.IsTestOrDevelopment() is an extension method, so you may want to customise this for your use-case.

@Karql
Copy link

Karql commented Jan 21, 2020

@mykeels Thank you!

Best regards,
Mateusz

@MrTomZed
Copy link

MrTomZed commented Oct 1, 2020

Example code is not working. I had to add RsaSigningAlgorithm as a parameter:

builder.AddSigningCredential(rsa.GetKey(), RsaSigningAlgorithm.RS512);

@jcmontx
Copy link

jcmontx commented Dec 31, 2020

You're a hero. Thanks a lot!

@MasoudShah
Copy link

As AddSigningCredential method gets called only at stratup, is there any way to regenerate the key automatically to avoid periodical restart of the service? For example when a new signing request get received, the code check for the expiration of the last key.

@Karql
Copy link

Karql commented Dec 13, 2022

As AddSigningCredential method gets called only at stratup, is there any way to regenerate the key automatically to avoid periodical restart of the service? For example when a new signing request get received, the code check for the expiration of the last key.

Keys managment is not an easy topic.
You cannot just generate a new key and use it (because current tokens will be rejected).

If you want to do it right you should condider something like key rotation (future key, current key, expired key, retired key).
You can catch basic concept from here: https://www.identityserver.com/documentation/keymanagement/

Next check what AddSigingCredential does: https://github.com/IdentityServer/IdentityServer4/blob/3ff3b46698f48f164ab1b54d124125d63439f9d0/src/IdentityServer4/src/Configuration/DependencyInjection/BuilderExtensions/Crypto.cs#L30

and start with implementing your own ISigningCredentialStore, IValidationKeysStore.

@MasoudShah
Copy link

Thanks a lot @Karql , I will check it out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment