Migrating oidc-client-js to use the OpenID Connect Authorization Code Flow and PKCE

Scott Brady
Scott Brady
Angular

Recently, there’s been a bit of a palaver around a draft specification proposed to the OAuth Working Group and its recommendation of abandoning the implicit flow in browser-based applications, e.g. Single Page Applications (SPAs), in favor of the authorization code flow with Proof-Key for Code Exchange (PKCE).

This article is going to look at how to update the Angular application found in my previous article “SPA Authentication using OpenID Connect, Angular CLI and oidc-client”, to start using the authorization code flow and PKCE.

Angular logo OpenID logo

Do I need to panic and abandon the implicit flow?

No. If you are using OpenID Connect in the way I’ve previously detailed, then the only significant benefit this new recommendation gives us is that we completely remove access tokens from the URL. Since we were using the hash fragment response type, then this was already a pretty slim risk, especially compared with the fact that we store tokens in browser storage. This is more of an exercise in hardening than patching a major vulnerability.

For pragmatic advice around the use of the implicit flow, check out:

I’m sure I’ll write my own ranty article on the subject at some point…

Oidc-client-js using the authorization code flow and PKCE

First, you’ll need the latest version of oidc-client:

"oidc-client": "^1.6.1"

You’ll then need to update the UserManagerSettings to something that looks like the following:

export function getClientSettings(): UserManagerSettings {
  return {
    authority: 'http://localhost:5555/',
    client_id: 'angular_spa',
    redirect_uri: 'http://localhost:4200/auth-callback',
    post_logout_redirect_uri: 'http://localhost:4200/',
    response_type: "code",
    scope: "openid profile api1",
    filterProtocolClaims: true,
    loadUserInfo: true
  };
}

All that’s changed is that the response_type is now code, meaning we receive an authorization code in return from the authorization endpoint, instead of an identity token and access token.

By setting this response type, two other things change in the background:

  • PKCE starts being used automatically (a code_challenge will be sent in the authorization request and a code_verifier in our new token request)
  • The response mode changes to query, at least when using IdentityServer4. This means that the authorization code is returned in the query string as opposed to the hash fragment that we typically used with the implicit flow

The use of PKCE gives us validation opportunities within both the authorization server and the client application. The authorization server can confirm that it is the client application swapping the code for tokens is the same as the client who made the initial request (using PKCE), and the client app can confirm that those tokens were intended in response to its request (using the nonce value). Where we store the code verifier and nonce values in our client application affects how much benefits this really brings us.

Using the hash fragment as the response mode will further limit the exposure of the authorization code, however, support for this is still being built into IdentityServer4 at time of writing.

IdentityServer4 Configuration for Browser-Based Client Application

IdentityServer4 requires 3 changes: the AllowedGrantTypes, RequirePkce, and RequireClientSecret. The latter is unnecessary due to the client application still being a public client, and therefore untrustworthy when it comes to client credentials.

This makes our IdentityServer4 client configuration look something like:

new Client {
    ClientId = "angular_spa",
    ClientName = "Angular 4 Client",
    AllowedGrantTypes = GrantTypes.Code,
    RequirePkce = true,
    RequireClientSecret = false,
    AllowedScopes = new List<string> {"openid", "profile", "api1"},
    RedirectUris = new List<string> {"http://localhost:4200/auth-callback", "http://localhost:4200/silent-refresh.html"},
    PostLogoutRedirectUris = new List<string> {"http://localhost:4200/"},
    AllowedCorsOrigins = new List<string> {"http://localhost:4200"},
    AllowAccessTokensViaBrowser = true
}

Refreshing Tokens

Silent refresh is still the preferred method for getting new tokens. In my opinion, refresh tokens are still way too risky to have within a client application running within the context of the browser. Silent refresh still works using the method detailed in my article “Silent Refresh - Refreshing Access Tokens when using the Implicit Flow”.

Until refresh tokens can be bound to both a client application and that individual session (maybe token binding or mutual TLS?), then I’m sticking with silent refresh.

Source Code

I’ve updated the GitHub repository for my existing article to use authorization code and PKCE. Migration, as you can see is relatively painless. You can still access the implicit version on the branch implicit.