Quantcast
Channel: ASP.NET Core – Software Engineering
Viewing all articles
Browse latest Browse all 171

Implement client assertions with client credentials flow using OAuth DPoP

$
0
0

This blog looks at implementing client assertions for the client credentials flow using OAuth 2.0 Demonstration of Proof-of-Possession (DPoP). The client credentials flow is an OAuth 2.0 authorization grant type used for machine-to-machine authentication. DPoP further strengthens the security by ensuring that the client possesses a specific key at the time of the request, forcing token binding.

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 different applications are used in this setup, an API which uses the DPoP access token, an OAuth client application implemented as a console app and an OAuth server, implemented using ASP.NET Core and Duende IdentityServer. The OAuth client credentials flow is used to acquire the access token and the signed JWT is used to authenticate the client request. A second RSA Key is used for the DPoP implementation and created on the fly for the token requests.

OAuth Server using Duende

Duende IdentityServer supports DPoP really good. The Enterprise license is required to use the DPoP feature. The client credentials flow just needs the RequireDPoP property set to true and DPoP is supported.

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

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

    AllowedScopes = { "scope-dpop" }
}

Client assertions required middleware which is not added in the default setup.

idsvrBuilder.AddJwtBearerClientAuthentication();

OAuth client credentials client requesting DPoP AT

(Note: code taken from the Duende samples.)

The Duende.AccessTokenManagement Nuget package is used to support client assertions and DPoP token usage in the client application. This is integrated into a named HttpClient factory. The support for client assertions and DPoP used this HttpClient is added using the AddClientCredentialsHttpClient extension.

services.AddDistributedMemoryCache();

services.AddScoped<IClientAssertionService, ClientAssertionService>();
// https://docs.duendesoftware.com/foss/accesstokenmanagement/advanced/client_assertions/

services.AddClientCredentialsTokenManagement()
 .AddClient("mobile-dpop-client", client =>
 {
	 client.TokenEndpoint = "https://localhost:5001/connect/token";

	 client.ClientId = "mobile-dpop-client";
	 // Using client assertion
	 //client.ClientSecret = "905e4892-7610-44cb-a122-6209b38c882f";

	 client.Scope = "scope-dpop";
	 client.DPoPJsonWebKey = CreateDPoPKey();
 });

services.AddClientCredentialsHttpClient("mobile-dpop-client", "mobile-dpop-client", client =>
{
 client.BaseAddress = new Uri("https://localhost:5005/");
});

The DPoP is created for each instance.

   private static string CreateDPoPKey()
   {
       var key = new RsaSecurityKey(RSA.Create(2048));
       var jwk = JsonWebKeyConverter.ConvertFromRSASecurityKey(key);
       jwk.Alg = "PS256";
       var jwkJson = JsonSerializer.Serialize(jwk);
       return jwkJson;
   }

The IClientAssertionService interface is used to add the client assertion to the client credentials client using the Duende client Nuget package. This works the same as in the previous blog.

public class ClientAssertionService : IClientAssertionService
{
    private readonly IOptionsSnapshot<ClientCredentialsClient> _options;

    public ClientAssertionService(IOptionsSnapshot<ClientCredentialsClient> options)
    {
        _options = options;
    }

    public Task<ClientAssertion?> GetClientAssertionAsync(
      string? clientName = null, TokenRequestParameters? parameters = null)
    {
        if (clientName == "mobile-dpop-client")
        {
            // 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");

            var options = _options.Get(clientName);

            var descriptor = new SecurityTokenDescriptor
            {
                Issuer = options.ClientId,
                Audience = options.TokenEndpoint,
                Expires = DateTime.UtcNow.AddMinutes(1),
                SigningCredentials = signingCredentials,

                Claims = new Dictionary<string, object>
                {
                    { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
                    { JwtClaimTypes.Subject, options.ClientId! },
                    { JwtClaimTypes.IssuedAt, DateTime.UtcNow.ToEpochTime() }
                }
            };

            var handler = new JsonWebTokenHandler();
            var jwt = handler.CreateToken(descriptor);

            return Task.FromResult<ClientAssertion?>(new ClientAssertion
            {
                Type = OidcConstants.ClientAssertionTypes.JwtBearer,
                Value = jwt
            });
        }

        return Task.FromResult<ClientAssertion?>(null);
    }
}

The services can be used like any other HttpClient named client.

var client = _clientFactory.CreateClient("mobile-dpop-client");
var response = await client.GetAsync("api/values", stoppingToken);

Notes

Using DPoP and client assertions work well together in this setup and different keys are used for the different OAuth flows. A lot of logic is solved using the Duende Nuget packages. Using DPoP and token binding for the API increases the security and should be used whenever possible. If using a web application with a user, a delegated OpenID Connect flow would be the better solution.

Links

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

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

https://github.com/DuendeSoftware/products/tree/main/aspnetcore-authentication-jwtbearer


Viewing all articles
Browse latest Browse all 171