Angular 10 Tutorial: Oauth2 Login and Refresh Token

by Didin J., updated on Oct 21, 2020 Angular 10 Tutorial: Oauth2 Login and Refresh Token

The comprehensive step by step Angular 10 tutorial on implementing Oauth2 login and refresh token in front-end web app

In this Angular 10 tutorial, we will implement the Oauth2 login and refresh token in the front-end web app. We will use our existing Node-Express-PostgreSQL Oauth2 server as the back-end. You can get the tutorial and the source code for the back-end here.

This tutorial divided into several steps:

The scenario for this tutorial is very simple. New users register to the Angular application using username, password, and name. The registered user login to the Angular app to get an access token and refresh token. The access token and refresh token save to local storage. Every request to the secure endpoint from the secure or guarded page should contain a valid access token.

The following tools, frameworks, and modules are required for this tutorial:

  1. Node.js (can run npm or yarn)
  2. Angular 10
  3. Angular-CLI
  4. Node-Express-PostgreSQL-Oauth2
  5. PostgreSQL Server
  6. Terminal or Node Command Line
  7. IDE or Text Editor

We assume that you have installed the latest recommended Node.js on your computer. Let's check them by type these commands in the terminal or Node command line.

node -v
v12.18.0
npm -v
6.14.7

That's our Node and NPM version. Let's get started with the main steps!


Step #1: Create an Angular 10 Application

Creating the Angular 10 application, we will use Angular-CLI. The Angular CLI is a command-line interface tool that use to initialize, develop, scaffold, and maintain Angular applications directly from a terminal or cmd. To install the Angular-CLI, type this command from the terminal and cmd.

sudo npm install -g @angular/cli

Now, we can create a new Angular 10 application by type this command.

ng new angular-oauth2

If you get these questions, just choose 'y' and 'SCSS'.

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/
documentation/syntax#scss                ]

Next, go to the newly created Angular 10 application then check it by running the application for the first time.

cd ./angular-oauth2 && ng serve

Open the browser then go to 'localhost:4200' and you will this standard Angular welcome page.

Angular 10 Tutorial: Oauth2 Login and Refresh Token - Angular Home


Step #2: Add Token and API Service

We will do any token CRUD operation in service, also, REST API operation in a different service. For that, generate these services using Angular-CLI.

ng g service auth
ng g service token

Open and edit `src/app/token.service.ts` then add these constant variables after the imports.

const ACCESS_TOKEN = 'access_token';
const REFRESH_TOKEN = 'refresh_token';

Add these token CRUD operation functions after the constructor function.

  getToken(): string {
    return localStorage.getItem(ACCESS_TOKEN);
  }

  getRefreshToken(): string {
    return localStorage.getItem(REFRESH_TOKEN);
  }

  saveToken(token): void {
    localStorage.setItem(ACCESS_TOKEN, token);
  }

  saveRefreshToken(refreshToken): void {
    localStorage.setItem(REFRESH_TOKEN, refreshToken);
  }

  removeToken(): void {
    localStorage.removeItem(ACCESS_TOKEN);
  }

  removeRefreshToken(): void {
    localStorage.removeItem(REFRESH_TOKEN);
  }

For accessing REST API, we will use the built-in Angular 10 feature HTTP Client module. Open and edit `src/app/app.module.ts` then add this HTTP Client module to the @NgModule import array.

  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule
  ],

Don't forget to add imports to that module.

import { HttpClientModule } from '@angular/common/http';

Next, open and edit `src/app/auth.service.ts` then declare the required OAuth client and secret and REST API URL as the constant variable.

const OAUTH_CLIENT = 'express-client';
const OAUTH_SECRET = 'express-secret';
const API_URL = 'http://localhost:3000/';

Also, add a constant variable that contains the headers for Form data and basic authorization that convert OAuth2 client and secret to basic auth token.

const HTTP_OPTIONS = {
  headers: new HttpHeaders({
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: 'Basic ' + btoa(OAUTH_CLIENT + ':' + OAUTH_SECRET)
  })
};

Declare the required variable a the top of the class body.

  redirectUrl = '';

Add the functions that handle error response and log message before the constructor.

  private static handleError(error: HttpErrorResponse): any {
    if (error.error instanceof ErrorEvent) {
      console.error('An error occurred:', error.error.message);
    } else {
      console.error(
        `Backend returned code ${error.status}, ` +
        `body was: ${error.error}`);
    }
    return throwError(
      'Something bad happened; please try again later.');
  }

  private static log(message: string): any {
    console.log(message);
  }

Inject the HttpClient and TokenService to the constructor.

  constructor(private http: HttpClient, private tokenService: TokenService) {
  }

Add the required API calls after the constructor.

  login(loginData: any): Observable<any> {
    this.tokenService.removeToken();
    this.tokenService.removeRefreshToken();
    const body = new HttpParams()
      .set('username', loginData.username)
      .set('password', loginData.password)
      .set('grant_type', 'password');

    return this.http.post<any>(API_URL + 'oauth/token', body, HTTP_OPTIONS)
      .pipe(
        tap(res => {
          this.tokenService.saveToken(res.access_token);
          this.tokenService.saveRefreshToken(res.refresh_token);
        }),
        catchError(AuthService.handleError)
      );
  }

  refreshToken(refreshData: any): Observable<any> {
    this.tokenService.removeToken();
    this.tokenService.removeRefreshToken();
    const body = new HttpParams()
      .set('refresh_token', refreshData.refresh_token)
      .set('grant_type', 'refresh_token');
    return this.http.post<any>(API_URL + 'oauth/token', body, HTTP_OPTIONS)
      .pipe(
        tap(res => {
          this.tokenService.saveToken(res.access_token);
          this.tokenService.saveRefreshToken(res.refresh_token);
        }),
        catchError(AuthService.handleError)
      );
  }

  logout(): void {
    this.tokenService.removeToken();
    this.tokenService.removeRefreshToken();
  }

  register(data: any): Observable<any> {
    return this.http.post<any>(API_URL + 'oauth/signup', data)
      .pipe(
        tap(_ => AuthService.log('register')),
        catchError(AuthService.handleError)
      );
  }

  secured(): Observable<any> {
    return this.http.get<any>(API_URL + 'secret')
      .pipe(catchError(AuthService.handleError));
  }


Step #3: Add Angular HTTP Interceptor

Now, we need to add an Angular HTTP Interceptor that intercepts the request and response from the REST API calls. Create a new file `src/app/auth.interceptor.ts` then add these imports to that file.

import {Injectable} from '@angular/core';
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http';
import {Router} from '@angular/router';
import {throwError} from 'rxjs';
import {TokenService} from './token.service';
import {catchError, map} from 'rxjs/operators';
import {AuthService} from './auth.service';

Add the injectable class that implements the HttpInterceptor.

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

}

Add a constructor with Router, AuthService, and TokenService parameters.

  constructor(
    private router: Router,
    private tokenService: TokenService,
    private authService: AuthService) {
  }

Add this intercept function after the constructor.

  intercept(request: HttpRequest<any>, next: HttpHandler): any {

    const token = this.tokenService.getToken();
    const refreshToken = this.tokenService.getRefreshToken();

    if (token) {
      request = request.clone({
        setHeaders: {
          Authorization: 'Bearer ' + token
        }
      });
    }

    if (!request.headers.has('Content-Type')) {
      request = request.clone({
        setHeaders: {
          'content-type': 'application/json'
        }
      });
    }

    request = request.clone({
      headers: request.headers.set('Accept', 'application/json')
    });

    return next.handle(request).pipe(
      map((event: HttpEvent<any>) => {
        if (event instanceof HttpResponse) {
          console.log('event--->>>', event);
        }
        return event;
      }),
      catchError((error: HttpErrorResponse) => {
        console.log(error.error.error);
        if (error.status === 401) {
          if (error.error.error === 'invalid_token') {
            this.authService.refreshToken({refresh_token: refreshToken})
              .subscribe(() => {
                location.reload();
              });
          } else {
            this.router.navigate(['login']).then(_ => console.log('redirect to login'));
          }
        }
        return throwError(error);
      }));
  }

If there's a token available, the request will intercept by headers that contain Authorization with Bearer token value and application/json content-type. Every successful response will be noted to the console log. Every error response will catch and specify if the response status is 401 (Unauthorized). Also, specified again the 401 to check the error message that contains "invalid_token". If found, then execute the refresh token API calls otherwise, it will redirect to the login page.

Register that created AuthInterceptor class to the app.module.ts inside the provider's array.

  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ],

Don't forget to add the imports.

import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {AuthInterceptor} from './auth.interceptor';


Step #4: Add Angular 10 Routing and Navigation

As you see in the previous Angular creation, there are also creating a routing. So, we have just need to modify the current generated routing to add the guarded or secured routes. Open this Angular 10 project using your IDE or text editor. Before modifying the Angular routing, add these required components by type these commands.

ng g component login
ng g component register
ng g component secure
ng g component not-found

Those commands will generate all required files (TS, HTML, SCSS) for the required components. They also added to the `src/app/app.module.ts`. Now, we can open and edit `src/app/app-routing.module.ts`. Modify the blank route array to be like this.

const routes: Routes = [
  { path: '', redirectTo: 'secure', pathMatch: 'full' },
  { path: 'secure', canActivate: [ AuthGuard ], component: SecureComponent },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: '404', component: NotFoundComponent },
  { path: '**', redirectTo: '404' }
];

As you can see, there is canActivate: [ AuthGuard ] parameter for the secure route. We can create this module later. Also, redirect to the not found page if navigate to the page other than registered routes. Using the right IDE, it will import the components automatically while typing the component name. Or you can import the component manually along with the AuthGuard module.

import { SecureComponent } from './secure/secure.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { AuthGuard } from './auth.guard';
import { NotFoundComponent } from './not-found/not-found.component';

Next, generate the AuthGuard module by type this command.

ng g guard auth

Then choose `CanActivate` same as the secure route parameter.

? Which interfaces would you like to implement? (Press <space> to select, <a> to
 toggle all, <i> to invert selection)
❯◉ CanActivate
 ◯ CanActivateChild
 ◯ CanDeactivate
 ◯ CanLoad

Open and edit `src/app/auth.guard.ts` then inject AuthService, TokenService, and Router to the constructor.

  constructor(private authService: AuthService, private tokenService: TokenService, private router: Router) {
  }

Modify the canActivate() function to be like this.

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean {
    const url: string = state.url;

    return this.checkLogin(url);
  }

Add a function to check if the user login or not.

  checkLogin(url: string): boolean {
    if (this.tokenService.getRefreshToken()) {
      return true;
    }

    this.authService.redirectUrl = url;

    this.router.navigate(['/login']).then(_ => false);
  }


Step #5: Implementing Login, Register, and Secure Page

We will use Angular Material to build UI/UX for this secure Angular 10 and Oauth2 application. First, install all required Angular modules and schematics.

ng add @angular/material

If there are questions like below, just use the default answer.

? Choose a prebuilt theme name, or "custom" for a custom theme: Indigo/Pink     
   [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? No
? Set up browser animations for Angular Material? Yes

We will register all required Angular 10 Material components or modules to `src/app/app.module.ts`. Open and edit that file then add these required imports of Angular Material components, forms, and reactive form module.

import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {MatInputModule} from '@angular/material/input';
import {MatPaginatorModule} from '@angular/material/paginator';
import {MatSortModule} from '@angular/material/sort';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatTableModule} from '@angular/material/table';
import {MatIconModule} from '@angular/material/icon';
import { NotFoundComponent } from './not-found/not-found.component';

Register those modules to `@NgModule` imports.

  imports: [
    ...
    FormsModule,
    ReactiveFormsModule,
    MatInputModule,
    MatPaginatorModule,
    MatProgressSpinnerModule,
    MatSortModule,
    MatTableModule,
    MatIconModule,
    MatButtonModule,
    MatCardModule,
    MatFormFieldModule
  ],

Next, open and edit `src/app/login/login.component.ts` then add these imports.

import { Component, OnInit } from '@angular/core';
import {AuthService} from '../auth.service';
import {ErrorStateMatcher} from '@angular/material/core';
import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms';
import {Router} from '@angular/router';

Add a class that implements ErrorStateMatcher before the @Component.

export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Modify @Component to use same SCSS file `app.component.scss`.

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['../app.component.scss']
})

Add all required variables before the constructor.

  loginForm: FormGroup;
  username = '';
  password = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

Inject the AuthService, Router, and FormBuilder to the constructor.

  constructor(private authService: AuthService, private router: Router, private formBuilder: FormBuilder) { }

Build the FormGroup inside the ngOnInit function.

  ngOnInit(): void {
    this.loginForm = this.formBuilder.group({
      username : [null, Validators.required],
      password : [null, Validators.required]
    });
  }

Add a function to submit the login form.

  onFormSubmit(): void {
    this.isLoadingResults = true;
    this.authService.login(this.loginForm.value)
      .subscribe(() => {
        this.isLoadingResults = false;
        this.router.navigate(['/secure']).then(_ => console.log('You are secure now!'));
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
  }

That submit function will call the login function from AuthService then navigate to the secure page on successful login or print the error to console if API calls error. Next, open and edit `src/app/login/login.component.html` then replace the default HTML template with this Angular Material template that contains the login form components.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="loginForm" (ngSubmit)="onFormSubmit()">
      <h2>Log In</h2>
      <mat-form-field class="example-full-width">
        <label>
          <input matInput placeholder="Username" formControlName="username"
                 [errorStateMatcher]="matcher">
        </label>
        <mat-error>
          <span *ngIf="!loginForm.get('username').valid && loginForm.get('username').touched">Please enter Username</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <label>
          <input type="password" matInput placeholder="Password" formControlName="password"
                 [errorStateMatcher]="matcher">
        </label>
        <mat-error>
          <span *ngIf="!loginForm.get('password').valid && loginForm.get('password').touched">Please enter Password</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!loginForm.valid" mat-flat-button color="primary"><mat-icon>login</mat-icon>Login</button>
      </div>
      <div class="button-row">
        <h4>Don't have an account?</h4>
      </div>
      <div class="button-row">
        <a mat-flat-button color="primary" [routerLink]="['/register']"><mat-icon>how_to_reg</mat-icon>Register</a>
      </div>
    </form>
  </mat-card>
</div>

Next, open and edit `src/app/register/register.component.html` then replace the imports with this.

import { Component, OnInit } from '@angular/core';
import {ErrorStateMatcher} from '@angular/material/core';
import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms';
import {AuthService} from '../auth.service';
import {Router} from '@angular/router';

Add a class that implements ErrorStateMatcher before the @Component.

export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Modify @Component to use same SCSS file `app.component.scss`.

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['../app.component.scss']
})

Add all required variables before the constructor.

  registerForm: FormGroup;
  username = '';
  password = '';
  name = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

Inject the AuthService, Router, and FormBuilder to the constructor.

  constructor(private authService: AuthService, private router: Router, private formBuilder: FormBuilder) { }

Build the FormGroup inside the ngOnInit function.

  ngOnInit(): void {
    this.registerForm = this.formBuilder.group({
      username : [null, Validators.required],
      password : [null, Validators.required],
      name : [null, Validators.required]
    });
  }

Add a function to submit the login form.

  onFormSubmit(): void {
    this.isLoadingResults = true;
    this.authService.register(this.registerForm.value)
      .subscribe((res: any) => {
        this.isLoadingResults = false;
        this.router.navigate(['/login']).then(_ => console.log('You are registered now!'));
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
  }

That submit function will call the register function from AuthService then navigate to the login page on successful registration or print the error to console if API calls error. Next, open and edit `src/app/register/register.component.html` then replace the default HTML template with this Angular Material template that contains the register form components.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="registerForm" (ngSubmit)="onFormSubmit()">
      <h2>Register</h2>
      <mat-form-field class="example-full-width">
        <label>
          <input matInput placeholder="Username" formControlName="username"
                 [errorStateMatcher]="matcher">
        </label>
        <mat-error>
          <span *ngIf="!registerForm.get('username').valid && registerForm.get('username').touched">Please enter Username</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <label>
          <input type="password" matInput placeholder="Password" formControlName="password"
                 [errorStateMatcher]="matcher">
        </label>
        <mat-error>
          <span *ngIf="!registerForm.get('password').valid && registerForm.get('password').touched">Please enter Password</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <label>
          <input matInput placeholder="Name" formControlName="name"
                 [errorStateMatcher]="matcher">
        </label>
        <mat-error>
          <span *ngIf="!registerForm.get('name').valid && registerForm.get('name').touched">Please enter Name</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!registerForm.valid" mat-flat-button color="primary"><mat-icon>how_to_reg</mat-icon>Register</button>
      </div>
      <div class="button-row">
        <h4>Already registered?</h4>
      </div>
      <div class="button-row">
        <a mat-flat-button color="primary" [routerLink]="['/login']"><mat-icon>login</mat-icon>Login</a>
      </div>
    </form>
  </mat-card>
</div>

Next, open and edit `src/app/secure/secure.component.ts` then replace all Typescript codes with this.

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

@Component({
  selector: 'app-secure',
  templateUrl: './secure.component.html',
  styleUrls: ['../app.component.scss']
})
export class SecureComponent implements OnInit {

  message = '';
  isLoadingResults = false;

  constructor(private authService: AuthService, private router: Router) { }

  ngOnInit(): void {
    this.isLoadingResults = true;
    this.authService.secured()
      .subscribe((data: any) => {
        this.message = data;
        console.log(data);
        this.isLoadingResults = false;
      });
  }

  logout(): void {
    this.authService.logout();
    this.router.navigate(['/login']).then(_ => console.log('Logout'));
  }

}

That Angular secure component load a secure REST API endpoint. If not authorized, it will redirect to the login page. Next, open and edit `src/app/secure/secure.component.html` then replace all HTML code with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <h4>{{message}}</h4>
    <div class="button-row">
      <a mat-flat-button color="primary" (click)="logout()"><mat-icon>logout</mat-icon>Logout</a>
    </div>
  </mat-card>
</div>

As you see in the previous Routes creation, there's a NotFoundComponent. We just need to modify the HTML file `src/app/not-found/not-found.component.html` then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <mat-card class="example-card">
    <h4>{{message}}</h4>
    <div class="button-row">
      <a mat-flat-button color="primary" (click)="logout()"><mat-icon>logout</mat-icon>Logout</a>
    </div>
  </mat-card>
</div>

Finally, we need to modify the default `src/app/app.component.html` with this.

<div class="container">
  <router-outlet></router-outlet>
</div>

Also, the `src/app/app.component.scss` that use by other components too.

.container {
  padding: 20px;
}

.example-container {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
  mat-icon {
    margin-right: 10px;
  }
}

.mat-flat-button {
  width: 120px;
  margin: 5px;
}


Step #6: Run and Test Angular 10 Oauth2 Login and Refresh Token

To make this Angular 10 OAuth2 application working, first, run the PostgreSQL server on your machine then run the Express-Oauth2-Postgre application.

cd NodeApps/express-oauth2-postgre
nodemon

Next, run the Angular 10 application in the separate terminal tab.

cd AngularApps/angular-oauth2
ng serve

And here we go, the full Angular 10 and OAuth2 application.

Angular 10 Tutorial: Oauth2 Login and Refresh Token - Demo 1
Angular 10 Tutorial: Oauth2 Login and Refresh Token - Demo 2
Angular 10 Tutorial: Oauth2 Login and Refresh Token - Demo 3

That it's, the Angular 10 Tutorial: Oauth2 Login and Refresh Token. You can get the full source code from our GitHub.

If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!

Loading…