ASP.NET Core using Proof Key for Code Exchange (PKCE)

OpenID Connect

Proof Key for Code Exchange (PKCE) was initially designed for native/mobile client applications when using OAuth; however, as a happy accident, it’s also handy for all other kinds of applications, and new specification and BCP documents are starting to encourage the use of PKCE across the board.

PKCE allows us to ensure that the client application swapping an authorization code for tokens, is the same application that initially requested the authorization code. It protects us from bad actors from stealing authorization codes and using them.

In this article, we’re going to see how we can add PKCE support to an existing ASP.NET Core OpenID Connect client application (with some IdentityServer4 config thrown in for good measure).

Skip to the PKCE.

ASP.NET Core Support for PKCE

ASP.NET Core does not include built-in support for PKCE out of the box. In the past when we’ve used PKCE, it’s been in native applications, and we would have typically used IdentityModel’s OidcClient library. So, to add support for PKCE, let’s use a combination of both libraries (or at least the code from them).

Standard ASP.NET Core using Authorization Code Flow

This example code will use the OpenID Connect Authorization Code flow with a response type of code and a response mode of form_post, as this allows us to keep codes out of the URL and protected via TLS.

So, an explicit implementation in ASP.NET Core would look like:

services.AddAuthentication()
    // other registrations
    .AddOpenIdConnect("oidc", options => {
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false; // dev only

        options.ClientId = "pkce_client";
        options.ClientSecret = "acf2ec6fb01a4b698ba240c2b10a0243";
        options.ResponseType = "code";
        options.ResponseMode = "form_post";
        options.CallbackPath = "/signin-oidc";
    });

With a call to AddAuthentication in our Configure method.

At this point your OpenID Provider (aka OAuth Authorization Server, aka Identity Provider, aka Security Token Service (STS)) would look something like this, using IdentityServer4 as an example:

new Client {
    ClientId = "pkce_client",
    ClientName = "MVC PKCE Client",
    AllowedGrantTypes = GrantTypes.Code,
    ClientSecrets = {new Secret("acf2ec6fb01a4b698ba240c2b10a0243".Sha256())},
    RedirectUris = {"http://localhost:5001/signin-oidc"},
    AllowedScopes = {"openid", "profile", "api1"}
}

Adding PKCE Support to the Authorization Request

We’re going to use some helper methods for the IdentityModel library to help us with common functionality such as random value generation and Base64 URL encoding, which we can install using:

install-package IdentityModel

To make an authorization request that uses PKCE, our authorization request has to include a code_challenge. So, let’s create our code_verifier value, hash it, and add it to our authorization request. The best place to do this is in the OnRedirectToIdentityProvider event on our OpenIdConnectOptions:

options.Events.OnRedirectToIdentityProvider = context => 
{
    // only modify requests to the authorization endpoint
    if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
    {
        // generate code_verifier
        var codeVerifier = CryptoRandom.CreateUniqueId(32);
    
        // store codeVerifier for later use
        context.Properties.Items.Add("code_verifier", codeVerifier);
    
        // create code_challenge
        string codeChallenge;
        using (var sha256 = SHA256.Create())
        {
            var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
            codeChallenge = Base64Url.Encode(challengeBytes);
        }
    
        // add code_challenge and code_challenge_method to request
        context.ProtocolMessage.Parameters.Add("code_challenge", codeChallenge);
        context.ProtocolMessage.Parameters.Add("code_challenge_method", "S256");
    }
    
    return Task.CompletedTask;
};

We can also tighten up our IdentityServer4 client configuration by setting RequirePkce to true, and AllowPlainTextPkce to false.

Adding PKCE Support to the Token Request

By adding a code_challenge to our authorization request, we should now have broken our integration with our authorization server. It should now be complaining that a code_verifier is missing. This is intended, and we can now address that by completing our PKCE implementation by including the plaintext code_verifier in our token request. Again, this can be achieved using the event on the OpenIdConnectOptions:

options.Events.OnAuthorizationCodeReceived = context =>
{
    // only when authorization code is being swapped for tokens
    if (context.TokenEndpointRequest?.GrantType == OpenIdConnectGrantTypes.AuthorizationCode)
    {
        // get stored code_verifier
        if (context.Properties.Items.TryGetValue("code_verifier", out var codeVerifier))
        {
            // add code_verifier to token request
            context.TokenEndpointRequest.Parameters.Add("code_verifier", codeVerifier);
        }
    }

    return Task.CompletedTask;
};

And now you should be able to use your authorization server again, but now with some added PKCE.

Hopefully the ASP.NET Core team will add PKCE support to their library at some point; however, this isn’t that much for us to maintain, and there’s not too much that can go wrong (famous last words).

Source Code

As always, a working implementation is available on GitHub.