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

Scott Brady
Scott Brady
OpenID Connect ・ Updated October 2019

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. Because of this, new specifications 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 (along with some IdentityServer4 config thrown in for good measure).

ASP.NET Core Support for PKCE

ASP.NET Core 2 does not include built-in support for PKCE out of the box, but ASP.NET Core 3 does, albeit only when using the authorization code flow. In this article, we are going to see how to use PKCE in both.

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";
    });

Don’t forget to call to AddAuthentication in your 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"}
}

Custom PKCE in ASP.NET Core 2

Adding PKCE Support to the Authorization Request

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).

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;
};

This is enough to make the authorization server require PKCE for the rest of the OAuth process, but 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).

Out of the box PKCE in ASP.NET Core 3

With ASP.NET Core 3, it’s a simple case of setting a property on OpenIdConnectOptions to true:

services.AddAuthentication()
    // other registrations
    .AddOpenIdConnect("oidc", options => {

        // existing config

        // Enable PKCE (authorization code flow only)
        options.UsePkce = true;
    });

Source Code

As always, a working implementation is available on GitHub. This had a working IdentityServer4 implementation and client applications for both ASP.NET Core 2 and ASP.NET Core 3.