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
Register
User Home
Admin Home
Admin Profile
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
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:
selector
– the HTML selector used to bind the component to the HTML template filetemplateUrl
– the HTML template file associated with the componentstyleUrls
– 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 andcurrentUser
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 thelogin()
method
- Else if an error parameter is present in the request then it sets the
isLoginFailed
flag anderrorMessage
.
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
: requiredpassword
: required, minLength=6
Register Component
This component binds form data (display name
, email
, password
, 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=20email
: required, email formatpassword
: required, minLength=6confimrPassword
: 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 theREDIRECT_URL
constant. When we do a social login from the client, thisredirect_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 thisredirect_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:
- Path – a string that matches the URL in the browser address bar
- 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 theapp.constants.ts
file and theapp.oauth2.authorizedRedirectUris
property in Spring Bootapplication.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
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
Excellent Sir. How can I contact you.
You can find my contact details here https://www.javachinna.com/about/
Hi, I like create a sign up with azure active directory… apply this?
Hi, You can refer this https://docs.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-active-directory
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 …
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.
Nice blog Chinna, how can you pack this together in a single deployable .war?
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.
Here you go https://www.javachinna.com/integrate-angular-with-spring-boot/. Please note that I’ve implemented 2FA authentication on top of this social login tutorial series. Hence, the integrated source code will include 2FA implementation as well.
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.
More specifically I get this: http://localhost:8080/?error=%5Bauthorization_request_not_found%5D#
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
Error 400: redirect_uri_mismatch
The redirect URI in the request, http://localhost:8080/login/oauth2/code/google, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs, visit: https://console.developers.google.com/apis/credentials/oauthclient/${your_client_id}?project=${your_project_number}
facing this error.
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
Login failed: Name not found from OAuth2 provider facing this error for git hub login
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.
Hello Chinna, Greate job but i got this error:
Firefox can’t establish a connection to the server at localhost:8081.
This means your angular app is not running at port 8081. Did you run the client app with
ng serve --port 8081
?where’s the source code for angular? i just see the source code for spring boot
Both Angular and Spring Boot source code are available here. Angular code is in the
angular-11-social-login
folderWhat versio of NG or/and node / npm is needed to run the angular part ? Didn’t notice where was instructions how to setup those?
I’m using node v12.18.4. I have updated this article to include the installation instructions for node/npm/TypeScript/Angular CLI
Can u help in writing the test case for authController in junit 5
Sure. I can help you with that.
hey can u write the test cases for authController in junit 5
Here you go https://www.javachinna.com/spring-boot-rest-controller-junit-tests-mockito/
why it is running only on 8081?if i want to run on different port number then what to do?
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.
Hi, how to enable CORS at backend.
I have already enabled CORS by Overriding
addCorsMappings
method in Web MVC Configuration and adding CORS filter in Web Security Configuration.thanks for this great job that you made
Login failed: Name not found from OAuth2 provider ,i faced this error when google login
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 ?
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
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 theapp.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.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.
Yes. I’m planning to implement the same at API gateway level. Thanks
Hi, Chinna, this series really helped me. I was wondering how can you create a github repo with 2 folder – frontend and backend?
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.
Logout is not working
We will connect and see why it is not working for you.
Hi, Chinna, How to implement logout functionality here.
Hi Rajesh,
Logout is already implemented. is it not working for you?
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.
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
This means Github is not returning the name in the userInfo endpoint response. does your GitHub public profile has a name?
Login failed: base64-encoded secret key cannot be null or empty.
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();
}