How to Build Spring Boot Angular User Registration and OAuth2 Social Login with Facebook, Google, LinkedIn, and Github – Part 3

Welcome to the 3rd part of the Spring Boot 2 Angular 10 OAuth2 Social Login tutorial series. In Part 1 & Part 2, we have implemented the backed Spring Boot REST application. In this article, we are going to implement the client with Angular 10 javascript framework to consume that Spring REST API.

What you’ll build

Login

Angular Login Page

Register

A

User Home

Angular user home page

Admin Home

Angular admin home page

Admin Profile

Angular Admin Profile Page

What you’ll need

Install Node.js

You can download and install the latest version from here.

Update NPM

npm is automatically installed along with the node. Since npm tends to get updated more frequently, we need to run the following command to update it to the latest version after installing Node.js

npm install [email protected] -g

Install TypeScript

npm install -g typescript

Install Angular CLI (Angular command line interface)

npm install -g @angular/cli

Design

Angular OAuth2 Social Login

Project Structure

+-- angular.json
|   karma.conf.js
|   package-lock.json
|   package.json
|   README.md
|   tsconfig.app.json
|   tsconfig.base.json
|   tsconfig.json
|   tsconfig.spec.json
|   tslint.json
|               
+---src
    |   favicon.ico
    |   index.html
    |   main.ts
    |   polyfills.ts
    |   styles.css
    |   test.ts
    |   
    +---app
    |   |   app-routing.module.ts
    |   |   app.component.css
    |   |   app.component.html
    |   |   app.component.spec.ts
    |   |   app.component.ts
    |   |   app.module.ts
    |   |   
    |   +---board-admin
    |   |       board-admin.component.css
    |   |       board-admin.component.html
    |   |       board-admin.component.spec.ts
    |   |       board-admin.component.ts
    |   |       
    |   +---board-moderator
    |   |       board-moderator.component.css
    |   |       board-moderator.component.html
    |   |       board-moderator.component.spec.ts
    |   |       board-moderator.component.ts
    |   |       
    |   +---board-user
    |   |       board-user.component.css
    |   |       board-user.component.html
    |   |       board-user.component.spec.ts
    |   |       board-user.component.ts
    |   |       
    |   +---common
    |   |       app.constants.ts
    |   |       
    |   +---home
    |   |       home.component.css
    |   |       home.component.html
    |   |       home.component.spec.ts
    |   |       home.component.ts
    |   |       
    |   +---login
    |   |       login.component.css
    |   |       login.component.html
    |   |       login.component.spec.ts
    |   |       login.component.ts
    |   |       
    |   +---profile
    |   |       profile.component.css
    |   |       profile.component.html
    |   |       profile.component.spec.ts
    |   |       profile.component.ts
    |   |       
    |   +---register
    |   |       register.component.css
    |   |       register.component.html
    |   |       register.component.spec.ts
    |   |       register.component.ts
    |   |       
    |   +---_helpers
    |   |       auth.interceptor.ts
    |   |       
    |   \---_services
    |           auth.service.spec.ts
    |           auth.service.ts
    |           token-storage.service.spec.ts
    |           token-storage.service.ts
    |           user.service.spec.ts
    |           user.service.ts
    |           
    +---assets
    |   |  
    |   |   
    |   \---img
    |           facebook.png
    |           github.png
    |           google.png
    |           linkedin.png
    |           logo.png
    |           
    \---environments
            environment.prod.ts
            environment.ts
         

Angular Application’s Entry Point

index.html

This is the entry point of the angular application

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Angular10SocialLogin</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>
</html>

As you can see above, we have used Bootstrap 4 for styling our application.

Also, note the custom <app-root></app-root> tags inside the <body> section is the root selector that Angular uses for rendering the application’s root component.

Creating Components

We can open the console terminal, then issue the following Angular CLI command to create a component.

ng generate component <name>

Root Component

Angular’s application files uses TypeScript, a typed superset of JavaScript that compiles to plain JavaScript. 

app.component.ts

import { Component, OnInit } from '@angular/core';
import { TokenStorageService } from './_services/token-storage.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private roles: string[];
  isLoggedIn = false;
  showAdminBoard = false;
  showModeratorBoard = false;
  username: string;

  constructor(private tokenStorageService: TokenStorageService) { }

  ngOnInit(): void {
    this.isLoggedIn = !!this.tokenStorageService.getToken();

    if (this.isLoggedIn) {
      const user = this.tokenStorageService.getUser();
      this.roles = user.roles;

      this.showAdminBoard = this.roles.includes('ROLE_ADMIN');
      this.showModeratorBoard = this.roles.includes('ROLE_MODERATOR');

      this.username = user.displayName;
    }
  }

  logout(): void {
    this.tokenStorageService.signOut();
    window.location.reload();
  }
}

The @Component metadata marker or decorator defines three elements:

  1. selector – the HTML selector used to bind the component to the HTML template file
  2. templateUrl – the HTML template file associated with the component
  3. styleUrls – one or more CSS files associated with the component

We have used the app.component.html and app.component.css files to define the HTML template and the CSS styles of the root component.

Finally, the selector element binds the whole component to the <app-root> selector included in the index.html file.

This component implements the interface OnInit which is a lifecycle hook that is called after Angular has initialized all data-bound properties of a directive. It defines a ngOnInit() method to handle any additional initialization tasks.

The constructor initializes the field tokenStorageService with an instance of TokenStorageService, which is pretty similar to what we do in Java.

The ngOnInit() method fetches the JWT token from the Browser Session Storage using the TokenStorageService. If the token is available, then it fetches the user from Browser Session Storage and sets the username and user roles.

It also sets showAdminBoard and showModeratorBoard flags. They will control how the template navbar displays its items.

The AppComponent template has a Logout button link that will call the logout() method which clears the session storage and reloads the page.

app.component.html

app.component.html file allows us to define the root AppComponent‘s HTML template. we’ll use it for creating the navigation bar.

<div id="app">
	<nav class="navbar navbar-expand navbar-dark bg-dark">
		<a class="navbar-brand p-0" href="/">
			<img src="/assets/img/logo.png" width="200" height="50" alt="JavaChinna">
		</a>
		<ul class="navbar-nav mr-auto" routerLinkActive="active">
			<li class="nav-item"><a href="/home" class="nav-link" routerLink="home">Home </a></li>
			<li class="nav-item" *ngIf="showAdminBoard"><a href="/admin" class="nav-link" routerLink="admin">Admin Board</a></li>
			<li class="nav-item" *ngIf="showModeratorBoard"><a href="/mod" class="nav-link" routerLink="mod">Moderator Board</a></li>
			<li class="nav-item"><a href="/user" class="nav-link" *ngIf="isLoggedIn" routerLink="user">User</a></li>
		</ul>
		<ul class="navbar-nav ml-auto" *ngIf="!isLoggedIn">
			<li class="nav-item"><a href="/register" class="nav-link" routerLink="register">Sign Up</a></li>
			<li class="nav-item"><a href="/login" class="nav-link" routerLink="login">Login</a></li>
		</ul>
		<ul class="navbar-nav ml-auto" *ngIf="isLoggedIn">
			<li class="nav-item"><a href="/profile" class="nav-link" routerLink="profile">{{ username }}</a></li>
			<li class="nav-item"><a href class="nav-link" (click)="logout()">LogOut</a></li>
		</ul>
	</nav>
	<div class="container-fluid bg-light">
		<router-outlet></router-outlet>
	</div>
</div>

The bulk of the file is standard HTML, with a few caveats worth noting.

The first one is the {{ username }} expression. The double curly braces {{ variable-name }} is the placeholder that Angular uses for performing variable interpolation.

The second thing to note is the routerLink attribute. Angular uses this attribute for routing requests through its routing module (more on this later). it’s sufficient to know that the module will dispatch a request to a specific component based on the path.

The RouterLinkActive directive tracks whether the linked route of an element is currently active, and allows you to specify one or more CSS classes to add to the element when the linked route is active.

The HTML template associated with the matching component will be rendered within the <router-outlet></router-outlet> placeholder.

Login Component

This component binds form data (email, password) from template to AuthService.login() method that returns an Observable object. If login is successful, it stores the token and calls the login() method.

ngOnInit() looks for the “token” and “error” query parameters in the request:

  • If a token is present in the Browser Session Storage then it sets the isLoggedIn flag to true and currentUser from the Storage.
  • Else If a token is present in the request, then it is a Social login request since the backend will redirect to the client login page along with the token after successful authentication.
    • Saves the token
    • Fetches the current user through UserService and calls the login() method
  • Else if an error parameter is present in the request then it sets the isLoginFailed flag and errorMessage.

login() method does the following:

  • Saves the user in Session Storage.
  • Sets the isLoggedIn flag to true
  • Sets the currentUser from the Storage.
  • Reloads the page

login.component.ts

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../_services/auth.service';
import { UserService } from '../_services/user.service';
import { TokenStorageService } from '../_services/token-storage.service';
import { ActivatedRoute } from '@angular/router';
import { AppConstants } from '../common/app.constants';


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

  form: any = {};
  isLoggedIn = false;
  isLoginFailed = false;
  errorMessage = '';
  currentUser: any;
  googleURL = AppConstants.GOOGLE_AUTH_URL;
  facebookURL = AppConstants.FACEBOOK_AUTH_URL;
  githubURL = AppConstants.GITHUB_AUTH_URL;
  linkedinURL = AppConstants.LINKEDIN_AUTH_URL;

  constructor(private authService: AuthService, private tokenStorage: TokenStorageService, private route: ActivatedRoute, private userService: UserService) {}

  ngOnInit(): void {
	const token: string = this.route.snapshot.queryParamMap.get('token');
	const error: string = this.route.snapshot.queryParamMap.get('error');
  	if (this.tokenStorage.getToken()) {
      this.isLoggedIn = true;
      this.currentUser = this.tokenStorage.getUser();
    }
  	else if(token){
  		this.tokenStorage.saveToken(token);
  		this.userService.getCurrentUser().subscribe(
  		      data => {
  		        this.login(data);
  		      },
  		      err => {
  		        this.errorMessage = err.error.message;
  		        this.isLoginFailed = true;
  		      }
  		  );
  	}
  	else if(error){
  		this.errorMessage = error;
	    this.isLoginFailed = true;
  	}
  }

  onSubmit(): void {
    this.authService.login(this.form).subscribe(
      data => {
        this.tokenStorage.saveToken(data.accessToken);
        this.login(data.user);
      },
      err => {
        this.errorMessage = err.error.message;
        this.isLoginFailed = true;
      }
    );
  }

  login(user): void {
	this.tokenStorage.saveUser(user);
	this.isLoginFailed = false;
	this.isLoggedIn = true;
	this.currentUser = this.tokenStorage.getUser();
    window.location.reload();
  }

}

login.component.html

<div class="col-md-12">
	<div class="card card-container">
		<img id="profile-img" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" class="profile-img-card" />
		<form *ngIf="!isLoggedIn" name="form" (ngSubmit)="f.form.valid && onSubmit()" #f="ngForm" novalidate>
			<div class="form-group">
				<label for="username">Email</label> <input type="text" class="form-control" name="username" [(ngModel)]="form.username" required #username="ngModel" />
				<div class="alert alert-danger" role="alert" *ngIf="f.submitted && username.invalid">Username is required!</div>
			</div>
			<div class="form-group">
				<label for="password">Password</label> <input type="password" class="form-control" name="password" [(ngModel)]="form.password" required minlength="6"
					#password="ngModel" />
				<div class="alert alert-danger" role="alert" *ngIf="f.submitted && password.invalid">
					<div *ngIf="password.errors.required">Password is required</div>
					<div *ngIf="password.errors.minlength">Password must be at least 6 characters</div>
				</div>
			</div>
			<div class="form-group">
				<button class="btn btn-primary btn-block">Login</button>
			</div>
			<div class="form-group">
				<div class="alert alert-danger" role="alert" *ngIf="isLoginFailed">Login failed: {{ errorMessage }}</div>
			</div>
			<div class="form-group">
				<p class="content-divider center mt-3">
					<span>or</span>
				</p>
				<p class="social-login text-center">
					Sign in with:
					<a href="{{ googleURL }}" class="ml-2">
						<img alt="Login with Google" src="/assets/img/google.png" class="btn-img">
					</a>
					<a href="{{ facebookURL }}">
						<img alt="Login with Facebook" src="/assets/img/facebook.png" class="btn-img">
					</a>
					<a href="{{ githubURL }}">
						<img alt="Login with Github" src="/assets/img/github.png" class="btn-img">
					</a>
					<a href="{{ linkedinURL }}">
						<img alt="Login with Linkedin" src="/assets/img/linkedin.png" class="btn-img-linkedin">
					</a>
				</p>
			</div>
		</form>
		<div class="alert alert-success" *ngIf="isLoggedIn">Welcome {{currentUser.displayName}} <br>Logged in as {{ currentUser.roles }}.</div>
	</div>
</div>

The ngSubmit directive calls the onSubmit() method when the form is submitted.

Next, we have defined the template variable #f which is a reference to the NgForm directive instance that governs the form as a whole. It gives you access to the aggregate value and validity status of the form, as well as user interaction properties like dirty and touched.

The NgForm directive supplements the form element with additional features. It holds the controls we have created for the elements with an ngModel directive and name attribute

The ngModel directive gives us two-way data binding functionality between the form controls and the client-side domain model.

This means that data entered in the form input fields will flow to the model – and the other way around. Changes in both elements will be reflected immediately via DOM manipulation.

Here are what we validate in the form:

  • email: required
  • password: required, minLength=6

Register Component

This component binds form data (display nameemailpassword, confirm password) from template to AuthService.register() method that returns an Observable object.

register.component.ts

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

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

  form: any = {};
  isSuccessful = false;
  isSignUpFailed = false;
  errorMessage = '';

  constructor(private authService: AuthService) { }

  ngOnInit(): void {
  }

  onSubmit(): void {
    this.authService.register(this.form).subscribe(
      data => {
        console.log(data);
        this.isSuccessful = true;
        this.isSignUpFailed = false;
      },
      err => {
        this.errorMessage = err.error.message;
        this.isSignUpFailed = true;
      }
    );
  }
}

register.component.html

<div class="col-md-12">
	<div class="card card-container">
		<img id="profile-img" src="//ssl.gstatic.com/accounts/ui/avatar_2x.png" class="profile-img-card" />
		<form *ngIf="!isSuccessful" name="form" (ngSubmit)="f.form.valid && onSubmit()" #f="ngForm" novalidate>
			<div class="form-group">
				<label for="displayName">Display Name</label> <input type="text" class="form-control" name="displayName" [(ngModel)]="form.displayName" required minlength="3"
					maxlength="20" #displayName="ngModel" />
				<div class="alert-danger" *ngIf="f.submitted && displayName.invalid">
					<div *ngIf="displayName.errors.required">Display Name is required</div>
					<div *ngIf="displayName.errors.minlength">Display Name must be at least 3 characters</div>
					<div *ngIf="displayName.errors.maxlength">Display Name must be at most 20 characters</div>
				</div>
			</div>
			<div class="form-group">
				<label for="email">Email</label> <input type="email" class="form-control" name="email" [(ngModel)]="form.email" required email #email="ngModel" />
				<div class="alert-danger" *ngIf="f.submitted && email.invalid">
					<div *ngIf="email.errors.required">Email is required</div>
					<div *ngIf="email.errors.email">Email must be a valid email address</div>
				</div>
			</div>
			<div class="form-group">
				<label for="password">Password</label> <input type="password" class="form-control" name="password" [(ngModel)]="form.password" required minlength="6"
					#password="ngModel" />
				<div class="alert-danger" *ngIf="f.submitted && password.invalid">
					<div *ngIf="password.errors.required">Password is required</div>
					<div *ngIf="password.errors.minlength">Password must be at least 6 characters</div>
				</div>
			</div>
			<div class="form-group">
				<label for="matchingPassword">Confirm Password</label> <input type="password" class="form-control" name="matchingPassword" [(ngModel)]="form.matchingPassword"
					required minlength="6" #matchingPassword="ngModel" />
				<div class="alert-danger" *ngIf="f.submitted && matchingPassword.invalid">
					<div *ngIf="matchingPassword.errors.required">Confirm Password is required</div>
					<div *ngIf="matchingPassword.errors.minlength">Confirm Password must be at least 6 characters</div>
				</div>
			</div>
			<div class="form-group">
				<button class="btn btn-primary btn-block">Sign Up</button>
			</div>
			<div class="alert alert-warning" *ngIf="f.submitted && isSignUpFailed">
				Signup failed!<br />{{ errorMessage }}
			</div>
		</form>
		<div class="alert alert-success" *ngIf="isSuccessful">Your registration is successful!</div>
	</div>
</div>

Here are what we validate in the form:

  • displayName: required, minLength=3, maxLength=20
  • email: required, email format
  • password: required, minLength=6
  • confimrPassword: required, minLength=6

Password and confirm password should match. This validation is done by the backend.

Home Component

HomeComponent will use UserService to get public resources from the back-end.

home.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';

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

  content: string;

  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.userService.getPublicContent().subscribe(
      data => {
        this.content = data;
      },
      err => {
        this.content = JSON.parse(err.error).message;
      }
    );
  }
}

home.component.html

<div>
  <h6 class="text-muted py-5">{{ content }}</h6>
</div>

Profile Component

This Component gets the current User from Browser Session Storage using TokenStorageService and shows user name, email, and roles.

profile.component.ts

import { Component, OnInit } from '@angular/core';
import { TokenStorageService } from '../_services/token-storage.service';

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

  currentUser: any;

  constructor(private token: TokenStorageService) { }

  ngOnInit(): void {
    this.currentUser = this.token.getUser();
  }
}

profile.component.html

<div class="container" *ngIf="currentUser; else loggedOut">
	<div>
		<h6 class="text-muted py-5">
			<strong>{{ currentUser.displayName }}</strong> Profile
		</h6>
	</div>
	<p>
		<strong>Email:</strong> {{ currentUser.email }}
	</p>
	<strong>Roles:</strong>
	<ul>
		<li *ngFor="let role of currentUser.roles">{{ role }}</li>
	</ul>
</div>
<ng-template #loggedOut> Please login. </ng-template>

Notice the use of the *ngFor directive. This directive is called a repeater, and we can use it for iterating over the contents of a variable and iteratively rendering HTML elements. In this case, we used it for dynamically rendering the user roles.

The *ngIf is a structural directive that conditionally includes a template based on the value of an expression coerced to Boolean. When the expression evaluates to true, Angular renders the template provided in a then clause, and when false or null, Angular renders the template provided in an optional else clause. The default template for the else clause is blank.

Role Specific Components

The following components are protected based on the user role. But authorization will be done by the back-end application.

  • board-user.component
  • board-admin.component
  • board-moderator.component

We only need to call the following UserService methods:

  • getUserBoard()
  • getModeratorBoard()
  • getAdminBoard()

Here is an example of BoardAdminComponent. BoardModeratorComponent & BoardUserComponent are similar.

board-admin.component.ts

import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';

@Component({
  selector: 'app-board-admin',
  templateUrl: './board-admin.component.html',
  styleUrls: ['./board-admin.component.css']
})
export class BoardAdminComponent implements OnInit {

  content: string;

  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.userService.getAdminBoard().subscribe(
      data => {
        this.content = data;
      },
      err => {
        this.content = JSON.parse(err.error).message;
      }
    );
  }
}

board-admin.component.html

<div>
  <h6 class="text-muted py-5">{{ content }}</h6>
</div>

Creating Services

We can open a console terminal, then create a service directory and issue the following Angular CLI command in the terminal to create a service class.

ng generate service <name>

Token Storage Service

TokenStorageService is responsible for storing and retrieving the token and user information (name, email, roles) from Browser’s Session Storage. For Logout, we only need to clear this Session Storage.

token-storage.service.ts

import { Injectable } from '@angular/core';

const TOKEN_KEY = 'auth-token';
const USER_KEY = 'auth-user';

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

  constructor() { }

  signOut(): void {
    window.sessionStorage.clear();
  }

  public saveToken(token: string): void {
    window.sessionStorage.removeItem(TOKEN_KEY);
    window.sessionStorage.setItem(TOKEN_KEY, token);
  }

  public getToken(): string {
    return sessionStorage.getItem(TOKEN_KEY);
  }

  public saveUser(user): void {
    window.sessionStorage.removeItem(USER_KEY);
    window.sessionStorage.setItem(USER_KEY, JSON.stringify(user));
  }

  public getUser(): any {
    return JSON.parse(sessionStorage.getItem(USER_KEY));
  }
}

@Injectable() metadata marker signals that the service should be created and injected via Angular’s dependency injectors.

Authentication Service

AuthService performs POST HTTP request to the http://localhost:8080/api/auth/signin and http://localhost:8080/api/auth/signup endpoints via Angular’s HttpClient for Login and Sign Up requests respectively. The login method returns an Observable instance that holds the user object.

auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AppConstants } from '../common/app.constants';

const httpOptions = {
  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(private http: HttpClient) { }

  login(credentials): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'signin', {
      email: credentials.username,
      password: credentials.password
    }, httpOptions);
  }

  register(user): Observable<any> {
    return this.http.post(AppConstants.AUTH_API + 'signup', {
      displayName: user.displayName,
      email: user.email,
      password: user.password,
      matchingPassword: user.matchingPassword,
      socialProvider: 'LOCAL'
    }, httpOptions);
  }
}

User Service

UserService performs GET HTTP requests to various endpoints via Angular’s HttpClient for fetching user details, public and role-specific protected content. All the methods return an Observable instance that holds the content.

user.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { AppConstants } from '../common/app.constants';

const httpOptions = {
		  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
		};


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

  constructor(private http: HttpClient) { }

  getPublicContent(): Observable<any> {
    return this.http.get(AppConstants.API_URL + 'all', { responseType: 'text' });
  }

  getUserBoard(): Observable<any> {
    return this.http.get(AppConstants.API_URL + 'user', { responseType: 'text' });
  }

  getModeratorBoard(): Observable<any> {
    return this.http.get(AppConstants.API_URL + 'mod', { responseType: 'text' });
  }

  getAdminBoard(): Observable<any> {
    return this.http.get(AppConstants.API_URL + 'admin', { responseType: 'text' });
  }

  getCurrentUser(): Observable<any> {
    return this.http.get(AppConstants.API_URL + 'user/me', httpOptions);
  }
}

Creating HTTP Interceptor

HttpInterceptor has intercept() method to inspect and transform HTTP requests before they are sent to the server.

AuthInterceptor implements HttpInterceptor. It adds the Authorization header with Bearer prefix to the token.

auth.interceptor.ts

import { HTTP_INTERCEPTORS, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { TokenStorageService } from '../_services/token-storage.service';
import {tap} from 'rxjs/operators';
import { Observable } from 'rxjs';

const TOKEN_HEADER_KEY = 'Authorization';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
	constructor(private token: TokenStorageService, private router: Router) {

	}

	intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
		let authReq = req;
		const loginPath = '/login';
		const token = this.token.getToken();
		if (token != null) {
			authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
		}
		return next.handle(authReq).pipe( tap(() => {},
		(err: any) => {
			if (err instanceof HttpErrorResponse) {
				if (err.status !== 401 || window.location.pathname === loginPath) {
					return;
				}
				this.token.signOut();
				window.location.href = loginPath;
			}
		}
		));
	}
}

export const authInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];

This AuthInterceptor is also responsible for handling the HTTP 401 Unauthorized response from Spring REST API. This could happen when the supplied token in the Authorization header in the request is expired. In that case, if the current path is not /login, then it will clear the Browser Session Storage and redirect to the login page.

Creating Constants

This class contains all the URL’s required for the application.

app.constants.ts

export class AppConstants {
    private static API_BASE_URL = "http://localhost:8080/";
    private static OAUTH2_URL = AppConstants.API_BASE_URL + "oauth2/authorization/";
    private static REDIRECT_URL = "?redirect_uri=http://localhost:8081/login";
    public static API_URL = AppConstants.API_BASE_URL + "api/";
    public static AUTH_API = AppConstants.API_URL + "auth/";
    public static GOOGLE_AUTH_URL = AppConstants.OAUTH2_URL + "google" + AppConstants.REDIRECT_URL;
    public static FACEBOOK_AUTH_URL = AppConstants.OAUTH2_URL + "facebook" + AppConstants.REDIRECT_URL;
    public static GITHUB_AUTH_URL = AppConstants.OAUTH2_URL + "github" + AppConstants.REDIRECT_URL;
    public static LINKEDIN_AUTH_URL = AppConstants.OAUTH2_URL + "linkedin" + AppConstants.REDIRECT_URL;
}

Defining Modules

We need to edit the app.module.ts file to import all the required modules, components, and services.

Additionally, we need to specify which provider we’ll use for creating and injecting the AuthInterceptor class. Otherwise, Angular won’t be able to inject it into the component classes. So we have imported authInterceptorProviders from AuthInterceptor class and specified as a provider.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';

import { authInterceptorProviders } from './_helpers/auth.interceptor';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    RegisterComponent,
    HomeComponent,
    ProfileComponent,
    BoardAdminComponent,
    BoardModeratorComponent,
    BoardUserComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [authInterceptorProviders],
  bootstrap: [AppComponent]
})
export class AppModule { }

Defining Module Routings

Although the components are functional in isolation, we still need to use a mechanism for calling them when the user clicks the buttons in the navigation bar.

This is where the RouterModule comes into play. So, let’s open the app-routing.module.ts file, and configure the module, so it can dispatch requests to the matching components.

app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'login', component: LoginComponent },
  { path: 'register', component: RegisterComponent },
  { path: 'profile', component: ProfileComponent },
  { path: 'user', component: BoardUserComponent },
  { path: 'mod', component: BoardModeratorComponent },
  { path: 'admin', component: BoardAdminComponent },
  { path: '', redirectTo: 'home', pathMatch: 'full' }
];

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

As we can see above, the Routes array instructs the router which component to display when a user clicks a link or specifies a URL into the browser address bar.

A route is composed of two parts:

  1. Path –  a string that matches the URL in the browser address bar
  2. Component – the component to create when the route is active (navigated)

If the user clicks the Login button, which links to the /login path, or enters the URL in the browser address bar, the router will render the LoginComponent component’s template file in the <router-outlet> placeholder.

Likewise, if they click the SignUp button, it will render the RegisterComponent component.

Run the Angular App

You can run this App with the below command and hit the URL http://localhost:8081/ in browser

ng serve --port 8081

References

https://bezkoder.com/angular-10-jwt-auth/

https://www.callicoder.com/spring-boot-security-oauth2-social-login-part-1/

https://www.baeldung.com/spring-boot-angular-web

https://angular.io/

Source Code

https://github.com/JavaChinna/spring-boot-angular-oauth2-social-login-demo

Conclusion

That’s all folks. In this article, we have developed an Angular Client application to consume the Spring Boot REST API.

Thank you for reading.

Read Next: Upgrade Spring Boot Angular OAuth2 Social Login Application to the Latest Version

This Post Has 22 Comments

  1. MOHD

    Excellent Sir. How can I contact you.

  2. Abi

    Hi, I like create a sign up with azure active directory… apply this?

      1. Abi

        Thank you very much, I already managed to do it, but I need to get the groups from Azure directory … How can I see the full answer from azure? Not just user data …

        1. Chinna

          I think you can use the Azure Graph API to get the user groups. You can refer this for getting the token and refer this to call the group-list API using the token. I’m not sure if this is what you are looking for. I hope it helps.

  3. Amey

    Nice blog Chinna, how can you pack this together in a single deployable .war?

    1. Chinna

      Hi Amey, Currently I’m working on packaging Angular and Spring Boot into a single war / jar. Will give you the steps for that in a blog post shortly.

  4. Jason

    Hello Chinna,
    I keep getting a 404 error after selecting my account for Google. I’m not sure what could be causing this error as I have followed the guide exactly.

    1. Chinna

      Hi Jason,

      This client redirect_uri is not correct. As per this guide, angular will be running on port number 8081. So the client redirect_uri should be “http://localhost:8081/login”. Also, your URL contains # at the end. So It looks like you are following this guide where both Angular & Spring will be packaged together in a single JAR / WAR and Hash (#) will be used in the angular paths.

      Let me know exactly which guide you followed. Based on that I can help you.

      Cheers,
      Chinna

    1. Chinna

      Please add the following redirect URI in the authorized redirect URI field in your google developer console. For more clarification, refer here

      http://localhost:8080/login/oauth2/code/google

  5. hrishikesh

    Login failed: Name not found from OAuth2 provider facing this error for git hub login

    1. Chinna

      This means Github is not returning the name in the userInfo endpoint response. Check your GitHub user profile/settings. Or try with some other Github account to see if you are facing the same issue.

  6. Issamdrmas

    Hello Chinna, Greate job but i got this error:
    Firefox can’t establish a connection to the server at localhost:8081.

    1. Chinna

      This means your angular app is not running at port 8081. Did you run the client app with ng serve --port 8081 ?

  7. Nathan

    where’s the source code for angular? i just see the source code for spring boot

    1. Chinna

      Both Angular and Spring Boot source code are available here. Angular code is in the angular-11-social-login folder

  8. Petri Airio

    What versio of NG or/and node / npm is needed to run the angular part ? Didn’t notice where was instructions how to setup those?

    1. Chinna

      I’m using node v12.18.4. I have updated this article to include the installation instructions for node/npm/TypeScript/Angular CLI

Leave a Reply