Implement client assertions for OAuth client credential flows in ASP.NET Core

This blog implements client assertions using an OAuth client credential flow in ASP.NET Core. Client assertions provide a secure way for client authentication without sharing a secret, enhancing the security the OAuth client credentials flow. By using JSON Web Tokens (JWTs) client assertions, this approach ensures strong client identity (application) verification and mitigates risks associated with traditional shared client secrets.

Code: https://github.com/damienbod/OAuthClientAssertions

Blogs in this series

NOTE: The code in the blog and the linked repository was created using the samples from IdentityServer.

Setup

Three applications are used in this setup, an API which uses the access token, an OAuth client application implemented as a console app and an OAuth server, implemented using ASP.NET Core and Duende IdentityServer. OAuth client credentials is used to acquire the access token and the signed JWT is used to authenticate the client request.

Console client

The console application is implemented using the Nuget packages from Duende. The Duende.IdentityModel and Duende.AccessTokenManagement.OpenIdConnect Nuget packages are added to the project.

The signing key is created from PEM files but this can imported from any source. Only the private key is required to sign the JWT client assertion.

var privatePem = File.ReadAllText(Path.Combine("", "rsa256-private.pem"));
var publicPem = File.ReadAllText(Path.Combine("", "rsa256-public.pem"));
var rsaCertificate = X509Certificate2.CreateFromPem(publicPem, privatePem);
var signingCredentials = new SigningCredentials(new X509SecurityKey(rsaCertificate), "RS256");

The JWT is created from the Duende sample code. The JWT is specified in an OAuth specification and all OAuth, OpenID Connect servers can implement this or have implemented this.

// Code from the Duende samples.
static string CreateClientToken(SigningCredentials credential, string clientId, string audience)
{
    var now = DateTime.UtcNow;

    var token = new JwtSecurityToken(
        clientId,
        audience,
        new List<Claim>()
        {
            new Claim(JwtClaimTypes.JwtId, Guid.NewGuid().ToString()),
            new Claim(JwtClaimTypes.Subject, clientId),
            new Claim(JwtClaimTypes.IssuedAt, now.ToEpochTime().ToString(), ClaimValueTypes.Integer64)
        },
        now,
        now.AddMinutes(1),
        credential
    );

    var tokenHandler = new JwtSecurityTokenHandler();
    var clientToken = tokenHandler.WriteToken(token);
    "\n\nClient Authentication Token:".ConsoleGreen();
    Console.WriteLine(token);
    return clientToken;
}

The token is requested using the private key. The ClientAssertion parameter is used to add the JWT to the token request.

static async Task<TokenResponse> RequestTokenAsync(SigningCredentials signingCredentials)
{
    var client = new HttpClient();

    var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
    if (disco.IsError) throw new Exception(disco.Error);

    var clientToken = CreateClientToken(signingCredentials, "mobile-client", disco.Issuer);
    var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
    {
        Address = disco.TokenEndpoint,

        ClientAssertion =
        {
            Type = OidcConstants.ClientAssertionTypes.JwtBearer,
            Value = clientToken
        },

        Scope = "mobile",
    });

    if (response.IsError) throw new Exception(response.Error);
    return response;
}

OAuth server using Duende IdentityServer

Client configuration

The Duende client configuration allows Secret definitions for the public key of the client assertion.

new Client
{
    ClientId = "mobile-client",
    ClientName = "Mobile client",

    AllowedGrantTypes = GrantTypes.ClientCredentials,
    ClientSecrets =
    [
        new Secret
        {
            // X509 cert base64-encoded
            Type = IdentityServerConstants.SecretTypes.X509CertificateBase64,
            Value = Convert.ToBase64String(rsaCertificate.GetRawCertData())
        }
    ],

    AllowedScopes = { "mobile" }
},

Services setup

The AddJwtBearerClientAuthentication extension method is used to add the support for the client assertions.

var idsvrBuilder = builder.Services
 .AddIdentityServer(options =>
 {
	 options.Events.RaiseErrorEvents = true;
	 options.Events.RaiseInformationEvents = true;
	 options.Events.RaiseFailureEvents = true;
	 options.Events.RaiseSuccessEvents = true;

	 options.EmitStaticAudienceClaim = true;
 })
 .AddInMemoryIdentityResources(Config.IdentityResources)
 .AddInMemoryApiScopes(Config.ApiScopes)
 .AddInMemoryClients(Config.Clients(builder.Environment))
 .AddAspNetIdentity<ApplicationUser>();

idsvrBuilder.AddJwtBearerClientAuthentication();

Notes

The client assertion was created using an RSA key but other key types can sizes can be used. Refer to the NIST specifications for the actual recommendations. Client assertions offer a way to avoid shared secrets between the client and the OAuth server. If implementing both client and server applications and sharing the secret in an Azure Key Vault, this client assertion has no real security improvement. Clients can also import the key from the host environment.

Links

https://docs.duendesoftware.com/identityserver/v7/tokens/authentication/jwt/

https://docs.duendesoftware.com/identityserver/v7/reference/validators/custom_token_request_validator/

https://docs.duendesoftware.com/identityserver/v7/tokens/authentication/jwt/

https://docs.duendesoftware.com/foss/accesstokenmanagement/advanced/client_assertions/

https://www.scottbrady.io/oauth/removing-shared-secrets-for-oauth-client-authentication

3 comments

  1. Unknown's avatar

    […] Implement client assertions for OAuth client credential flows in ASP.NET Core […]

  2. […] Implement client assertions for OAuth client credential flows in ASP.NET Core (Damien Bowden) […]

  3. Unknown's avatar

    […] Implement client assertions for OAuth client credential flows in ASP.NET Core […]

Leave a reply to Implement client assertions with client credentials flow using OAuth DPoP | Software Engineering Cancel reply

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