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 npm@latest -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;
}

Note: We have specified the angular client URL as redirect_uri in the REDIRECT_URL constant. When we do a social login from the client, this redirect_uri will be passed in the login request to the Spring Boot REST application which will then store it in a cookie. Once the social login is successful, the backend application will then use this redirect_uri to redirect the user back to the angular client application.

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

Note: If you wanna run the client application on a different port, then make sure to update the redirect_uri in the app.constants.ts file and the app.oauth2.authorizedRedirectUris property in Spring Boot application.properties file with your port number.

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 47 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

  9. Abhi

    Can u help in writing the test case for authController in junit 5

    1. Chinna

      Sure. I can help you with that.

  10. abhu

    hey can u write the test cases for authController in junit 5

  11. Rey

    why it is running only on 8081?if i want to run on different port number then what to do?

    1. Chinna

      Please read here on how to run on different port number and here to know why it is required. Also have a look at the Social Login Flow in the Flow Diagram here.

  12. venkat

    Hi, how to enable CORS at backend.

  13. ons

    thanks for this great job that you made
    Login failed: Name not found from OAuth2 provider ,i faced this error when google login

    1. Chinna

      This means the Google UserInfo endpoint is not returning the account name. But I’m not sure why it is not returning that. Can you check the attributes returned by Google by debugging OAuth2UserInfoFactory class ?

  14. Carlos

    Hi Chinna,

    The first of all, thanks for your work, that´s very useful.

    I am facing a problem, I can select my gmail address on the google authenticator but once I do click on it, I have the following text in my screen:

    Whitelabel Error Page
    This application has no explicit mapping for /error, so you are seeing this as a fallback.

    Wed Dec 22 11:25:38 CET 2021
    There was an unexpected error (type=Not Found, status=404).

    And in the browser URL input I have:http://localhost:8081/#

    Angular is running in localhost:4200 and the back-end in 8081.

    This is the URL Constants file:

    private static API_BASE_URL = “http://localhost:8081/”;

    I can´t find the fails reason.

    Thanks in advance,
    Carlos

    1. Chinna

      Hi Carlos,

      In Spring Boot application.properties, the client redirect URL is configured with port 8081. Did you change this to 4200 since you are running your client on that port? If I have understood correctly, you are trying to login via Google Social login. For this, the client URL should match with the one configured in the app.oauth2.authorizedRedirectUris property in the backed. Else, the authentication will fail and it will be redirected to “http://localhost:8081/error page. In your case, you don’t have any explicit mapping for the error page. So it ends up with 404 Page Not Found.

  15. Sai

    Great job! Are you planning to implement the same at api gateway level (probably zuul or spring cloud gateway)? It will be very helpful for microservices architecture.

    1. Chinna

      Yes. I’m planning to implement the same at API gateway level. Thanks

  16. Trinh Quy Cong

    Hi, Chinna, this series really helped me. I was wondering how can you create a github repo with 2 folder – frontend and backend?

    1. Chinna

      Hi, You can create a repo in github and then clone it. This will create a folder in your machine. Now, you can create 2 different folders inside this and push it to the remote repo.

  17. Vikasj

    Logout is not working

    1. Chinna

      We will connect and see why it is not working for you.

  18. Rajesh

    Hi, Chinna, How to implement logout functionality here.

    1. Chinna

      Hi Rajesh,

      Logout is already implemented. is it not working for you?

  19. Hamza

    Hi chinna,
    i want to say that what you doing is so great,aslo i want to ask you about some issue im facing there when i try to signup with my google account it redirects me to (http://localhost:8081/login?error=%5Binvalid_grant%5D%20Bad%20Request)localhost refused to connect. for more infos im using port.8080 on the back and 4200 on the front.

  20. hamza

    hi,Chinna
    im facing “Login failed: Name not found from OAuth2 provider”,when trying to signup with the github account also for the configuration of the app on github i have puted the homepage URL as :http://localhost:8080/
    and for the Callback URL:http://localhost:8080/login/oauth2/code/github
    for the backend im using port 8080 and frontend port number 8081.thankyou
    ps:you are doing a great work

    1. Chinna

      This means Github is not returning the name in the userInfo endpoint response. does your GitHub public profile has a name?

  21. Ayaan

    Login failed: base64-encoded secret key cannot be null or empty.

  22. Priya

    Hi,

    I am facing below error while logging Google account. Please find the below details. It would be helpful if you would help me on it to resolve this issue.

    org.springframework.security.oauth2.core.OAuth2AuthenticationException: [missing_user_info_uri] Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: google

    Below is the property i defined in application prop file.

    spring.security.oauth2.client.provider.google.user-info-uri = https://www.googleapis.com/oauth2/v3/userinfo

    and below is the code for filterchain

    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

    http
    .cors()
    .and()
    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    .and()
    .csrf().disable()
    .formLogin().disable()
    .httpBasic().disable()
    .exceptionHandling()
    .authenticationEntryPoint(new RestAuthenticationEntryPoint())
    .and()
    .authorizeRequests()
    .antMatchers(“/”, “/error”, “/api/all”, “/api/auth/**”, “/oauth2/**”).permitAll()
    .anyRequest()
    .authenticated()
    .and()
    .oauth2Login()
    .authorizationEndpoint()
    .authorizationRequestRepository(cookieAuthorizationRequestRepository())
    .and()
    .redirectionEndpoint()
    .and()
    .userInfoEndpoint()
    .oidcUserService(customOidcUserService)
    .userService(customOAuth2UserService)
    .and()
    .successHandler(oAuth2AuthenticationSuccessHandler)
    .failureHandler(oAuth2AuthenticationFailureHandler);

    http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    return http.build();
    }

Leave a Reply