10 Jan 2020
cat: Angular 2+
4 Comments

Angular 8 OAuth 2 Authorization Code Flow with PKCE

Introduction

In this tutorial we will create an Angular application that authenticates using Authorization Code flow with PKCE. PKCE stands for Public Key Code Exchange and is useful authentication code flow when you know it is not safe for the app to store the client secret such as SPAs (Single Page Apps).

Setup

To generate a new Angular project you can run this command.

ng new myapp --routing

Run this command to install the necessary packages.

npm install angular-2-local-storage crypto-js @types/crypto-js --save

The first package angular-2-local-storage will be used to store the state and code_verifier. These values will be used to get the access token after the authorization server redirects back to your app.

code_verifier will be hashed using crypto-js Base 64 encoding and SHA256 hashing to create a code_challenge. @types/crypto-js is just the typescript definition files for crypto-js.

Now we can import the modules that we will need namely HttpClientModule and LocalStorageModule.

src\app\app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { LocalStorageModule } from 'angular-2-local-storage';
import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { HomeComponent } from './components/home/home.component';
import { AuthComponent } from './components/auth/auth.component';

@NgModule({
    declarations: [
        AppComponent,
        HomeComponent,
        AuthComponent,
    ],
    imports: [
        BrowserModule,
        AppRoutingModule,
        HttpClientModule,
        LocalStorageModule.forRoot({
            prefix: 'tutorial',
            storageType: 'localStorage'
        })
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }

We also have some environment variables that we will access in other files.

src\environments\environment.prod.ts

export const environment = {
    production: true,
    oauthClientId: '4',
    oauthLoginUrl: 'http://api.tutorial.test/oauth/authorize',
    oauthTokenUrl: 'http://api.tutorial.test/oauth/token',
    oauthCallbackUrl: 'http://localhost:4200/oauth/callback',
};

Redirecting to Authorization Server to Login

The login URL for Authorization Server will need a state, code_verifier, and code_challenge parameter which are random strings. There are a number of ways to generate a random string and here is one we can use.

You can put this to any files where you login. For this tutorial we have a login link in the header which is accessible to the app.component.ts.

src\app\app.component.ts

private strRandom(length: number) {
    let result = '';
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    const charactersLength = characters.length;
    for (let i = 0; i < length; i++) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
    }
    return result;
}

Next is to write another method that generates the login URL and redirects to it.

src\app\app.component.ts

goToLoginPage() {

    const state = this.strRandom(40);
    const codeVerifier = this.strRandom(128);

    this.storage.set('state', state);
    this.storage.set('codeVerifier', codeVerifier);

    const codeVerifierHash = CryptoJS.SHA256(codeVerifier).toString(CryptoJS.enc.Base64);

    const codeChallenge = codeVerifierHash
        .replace(/=/g, '')
        .replace(/\+/g, '-')
        .replace(/\//g, '_');

    const params = [
        'response_type=code',
        'state=' + state,
        'client_id=' + environment.oauthClientId,
        'scope=read_user_data write_user_data',
        'code_challenge=' + codeChallenge,
        'code_challenge_method=S256',
        'redirect_uri=' + encodeURIComponent(environment.oauthCallbackUrl),
    ];

    window.location.href = environment.oauthLoginUrl + '?' + params.join('&');
}

In this method we first generate random strings for state and code_verifier and store it to the browser’s local storage. We then hashed the code_verifier using SHA256 and encode to Base 64. Before passing this value as code_challenge, we must first remove the “=” from the Base 64 string and replace “+” and “\” to “-” and “_” respectively if there are any.

Make sure that redirect_uri is the same URI saved for the client_id.

After that we have an array of query parameters needed by the authorization server and redirect to it. We can now use this method in our HTML.

src\app\app.component.html

<a class="nav-link"
   (click)="goToLoginPage()">Login</a>

Now clicking the link will redirect to a URL like this.

http://api.tutorial.test/oauth/authorize?client_id=4&code_challenge=DtwrxXBJiCrYc6FF3sNjNAz_-24_WD1AEK0OYyqShE8&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A4200%2Foauth%2Fcallback&response_type=code&scope=read_user_data%20write_user_data&state=aJeTxvqipKE9iYZAyuXaHcd1CFhiS3u0r5gordn4

Here are some screenshots.

Angular 8 OAuth 2 Authorization Code Flow with PKCE

Angular 8 OAuth 2 Authorization Code Flow with PKCE

The “Authorize” button will redirect back to the Angular app with a query parameter code that we can use to get an access token.

Getting an Access Token

We will need to create our callback URL. You can run this command to generate a component that we will use to handle it.

ng generate component auth

Then we can update the routing module and add a new path 'oauth/callback' using AuthComponent that we just generated.

src\app\app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { AuthComponent } from './components/auth/auth.component';

const routes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'oauth/callback', component: AuthComponent },
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

When AuthComponent loads, we can do a couple of things.

First check the state query parameter against the store state in the local storage. Then if that is successful, we can use the code query parameter and submit it to the authorization server along with the code_verifier from the local storage.

src\app\components\auth\auth.component.ts

import { HttpClient, HttpParams } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { LocalStorageService } from 'angular-2-local-storage';
import { environment } from 'src/environments/environment';


@Component({
    templateUrl: './auth.component.html',
    styles: []
})
export class AuthComponent implements OnInit {

    constructor(
        private activatedRoute: ActivatedRoute,
        private http: HttpClient,
        private storage: LocalStorageService
    ) {
    }

    ngOnInit() {
        this.activatedRoute.queryParams.subscribe(params => {
            if (params.code) {
                this.getAccessToken(params.code, params.state);
            }
        });
    }

    getAccessToken(code: string, state: string) {

        if (state !== this.storage.get('state')) {
            alert('Invalid state');
            return;
        }

        const payload = new HttpParams()
            .append('grant_type', 'authorization_code')
            .append('code', code)
            .append('code_verifier', this.storage.get('codeVerifier'))
            .append('redirect_uri', environment.oauthCallbackUrl)
            .append('client_id', environment.oauthClientId);

        this.http.post(environment.oauthTokenUrl, payload, {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        }).subscribe(response => {
            console.log(response);
        });
    }
}

For demonstration purposes we just log the response to the console. You can put these values to the local storage like we did on state and code_verifier and use it to make request to the resource server.

Angular 8 OAuth 2 Authorization Code Flow with PKCE

That is it for this tutorial. Thanks for reading!

References

  1. Andres de la Cruz


    Hi, thx for the post, is what i looking for, but i have a question about handle the send and reception of code challenge. which approach is better, handle it by backend or frontend?.

Post A Comment