In this article, we are going to implement JWT(JSON Web Token) authentication in the Angular(v14) application. We also understand how to use the Refresh Token when the JWT Access Token expires.
Now let's add the bootstrap menu in 'app.component.html'.
JSON Web Token(JWT):
JSON Web Token is a digitally signed and secured token for user validation. The JWT is constructed with 3 informative parts:
- Header
- Payload
- Signature
Create An Angular(v14) Application:
Let's create an Angular(v14) application to accomplish our demo.
Command To Create Angular App
ng new name_of_your_project
ng new name_of_your_project
Let's install the bootstrap package
npm install bootstrap@5.2.0
Configure the bootstrap CSS and JS file reference in 'angular.json'.
src/app/app.component.html:
<nav class="navbar navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" routerLink="/">Jwt Auth Demo</a> </div> </nav>
- Here is the added home page route for the title of our application.
Create 'Home' Component:
Let's create an angular component like 'Home'. It will be like a home page that can be accessed by any kind of user, authentication is not required for this page.
ng generate component home --skip-tests
Now configure the 'Home' component route at 'AppRoutingModule'
src/app/app-routing.module.ts:
import { HomeComponent } from './home/home.component'; // existing code hidden for display purpose const routes: Routes = [ { path: '', component: HomeComponent, }, ];Let's add the below HTML in 'home.component.html' to display a simple welcome message.
src/app/home/home.component.html:
<div class="container"> <div class="row mt-5"> <div class="col d-flex justify-content-center"> <div class="card"> <div class="card-body"> <h1 class="card-title d-flex justify-content-center">Welcome!</h1> <h4 class="card-title">Angular 14 JWT authentication.</h4> </div> </div> </div> </div> </div>
API For Authentication:
Now we need an authentication API for generating the jwt token or refreshing token. To achieve the JWT authentication, I have created a sample application using NestJS(nodejs). So what to install, how to set up, how to run the API, test credentials everything in detail explained in the below GitHub link so please check them after cloning the application.
Create A 'Auth' Feature Module And 'Login' Component:
Let's create a feature module like the 'Auth' module.
ng generate module auth --routing
Let's do a lazy loading module route configuration at 'app-routing.module.ts'.
src/app/app-routing.module.ts:
// existing code hidden for display purpose const routes: Routes = [ { path: 'auth', loadChildren: () => import('./auth/auth.module').then((_) => _.AuthModule), }, ];Let's create the 'Login' component inside of the 'Auth' module.
ng generate component auth/login --skip-tests
Let's add the 'Login' component route inside of the 'auth-routing.module.ts'.
src/app/auth/auth-routing.module.ts:
import { LoginComponent } from './login/login.component'; const routes: Routes = [{ path:'login', component: LoginComponent }];
Install @auth0/angular-jwt Package:
The benefits of using the '@auth0/angular-jwt' package are:
- Easy to decrypt the JWT token to read the payload inside of the token
- Easy to determine token expiration
- Automatically adds the 'authorization' header and token as value to API request
- Easy to maintain the authentication state.
npm install @auth0/angular-jwt
Implement Login API Call:
Let's first create the login API payload model like 'LoginModel'.
ng generate interface auth/login-model
src/app/auth/login-model.ts:
export interface LoginModel { username: string; password: string; }
- Here 'username', 'password', properties please make sure to copy those property names from the GitHub Link.
ng generate interface shared/auth/token-model
src/app/shared/auth/token-model.ts:
export interface TokenModel { access_token: string; refresh_token: string; }
The JWT token contains some user claims like 'email', 'username', 'exp', etc. So we can use this information to display on our angular application. So we will decode the jwt token and we will create the model for this data like 'UserProfile'.
ng generate interface shared/auth/user-profile
src/app/shared/auth/user-profile.ts:export interface UserProfile { username: string; sub: number; email: string; iat: number; exp: number; }Create a service like 'AuthService' which contains API call logic. The 'AuthService' add under the 'shared/auth' folder.
ng generate service shared/auth/auth --skip-tests
src/app/shared/auth/auth.service.ts:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { JwtHelperService } from '@auth0/angular-jwt'; import { BehaviorSubject, catchError, map, of } from 'rxjs'; import { LoginModel } from 'src/app/auth/login-model'; import { TokenModel } from './token-model'; import { UserProfile } from './user-profile'; @Injectable({ providedIn: 'root', }) export class AuthService { constructor(private httpClient: HttpClient) {} userProfile = new BehaviorSubject<UserProfile | null>(null); jwtService: JwtHelperService = new JwtHelperService(); userLogin(payload: LoginModel) { return this.httpClient .post('http://localhost:3000/auth/login', payload) .pipe( map((data) => { var token = data as TokenModel; localStorage.setItem('tokens', JSON.stringify(token)); var userInfo = this.jwtService.decodeToken( token.access_token ) as UserProfile; this.userProfile.next(userInfo); return true; }), catchError((error) => { console.log(error); return of(false); }) ); } }
- (Line: 12) Inject the 'HttpClient' loads from the '@angular/common/http'.
- (Line: 13) Declared a 'userProfile' variable of type BehaviorSubject<UserProfile>.
- (Line: 14) Declare a 'jwtService' variable of type 'JwtHelperService' that loads from the '@auth0/angular-jwt'.
- (Line: 16-39) The 'userLogin()' method invokes login API.
- (Line: 19) Added the login API and user credentials as payload.
- (Line: 22) API response type cast to 'TokenModel'.
- (Line: 24) Convert the 'TokenModel' to string and then save to browser local storage.
- (Line: 26-28) Using 'JwtHelperService' decode the jwt access token and then type cast as 'UserProfile'.
- (Line: 30) Update the 'userProfile' observable variable.
src/app/app.module.ts:
import { HttpClientModule } from '@angular/common/http'; // code hidden for display purpose @NgModule({ imports: [ HttpClientModule ] }) export class AppModule { }
Login Form:
Let's update the 'login.component.ts' as below.
src/app/auth/login/login.component.ts:
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from 'src/app/shared/auth/auth.service'; import { LoginModel } from '../login-model'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'], }) export class LoginComponent implements OnInit { constructor(private authService: AuthService, private router: Router) {} loginForm: LoginModel = { username: '', password: '', }; ngOnInit(): void {} userLogin() { this.authService.userLogin(this.loginForm).subscribe((data) => { if (data) { this.router.navigate(['/']); } }); } }
- (Line: 12) Injected the 'AuthService' and the 'Router' services.
- (Line: 14-17) Declared and initialized the 'loginForm' of type 'LoginModel'.
- (Line: 21-27) The 'userLogin()' method invokes the login API call. On login, success navigates back to the home page.
<div class="container"> <legend>Sign-In</legend> <div class="mb-3"> <label for="txtUserName" class="form-label">User Name</label> <input type="text" class="form-control" id="txtUserName" [(ngModel)]="loginForm.username" /> </div> <div class="mb-3"> <label for="txtPassword" class="form-label">Password</label> <input type="password" class="form-control" id="txtPassword" [(ngModel)]="loginForm.password" /> </div> <button class="btn btn-primary" (click)="userLogin()">Login</button> </div>
- Here simple form with 'username' and 'password' fields and the button to submit the form whose click event registered to 'userLogin()' method.
src/app/auth/auth.module.ts:
import { FormsModule } from '@angular/forms'; // existing code hidden for display purpose @NgModule({ imports: [ FormsModule ] }) export class AuthModule { }Now let's try to read the authenticated information into the 'App' component that is because we need to toggle the login menu item and also we need to bind the user email to the menu if the user logged in successfully.
src/app/app.component.ts:
import { Component, OnInit } from '@angular/core'; import { AuthService } from './shared/auth/auth.service'; import { UserProfile } from './shared/auth/user-profile'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent implements OnInit { constructor(private authService: AuthService) {} title = 'ang14-jwtauth'; userProfile?: UserProfile | null; ngOnInit(): void { this.authService.userProfile.subscribe((data) => { this.userProfile = data; }); } }
- (Line: 13) Declared the 'userProfile' variable.
- (Line: 16-18) Listening for the data from the 'authService.userProfile' behavior subject.
<nav class="navbar navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" routerLink="/">Jwt Auth Demo</a> <div class="d-flex"> <ul class="navbar-nav"> <li class="nav-item" *ngIf="(userProfile?.sub ?? 0) == 0"> <a class="nav-link active" routerLink="/auth/login">Login</a> </li> <li class="nav-item" *ngIf="(userProfile?.sub ?? 0) > 0"> <a class="nav-link active" href="#">{{userProfile?.email}}</a> </li> </ul> </div> </div> </nav>
- Here 'sub' is the user id value. If the sub value is 0 then use not authenticated then we show 'Login' menu item. If the sub value is greater than 0 then the user is authenticated then we hide the 'Login' menu and display user's email as one of the menu items.
(Step 2)
(Step 3)
(Step 4)
Create A 'Movies' Feature Module And 'Fav-Movie' Component:
Till now we have completed the user authentication by fetching the JWT access token. Now let's try to consume a secure endpoint that requires jwt access token authorization header value. So to accomplish this let's create new 'Movies' Feature Module and a 'Fav-Movie' component.
Let's create our feature module
ng generate module movies --routing
Now let's implement the 'Movies' module lazy load route at 'AppRoutingModule'.
src/app/app-routing.module.ts:
// hidden existing code for display purpose const routes: Routes = [ { path: 'movies', loadChildren: () => import('./movies/movies.module').then((_) => _.MoviesModule), }, ]Let's create the 'Fav-Movie' component inside of the 'Movies' module.
ng generate component movies/fav-movie --skip-tests
Now configure 'Fav-Movie' component routing in 'MoviesRoutingModule'.src/app/movies/movies-routing.module.ts:
import { FavMovieComponent } from './fav-movie/fav-movie.component'; // existing code hidden for dispal const routes: Routes = [ { path: 'fav-movies', component: FavMovieComponent, }, ];
Implement Logic To Consume Secure Endpoint:
The secured endpoint and its response can be looked at NestJS Github Link
Let's create an API response model like 'fav-movie.ts'.
ng generate interface movies/fav-movie
src/app/movies/fav-movie.ts:
export interface FavMovie { id: number; name: string; genre: string; }Let's create a service file like 'movies.service.ts'.
ng generate service movies/movies --skip-tests
src/app/movies/movies.service.ts:
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { FavMovie } from './fav-movie'; @Injectable({ providedIn: 'root', }) export class MoviesService { constructor(private httpClient: HttpClient) {} getFavMovies() { return this.httpClient.get<FavMovie[]>('http://localhost:3000/user/fav-movies'); } }
- (Line: 11-13) Secured API call.
src/app/movies/fav-movie/fav-movie.component.ts:
import { Component, OnInit } from '@angular/core'; import { FavMovie } from '../fav-movie'; import { MoviesService } from '../movies.service'; @Component({ selector: 'app-fav-movie', templateUrl: './fav-movie.component.html', styleUrls: ['./fav-movie.component.css'] }) export class FavMovieComponent implements OnInit { constructor(private moviesService:MoviesService) { } faveMovies:FavMovie[] = []; ngOnInit(): void { this.moviesService.getFavMovies() .subscribe((data) => { this.faveMovies = data; }) } }
- (Line: 14) Declared the 'favMovies' variable of an array of types 'FavMovies'.
- (Line: 17-19) Invoking the secured API endpoint.
<div class="row row-cols-1 row-cols-md-2 g-4"> <div class="col" *ngFor="let fav of faveMovies"> <div class="card"> <div class="card-body"> <h5 class="card-title">{{ fav.name }}</h5> <p class="card-text">Genere | {{ fav.genre }}</p> </div> </div> </div> </div>Let's add the new menu item for the movies.
src/app/app.component.html:
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" routerLink="/">Jwt Auth Demo</a> <div class="collapse navbar-collapse"> <ul class="navbar-nav"> <li class="nav-item" *ngIf="(userProfile?.sub ?? 0) > 0"> <a class="nav-link active" routerLink="/movies/fav-movies">Movies</a> </li> </ul> </div> <div class="d-flex"> <ul class="navbar-nav"> <li class="nav-item" *ngIf="(userProfile?.sub ?? 0) == 0"> <a class="nav-link active" routerLink="/auth/login">Login</a> </li> <li class="nav-item" *ngIf="(userProfile?.sub ?? 0) > 0"> <a class="nav-link active" href="#">{{ userProfile?.email }}</a> </li> </ul> </div> </div> </nav> <router-outlet></router-outlet>
- (Line: 6-8) Add the 'Movies' menu item.
Configure JWT Module From '@auth0/angular-jwt':
The '@auth0/angular-jwt' library support automatically adding the jwt access token as an authorization header value to the API calls.
In 'AuthService' let's create a method that always tries to read the token from the browser's local storage.
src/app/shared/auth/auth.service.ts:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { JwtHelperService } from '@auth0/angular-jwt'; import { BehaviorSubject, catchError, map, of } from 'rxjs'; import { LoginModel } from 'src/app/auth/login-model'; import { TokenModel } from './token-model'; import { UserProfile } from './user-profile'; // existing code hidden for display purpose @Injectable({ providedIn: 'root', }) export class AuthService { constructor(private httpClient: HttpClient) {} userProfile = new BehaviorSubject<UserProfile | null>(null); jwtService: JwtHelperService = new JwtHelperService(); getAccessToken():string{ var localStorageToken = localStorage.getItem('tokens'); if(localStorageToken){ var token = JSON.parse(localStorageToken) as TokenModel; var isTokenExpired = this.jwtService.isTokenExpired(token.access_token); if(isTokenExpired){ this.userProfile.next(null); return ""; } var userInfo = this.jwtService.decodeToken( token.access_token ) as UserProfile; this.userProfile.next(userInfo); return token.access_token; } return ""; } }
- (Line: 19-35) The 'getAccessToken()' method is used by the JWTModule for fetching the access token from the browser's local storage.
- (Line: 20) Fetching token data from the browser's local storage.
- (Line: 21-27) The logic is executed if the tokens exist in the browser's local storage.
- (Line: 23) Checked jwt access token expired or not.
- (Line: 24-27) If the token expired then pass 'null' data to the 'userProfile' observable and then return the empty string value.
- (Line: 28-32) If the token is not expired then decode the jwt access token and then pass data to the 'userProfile' observable and finally return the access token
src/app/app.module.ts:
import { HttpClientModule } from '@angular/common/http'; import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { JwtModule, JWT_OPTIONS } from '@auth0/angular-jwt'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HomeComponent } from './home/home.component'; import { AuthService } from './shared/auth/auth.service'; export function jwtOptionFactor(authService:AuthService){ return { tokenGetter:() => { return authService.getAccessToken(); }, allowedDomains:["localhost:3000"], disallowedRoutes:[ "http://localhost:3000/auth/login" ] } } // existing code hidden for display purpose @NgModule({ imports: [ JwtModule.forRoot({ jwtOptionsProvider:{ provide:JWT_OPTIONS, useFactory: jwtOptionFactor, deps:[AuthService] } }) ] }) export class AppModule { }
- (Line: 11-21) The 'jwtOptionFactor' is the factory method for loading the setting of the jwt token into the 'JwtModule'. Here this method takes 'AuthService' as an input parameter.
- (Line: 13-15) The 'tokenGetter' always watches for the jwt token.
- (Line: 16) The 'allowedDomains' is the area to register the domains to which jwt token authorization header value to be added automatically.
- (Line: 17-19) The 'disallowedRoutes' to specify the routes which don't need a jwt token as authorization header for example login API never requires the jwt token.
- (Line: 27-33) Imported the 'JwtModule' that loads from the '@auth0/angular-jwt'.
- (Line: 30) To the 'useFactory' property assign our 'jwtOptionFactor' function.
- (Line: 31) The 'deps' represent dependencies here we have to pass our 'AuthService' that is because we are using the 'AuthService' as an input parameter to 'jwtOptionFactor' function.
(Step 1)
(Step 2)
In the next part of the article, we will implement angular routing guards, interceptors to invoke the refresh token endpoint, and then finally user logout.
Support Me!
Buy Me A Coffee
PayPal Me
Video Session:
Wrapping Up:
Hopefully, I think this article delivered some useful information on the Angular 14 Jwt Access Token Authentication and Refresh Token. using I love to have your feedback, suggestions, and better techniques in the comment section below.
Comments
Post a Comment