Secure Your SPA with Authorization Code Flow with PKCE

Single page applications (SPAs) offer many benefits over classic web applications. Among these benefits, SPAs have the ability to provide users with a rich and responsive user interface. This is largely due to the fact that much of a SPA’s application logic resides in the browser. If you’re wondering how to secure an application that primarily runs in the browser then you’ve come to the right place! In this article, we will discuss how you can leverage OpenID Connect with Angular to secure an ASP.NET Core 3.0 web application.

Implicit Flow

In the past, the OAuth working group’s recommendation for securing a SPA was Implicit Flow. With Implicit Flow, unauthenticated users are sent to an identity provider’s authorization endpoint. Following successful authentication, the end-user is redirected back to the client application with a token included in the url.

While returning the token in the redirect url might make you feel uneasy, there isn’t much risk when only requesting an id token (as shown below). Id tokens typically only contain identity data such as the user’s name, email, etc. Unlike an access token, id tokens are not used for authentication.

GET https://some-authorization-server.com/authorize 
&client_id=…
&response_type=id_token 
&redirect_uri=https://my-web-app.com/callback
&scope=openid profile 
&state=…
&nonce=…

That said, it is very common for a SPA to call an API to interact with persistent data. In order to authenticate, an access token is required. Access tokens are credentials used to “access” to a secure resource. In this case, the secure resource is the API we are calling from our SPA.

This is precisely where the problem lies. Returning an access token to the client in the redirection url could result in the token ending up in your browser’s history or in a referrer header.

As you probably guessed due to their sensitivity, we do not want to transmit access tokens in the url if we can avoid it. This is why the OAuth2 IETF working group now recommends using Authorization Code Flow with PKCE to secure your Single Page Applications.

Authorization Code Flow with PKCE

People always ask me why Implicit Flow was recommended in the first place if Authorization Code Flow is inherently more secure. This is largely due to the fact that for many years browsers prevented JavaScript from making an HTTP request to a server that was hosted in a different domain. Implicit Flow was a way to work around this restriction by leveraging the redirection url. Tokens could be passed in the redirect URL instead of making a direct HTTP request. Fortunately, this is no longer required as the majority of modern browsers support Cross-Origin Resource Sharing (CORS).

This new capability, opens the door for the Authorization Code Flow. Unlike Implicit Flow, Authorization Code Flow happens in two steps. First, an Authorization Code is requested.

GET https://some-authorization-server.com/authorize 
&client_id=… 
&response_type=code 
&redirect_uri=https://my-web-app.com/callback 
&scope=openid profile
&code_challenge=xxxxxxxxxxxxx
&code_challenge_method=S256 
&state=…

Second, the code is exchanged for an access token. This is done in a separate HTTP request allowing the access token to be returned in the response body.

POST https://some-authorization-server.com/token
&client_id=… 
&code=xxxxxxxxxxxxx
&grant_type=authorization_code
&code_verifier=xxxxxxxxxxxxx
&redirect_uri=https://my-web-app.com/callback 

Where does PCKE (Proof of Code Key Exchange) come into play? Take another look at the initial request and note the code_challenge. The code challenge is a random value that gets cryptographically generated by a code verifier. This is the same code verifier that gets sent in the secondary request to the token endpoint. In order for an access token to be granted, the code_verifier must match the code_challenge. This is an additional security measure. If the authorization code gets intercepted, it’s is useless without the correct code verifier.

Angular and Auth0

So how do we get started? The first step is to find a suitable client library. Since OpenID Connect is merely a specification, a number of JavaScript SDKs are available. In this article, we will be using Auth0 as our identity provider so, it makes sense to use their auth0-spa-js library.

npm install @auth0/auth0-spa-js --save 

Once installed, we can get started by following the step-by-step instructions in the SPA quickstart guide. I won’t rehash these steps here however, I will point out that when using the Auth0 client library, there is no need to specify detailed OpenID Connect configuration. The library abstracts most of those details away, as shown below.

auth0Client$ = (from(
  createAuth0Client({
    domain: "YOUR_DOMAIN",
    client_id: "YOUR_CLIENT_ID",
    redirect_uri: `${window.location.origin}/callback`
  })

After completing the quickstart guide, you should notice a Log In button in your web application. Clicking the button redirects you to an Auth0 login screen. Using your browser’s built-in developer tools, you can see the Authorization Code Flow with PCKE in action! The initial redirect url is shown below.

https://espressocoder.auth0.com/authorize
  ?client_id=...
  &redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Fcallback
  &scope=openid%20profile%20email
  &response_type=code
  &response_mode=query
  &state=UXVlV2FGbkpJZGRRUlQyRGJ1aHdVN1BZOWRLYWVxWS5tbHQ4NjVOQn42QQ%3D%3D
  &nonce=D5K7B-ZjdPiahF6wL1lfj9T-YXvxjWab12wi-Da1F6F
  &code_challenge=jtxUMOTQ-6Kfrxvxp-EGv20t9YmUzG2ZZIy0O7aRt1w
  &code_challenge_method=S256
  &auth0Client=eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMy4xIn0%3D

As we’ve seen before, the URL contains familiar query string parameters such as response_type=code as well as a code challenge. This establishes with Auth0 how we plan on retrieving our token. After we are successfully authenticated, we are returned an authorization code as a URL fragment and as we’d expect, this is followed up by a subsequent request to retrieve our access token.

https://espressocoder.auth0.com/oauth/token

{
  "grant_type":"authorization_code",
  "redirect_uri":"http://localhost:4200",
  "client_id":"...",
  "code_verifier":"kzdJ7.XT4QQf.2oOfvfoXLIdTapXm2PBANx8G078PpS",
  "code":"nsg2f5jYYAMP6lnC"
} 

Once the code and code_verifier are validated, an access_token is returned. As we’ve mentioned, this is great as we avoid returning this information in the URL. What do we do with this amazing access_token? Let’s take a look next at how we can use an access_token for authentication in ASP.NET Core 3.0.

Validating your token in ASP.NET Core

Being the defacto standard for authentication in the web, OpenID Connect is supported by most web frameworks. Of course, ASP.NET Core is no different. To start requiring an access token, only a few changes are required. First, we need to add the following lines to the ConfigureServices function.

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(o =>
    {
        o.Authority = "https://espressocoder.auth0.com";
        o.Audience = "https://jrtech.oauth.samples";
    });

    services.AddControllers();
}

Next, we need to add the Authentication middleware to the Configure function. As you may already know, the sequence is important here so, we want to be sure to add the Authentication middleware just before the UseEndpoints middleware is added to the request pipeline.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();
            
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

The last step is to identify the controllers and actions that need to be secured and decorate them with the *Authorize* attribute. Once this is complete, making requests without a valid access token results in an 401 Unauthorized status code!