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
That is it for this tutorial. Thanks for reading!
References
- Tutorial Source Codes – Does not include node_modules directory so you will have to run
npm install
. - Part 1 API Authentication
- Part 3 API Get Photos and Videos
- Part 4 Client Get Photos and Videos
- Part 5 API File Upload
- Part 6 Client Form File Upload
- Laravel API and Angular Client Tutorial – Part 1 API Authentication
- Angular 8 OAuth 2 Authorization Code Flow with PKCE