Use EdDSA signatures to validate tokens in ASP.NET Core using OpenID Connect

Some identity providers use the EdDSA / ED25519 algorithm to sign and issue tokens. This post shows how to validate the tokens using the Nuget package from ScottBrady and ASP.NET Core. Using the default OpenID Connect setup, the keys are not read and the tokens cannot be validated.

The error message could return something like this:

IDX10511: Signature validation failed. Keys tried: ‘Microsoft.IdentityModel.Tokens.JsonWebKey

The Nuget package ScottBrady.IdentityModel is used to implement this requirement, thanks to Scott for creating this.

The keys would be published on the OpenID Connect server using the web format as specified in the standards.

{
  "keys": [
    {
      "alg": "EdDSA",
      "crv": "Ed25519",
      "kid": ".....",
      "kty": "OKP",
      "x": "....."
    },

An OpenID Connect server provides a JWK endpoint where the public keys are published. This is used to validate the signatures of the issued tokens. The list of keys can be read using the JsonWebKeySet type and can be converted to EdDsaSecurityKey keys.

using Microsoft.IdentityModel.Tokens;
using Org.BouncyCastle.Tls;
using ScottBrady.IdentityModel.Crypto;
using ScottBrady.IdentityModel.Tokens;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace RazorPageOidcClient;

public class LoadPublicSigningKeys
{
    public static async Task<IEnumerable<EdDsaSecurityKey>> LoadEdDsaKeysAsync(string jwkUrl)
    {
        using var httpClient = new HttpClient();
        var jwkJson = await httpClient.GetStringAsync(jwkUrl);

        var jwkSet = JsonSerializer.Deserialize<JsonWebKeySet>(jwkJson);
        if(jwkSet == null)
        {
            throw new ArgumentNullException("Jwk endpoint not working or not found");
        }

        var keys = new List<EdDsaSecurityKey>();

        foreach (var key in jwkSet.Keys.Where(k => k.Alg == "EdDSA" && k.Crv == "Ed25519"))
        {
            // Decode the public key
            byte[] publicKeyBytes = Base64UrlEncoder.DecodeBytes(key.X);

            // Create EdDSA parameters with only the public key
            var parameters = new EdDsaParameters(ExtendedSecurityAlgorithms.Curves.Ed25519)
            {
                X = publicKeyBytes
            };

            // Create EdDSA public key
            var edDsa = EdDsa.Create(parameters);


            keys.Add(new EdDsaSecurityKey(edDsa));
        }

        return keys;
    }
}

The LoadEdDsaKeysAsync method can be used to get the keys and to set the TokenValidationParameters.IssuerSigningKeys option to validate the issued tokens.

 var keys = LoadPublicSigningKeys
	.LoadEdDsaKeysAsync("https://{authority}/.well-known/jwks.json")
	.GetAwaiter().GetResult();

 services.AddAuthentication(options =>
 {
     options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
     options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
 })
 .AddCookie()
 .AddOpenIdConnect( options =>
 {
     ...

     options.TokenValidationParameters.IssuerSigningKeys = keys;

Reading the keys like this is not optimal. It would be better to hook in directly to the default reading of the OpenID Connect well known endpoints.

Improved solution

Thanks to Frank Quednau

Create an OIDC retriever:

internal class OkpEnrichedRetriever : IConfigurationRetriever<OpenIdConnectConfiguration>
{
    private readonly IConfigurationRetriever<OpenIdConnectConfiguration> baseImplementation 
           = new OpenIdConnectConfigurationRetriever();
    public async Task<OpenIdConnectConfiguration> GetConfigurationAsync(string address, 
        IDocumentRetriever retriever, CancellationToken cancel)
    {
        var config = await baseImplementation.GetConfigurationAsync(address, retriever, cancel);

        foreach (var jsonWebKey in config.JsonWebKeySet.Keys)
        {
            if (ExtendedJsonWebKeyConverter.TryConvertToEdDsaSecurityKey(jsonWebKey, out var key))
            {
                config.SigningKeys.Add(key);
            }
        }
        return config;
    }
}

This can be used as follows:

services.AddAuthentication(options =>
{
	options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect( options =>
{
	options.ConfigurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
		$"{authority}/.well-known/openid-configuration",
		new OkpEnrichedRetriever());

And now you have a much cleaner solution.

Links

https://github.com/scottbrady91/IdentityModel

https://www.scottbrady.io/c-sharp/eddsa-for-jwt-signing-in-dotnet-core

https://datatracker.ietf.org/doc/html/rfc7517

https://www.scottbrady.io/

https://en.wikipedia.org/wiki/EdDSA

https://billatnapier.medium.com/a-bluffers-guide-to-eddsa-and-ecdsa-08f578447c57

realfiction

One comment

  1. […] Use EdDSA signatures to validate tokens in ASP.NET Core using OpenID Connect (Damien Bowden) […]

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.