Refreshing your Legacy ASP.NET IdentityServer Client Applications (with PKCE)

Scott Brady
Scott Brady
ASP.NET

If you have an ASP.NET MVC application in production that uses IdentityServer, you may soon find yourself in its codebase due to the upcoming SameSite cookie changes spearheaded by Google.

While you’re in there messing with the code, why don’t you give your old application a freshen up and update your OpenID Connect usage to take advantage of some of the features of the newer OWIN libraries and the latest security recommendations of authorization code plus PKCE?

Don’t want the history lesson? Skip to the good stuff. Or, if you are in the wrong place, check out how to use PKCE in ASP.NET Core.

To learn about PKCE in general and how it builds upon client authentication, check out my article "Client Authentication vs. PKCE: Do you need both?".

Original Code - 2015

Let’s say you last looked at this application in 2015 when you were using IdentityServer3. We told you to use the hybrid flow, and you have an OWIN startup class that looks something like the following:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "cookie"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = "mvc.owin",
            Authority = "http://localhost:5000",
            RedirectUri = "http://localhost:5001/",
            ResponseType = "code id_token",
            Scope = "openid profile api1",

            SignInAsAuthenticationType = "cookie",

            UseTokenLifetime = false,

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                AuthorizationCodeReceived = async n =>
                {
                    // swap authorization code for an access token
                    var tokenClient = new TokenClient(
                        "http://localhost:5000/connect/token", "mvc.owin", "secret");
                    var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
                        n.Code, n.RedirectUri);

                    // use access token to call the user info endpoint
                    var userInfoClient = new UserInfoClient("http://localhost:5000/connect/userinfo");
                    var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);

                    // create a new identity using the claims from the user info endpoint (including tokens)
                    var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                    id.AddClaims(userInfoResponse.Claims);
                    id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                    id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

                    n.AuthenticationTicket = new AuthenticationTicket(
                        new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType),
                        n.AuthenticationTicket.Properties);
                }
            }
        });
	}
}

That’s a large amount of custom code required to swap authorization codes, call the user info endpoint, and then store your tokens for later use. In ASP.NET Core, you can achieve the same functionality with significantly fewer lines of code.

Updated OWIN - 2019

Good news, a lot of the features from the ASP.NET Core OpenID Connect authentication middleware have found their way into version 4.1 of the OWIN security libraries. These updates mean that you can now achieve the same functionality from 2015 with the following code:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "cookie"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = "mvc.owin",
            Authority = "http://localhost:5000",
            RedirectUri = "http://localhost:5001/",
            ResponseType = "code id_token",
            Scope = "openid profile api1",

            SignInAsAuthenticationType = "cookie",

            UseTokenLifetime = false,

            RedeemCode = true,
            SaveTokens = true,
            ClientSecret = "secret"
        });
	}
}

Note that with this approach, tokens will be stored in the AuthenticationProperties like they are in ASP.NET Core.

Authorization Code with PKCE - 2020

Now that you have reduced the amount of code that you need to maintain, why don’t you now undo all that hard work in the name of security.

ASP.NET does not support PKCE out of the box, which means we’ll need to re-introduce some notifications. Luckily, these are not as complex as the ones we used to need for token swap, nor do they make any external calls. We’ll simply be adding some parameters to outbound requests (the code_verifier and code_challenge), but the bulk of our custom code will come from remembering the code verifier between requests.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = "cookie"
        });

        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = "mvc.owin",
            Authority = "http://localhost:5000",
            RedirectUri = "http://localhost:5001/",
            Scope = "openid profile api1",

            SignInAsAuthenticationType = "cookie",

            RequireHttpsMetadata = false,
            UseTokenLifetime = false,

            RedeemCode = true,
            SaveTokens = true,
            ClientSecret = "secret",

            ResponseType = "code",
            ResponseMode = "query",

            Notifications = new OpenIdConnectAuthenticationNotifications
            {
                RedirectToIdentityProvider = n =>
                {
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
                    {
                        // generate code verifier and code challenge
                        var codeVerifier = CryptoRandom.CreateUniqueId(32);

                        string codeChallenge;
                        using (var sha256 = SHA256.Create())
                        {
                            var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                            codeChallenge = Base64Url.Encode(challengeBytes);
                        }
    
                        // set code_challenge parameter on authorization request
                        n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
                        n.ProtocolMessage.SetParameter("code_challenge_method", "S256");

                        // remember code verifier in cookie (adapted from OWIN nonce cookie)
                        // see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L85
                        RememberCodeVerifier(n, codeVerifier);
                    }

                    return Task.CompletedTask;
                },
                AuthorizationCodeReceived = n =>
                {
                    // get code verifier from cookie
                    // see: https://github.com/scottbrady91/Blog-Example-Classes/blob/master/AspNetFrameworkPkce/ScottBrady91.BlogExampleCode.AspNetPkce/Startup.cs#L102
                    var codeVerifier = RetrieveCodeVerifier(n);

                    // attach code_verifier on token request
                    n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);

                    return Task.CompletedTask;
                }
            }
        });
	}
}

Unfortunately, the OWIN libraries do not give us a chance to mess with correlation cookies (from what I can tell), which means you must add your own to remember the code verifier. In this case, I have used the same approach as the nonce cookie, but using the state value to match the cookie to the current request. You can find this implementation on GitHub, as it’s just too big to reasonably fit in this blog post.

We have also set the response mode to “query”. Because we are using a proof-key, we don’t have to worry so much about exposing our authorization code across the wire. Using the query string simplifies the flow a little and also saves you having to worry about the correlation cookies having a SameSite value of Lax.

IdentityServer Configuration

As a result, your IdentityServer4 Client record for your app will need to change a little. Before enabling PKCE, your typical web application would likely have the following configuration, using the Hybrid flow and some form of client authentication. Note that RequirePkce is set to true by default in newer versions of IdentityServer.

new Client
{
    ClientId = "mvc.owin",
    ClientName = "MVC Client",
    AllowedGrantTypes = GrantTypes.Hybrid,
    ClientSecrets = {new Secret("secret".Sha256())},
    RedirectUris = {"http://localhost:5001/"},
    AllowedScopes = {"openid", "profile", "api1"}
}

Following this tutorial, you will end up with the following configuration, using the authorization code flow, PKCE, and client authentication. You need both client authentication and PKCE to be secure.

new Client
{
    ClientId = "mvc.owin",
    ClientName = "MVC Client",
    AllowedGrantTypes = GrantTypes.Code,
    ClientSecrets = {new Secret("secret".Sha256())},
    RedirectUris = {"http://localhost:5001/"},
    AllowedScopes = {"openid", "profile", "api1"},
    AllowPlainTextPkce = false,
    RequirePkce = true
}

Source Code

You can find a working sample on GitHub, including the above PKCE implementation integrated with IdentityServer4.