26 Jan 2020
cat: Angular 2+, API
0 Comments

Laravel API and Angular Client Tutorial – Part 2 Client OAuth Login

Introduction

In this tutorial we will create an Angular application that will authenticate to a Laravel OAuth server. We will use the authorization code with PKCE flow since the Angular application is an SPA or Single-page-application. In SPAs we can’t store the client secret since it will be visible in the browser. So we are going to write the codes that will generate the correct login URL and redirect to it. After logging in to that URL the OAuth server will redirect back to our app where we can get an access token that we can use to request protected resources.

Most of these steps are also covered in my other post here. The difference here is that we move some of the codes to service classes and display the login, logout, register button work at the right time.

Setup

To create a new Angular project we run this command.

ng new tutorial --routing

The the project directory is “tutorial” and --routing flag will tell the CLI to add routing as well.

Next is to install the necessary packages using this command.

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

In src\app\app.module.ts we import the packages as well as HttpClientModule.

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

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

We will add a bit of styling to our project and use bootstrap. If you want to customize bootstrap you may need to install it using npm and extend the styles.

src\index.html

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Tutorial</title>
    <base href="/">
    <meta name="viewport"
          content="width=device-width, initial-scale=1">
    <link rel="icon"
          type="image/x-icon"
          href="favicon.ico">
    <link rel="stylesheet"
          href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
          integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
          crossorigin="anonymous">
</head>

<body>
    <app-root></app-root>
</body>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
        crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
        integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
        crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
        integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
        crossorigin="anonymous"></script>

</html>

Also in the src\app\app.component.html we remove the generated HTML codes and replace it with this navigation bar and a container for the main content.

<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
  <a class="navbar-brand"
     href="#">Photo & Video Sharing App</a>
  <button class="navbar-toggler"
          type="button"
          data-toggle="collapse"
          data-target="#navbarsExampleDefault"
          aria-controls="navbarsExampleDefault"
          aria-expanded="false"
          aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse"
       id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link"
           href="#">Home <span class="sr-only">(current)</span></a>
      </li>
    </ul>
    <ul class="navbar-nav">
      <li class="nav-item">
        <a class="nav-link"
           href="#">Login</a>
      </li>
      <li class="nav-item">
        <a class="nav-link"
           href="#">Register</a>
      </li>
    </ul>
  </div>
</nav>
<main role="main"
      class="container">
  <router-outlet></router-outlet>
</main>

We also need to add some padding at the top because the navbar is fixed.

src\styles.css

body {
  padding-top: 5rem;
}

Home Page

To generate a new component we run this command.

ng generate component components/home --skip-tests --inline-style

src\app\app-routing.module.ts

// ... other imports
import { HomeComponent } from './components/home/home.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
];

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

Authentication Service

We will create a service where we can put the logic for login, logout as well as event emitters for them. We may need to subscribe to such events to tell other components that the user has logged-in or logged-out.

ng generate service services/auth --skip-tests

src\app\services\auth.service.ts

import { HttpClient, HttpParams } from '@angular/common/http';
import { EventEmitter, Injectable } from '@angular/core';
import { LocalStorageService } from 'angular-2-local-storage';
import * as CryptoJS from 'crypto-js';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  public onLogin: EventEmitter<boolean> = new EventEmitter();
  public onLogout: EventEmitter<boolean> = new EventEmitter();

  private isAuth = false;

  constructor(
    private http: HttpClient,
    private storage: LocalStorageService
  ) {
    const acessToken: string = this.storage.get('accessToken') || '';
    this.isAuth = acessToken.length > 0;
  }

  get isAuthenticated() {
    return this.isAuth;
  }

  public getLoginUrl(): string {

    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),
    ];

    return environment.oauthLoginUrl + '?' + params.join('&');
  }

  public getAccessToken(code: string, state: string): Observable<any> {

    if (state !== this.storage.get('state')) {
      return new Observable(o => {
        o.next(false);
      });
    }

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

    return this.http.post(environment.oauthTokenUrl, payload, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }).pipe(map((response: any) => {
      this.storage.set('tokenType', response.token_type);
      this.storage.set('expiresIn', response.expires_in);
      this.storage.set('accessToken', response.access_token);
      this.storage.set('refreshToken', response.refresh_token);
      this.isAuth = true;
      this.onLogin.emit(true);
      return response;
    }));
  }

  public logout() {
    this.storage.clearAll();
    this.isAuth = false;
    this.onLogout.emit(false);
  }

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

You can read more about the login URL on this blog post.

When a user login we store the tokens in the local storage and fire an event. Also, the logout removes the tokens from the local storage and fire an event. Aside from that, we have a handy public property isAuthenticated where we can check if there is a user logged in. This will be useful when navigating to other URLs and you want to check if the user can access the page or not.

Of course you can add another request if you like after the user has logged-in like get the profile data.

AppComponent holds the HTML codes for the navigation bar e.g. login, register, etc. So we can use the AuthService here.

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { environment } from 'src/environments/environment';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {

  isAuthenticated = false;

  constructor(
    private auth: AuthService,
    private router: Router
  ) {
    this.auth.onLogin.subscribe((b: boolean) => {
      this.isAuthenticated = b;
    });
    this.auth.onLogout.subscribe((b: boolean) => {
      this.isAuthenticated = b;
    });
  }

  ngOnInit() {
    this.isAuthenticated = this.auth.isAuthenticated;
  }

  goToLoginPage() {
    window.location.href = this.auth.getLoginUrl();
  }

  goToRegisterPage() {
    window.location.href = environment.registerUrl;
  }

  logout() {
    this.auth.logout();
    this.router.navigate(['/']);
  }
}

In this update, we have a isAuthenticated property that we can use to update the navigation bar whether to show or hide the links.

src\app\app.component.html

<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
  <a class="navbar-brand"
     href="#">Photo & Video Sharing App</a>
  <button class="navbar-toggler"
          type="button"
          data-toggle="collapse"
          data-target="#navbarsExampleDefault"
          aria-controls="navbarsExampleDefault"
          aria-expanded="false"
          aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse"
       id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link"
           routerLink="/">Home <span class="sr-only">(current)</span></a>
      </li>
    </ul>
    <ul class="navbar-nav">
      <li class="nav-item"
          *ngIf="isAuthenticated">
        <a class="nav-link"
           routerLink="post">Post</a>
      </li>
      <li class="nav-item"
          *ngIf="isAuthenticated">
        <a class="nav-link"
           (click)="logout()">Logout</a>
      </li>
      <li class="nav-item"
          *ngIf="!isAuthenticated">
        <a class="nav-link"
           (click)="goToLoginPage()">Login</a>
      </li>
      <li class="nav-item"
          *ngIf="!isAuthenticated">
        <a class="nav-link"
           (click)="goToRegisterPage()">Register</a>
      </li>
    </ul>
  </div>
</nav>
<main role="main"
      class="container">
  <router-outlet></router-outlet>
</main>

Trying it out

Laravel API and Angular Client Tutorial – Part 2 Client OAuth Login

Laravel API and Angular Client Tutorial – Part 2 Client OAuth Login

Laravel API and Angular Client Tutorial – Part 2 Client OAuth Login

Laravel API and Angular Client Tutorial – Part 2 Client OAuth Login

That is it for this tutorial. Thanks for reading!

References

Be the first to write a comment.

Post A Comment