Published: December 08 2022

Angular 14 - JWT Authentication with Refresh Tokens Example & Tutorial

Tutorial built with Angular 14.2.12

Other versions available:

In this post we'll go through an example of how to implement JWT authentication with refresh tokens in Angular 14.

Example Angular 14 App

The example app is pretty minimal and contains just 2 pages to demonstrate JWT authentication with refresh tokens in Angular:

  • Login (/login) - public login page with username and password fields, on submit the page sends a POST request to the API to authenticate user credentials, on success the API returns two tokens:
    • A JWT (JSON Web Token) used to make authenticated requests to secure API routes, the JWT is short-lived and expires after 15 minutes.
    • A Refresh Token used to request a new JWT from the API when the old one expires (a.k.a. to refresh the token).
  • Home (/) - secure home page with a welcome message and a list of users, the users are fetched from a secure API endpoint with the JWT received after successful login.

JWT with Refresh Tokens vs JWT Only

The benefit of using refresh tokens over JWT alone is increased security because it allows you to use short-lived JWT tokens for authentication. JWTs are usually self contained tokens that cannot be revoked and are valid until they expire, so having a long-lived JWT poses a greater security risk if a token is compromised.

Refresh tokens are less likely to be compromised, they can be stored in HTTP Only cookies that are not accessible to client-side javascript which prevents XSS (cross site scripting). Refresh tokens are only sent with requests to generate new JWT tokens, they cannot access other secure routes which prevents them from being used in CSRF (cross site request forgery). Refresh tokens are revokable, if one is compromised it can be revoked on the server so it cannot generate any more JWTs.

The benefit of using JWT alone is simpler code and less complexity, the approach you choose depends on your use case and requirements. For the simplified version of this tutorial that doesn't use refresh tokens see Angular 14 - JWT Authentication Example & Tutorial.

Silent refresh of JWT auth tokens

On successful login the Angular app starts a countdown timer to automatically refresh the JWT one minute before it expires, this is known as silent refresh since it happens in the background. The timer restarts after each silent refresh so the JWT is always valid.

Keep logged in between browser sessions

To keep the user logged in between browser sessions the Angular app automatically attempts to request a new JWT from the API on startup. If the user logged in previously (without logging out) the browser will have a valid refresh token cookie that is sent with the request to generate a new JWT. On success the app starts up already logged in on the home page, otherwise the login page is displayed.

Login Form with Angular Reactive Forms

The login form in the example is built with the Reactive Forms library that comes as part of the Angular framework (in @angular/forms). It uses a model-driven approach to build, validate and handle forms in Angular. For more info see https://angular.io/guide/reactive-forms.

Fake Backend API

The Angular app runs with a fake backend by default to enable it to run completely in the browser without a real backend api (backend-less), to switch to a real api simply remove or comment out the line below the comment // provider used to create fake backend located in the app module (/src/app/app.module.ts).

You can build your own API or hook it up with a .NET API or Node.js API available below.

Styled with Bootstrap 5

The example login app is styled with the CSS from Bootstrap 5.2, for more info about Bootstrap see https://getbootstrap.com/docs/5.2/getting-started/introduction/.

Code on GitHub

The example project is available on GitHub at https://github.com/cornflourblue/angular-14-jwt-refresh-tokens.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-14-jwt-refresh-tokens)


Run the Angular JWT with Refresh Tokens Example Locally

  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the Angular project from https://github.com/cornflourblue/angular-14-jwt-refresh-tokens
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Start the app by running npm start from the command line in the project root folder, this will compile the Angular app and automatically launch it in the browser on the URL http://localhost:4200.

NOTE: You can also start the app with the Angular CLI command ng serve --open. To do this first install the Angular CLI globally on your system with the command npm install -g @angular/cli.


Connect the Angular App with a .NET 6.0 API

For full details about the .NET 6 API see .NET 6.0 - JWT Authentication with Refresh Tokens Tutorial with Example API. But to get up and running quickly just follow the below steps.

  1. Install the .NET SDK from https://dotnet.microsoft.com/download.
  2. Download or clone the project source code from https://github.com/cornflourblue/dotnet-6-jwt-refresh-tokens-api
  3. Start the API by running dotnet run from the command line in the project root folder (where the WebApi.csproj file is located), you should see the message Now listening on: http://localhost:4000.
  4. Back in the Angular app, remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file, then start the Angular app and it should now be connected with the .NET API.


Connect the Angular App with a Node.js + MongoDB API

For full details about the Node API see Node.js + MongoDB API - JWT Authentication with Refresh Tokens. But to get up and running quickly just follow the below steps.

  1. Install MongoDB Community Server from  https://www.mongodb.com/download-center/community.
  2. Run MongoDB, instructions are available on the install page for each OS at https://docs.mongodb.com/manual/administration/install-community/
  3. Download or clone the project source code from https://github.com/cornflourblue/node-mongo-jwt-refresh-tokens-api
  4. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  5. Start the api by running npm start (or npm run start:dev to start with nodemon) from the command line in the project root folder, you should see the message Server listening on port 4000, and you can view the Swagger API documentation at http://localhost:4000/api-docs.
  6. Back in the Angular app, remove or comment out the line below the comment // provider used to create fake backend located in the /src/app/app.module.ts file, then start the Angular app and it should now be hooked up with the Node.js + MongoDB API.


Angular 14 Project Structure

The Angular CLI was used to generate the base project structure with the ng new <project name> command, the CLI is also used to build and serve the application. For more info about the Angular CLI see https://angular.io/cli.

The app and code structure of the tutorial mostly follow the best practice recommendations in the official Angular Style Guide, with a few of my own tweaks here and there.

Folder Naming Convention

Each feature has it's own folder (home & login), other shared/common code such as services, models, helpers etc are placed in folders prefixed with an underscore _ to easily differentiate them from features and group them together at the top of the folder structure.

Barrel Files

The index.ts files in each folder are barrel files that group the exported modules from each folder together so they can be imported using only the folder path instead of the full module path, and to enable importing multiple modules in a single import (e.g. import { AuthenticationService, UserService } from '../_services').

TypeScript Path Aliases

Path aliases @app and @environments have been configured in tsconfig.json that map to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent').

Here are the main project files that contain the application logic, I didn't include files that were generated by the Angular CLI ng new command that I left unchanged.

 

App Initializer

Path: /src/app/_helpers/app.initializer.ts

The app initializer attempts to automatically login to the Angular app on startup by calling authenticationService.refreshToken() which sends a request to the API for a new JWT auth token. If the user previously logged in (without logging out) the browser will have a valid refresh token cookie that is sent with the request to generate a new JWT. On success the app starts on the home page with the user already logged in, otherwise the login page is displayed.

When an Angular app initializer function returns an Observable like this one, the observable must complete before the app can startup. An observable is complete when it has finished emitting all values without any errors. The catchError() operator is used to ensure the observable always reaches the completed state even if the refreshToken() request fails.

The appInitializer is configured as an Angular initialization function in the providers section of the app module. It's added with the injection token APP_INITIALIZER and has dependencies (deps) set to [AuthenticationService]. For more info see https://angular.io/api/core/APP_INITIALIZER

import { AuthenticationService } from '@app/_services';
import { catchError, finalize, of } from 'rxjs';

export function appInitializer(authenticationService: AuthenticationService) {
    return () => authenticationService.refreshToken()
        .pipe(
            // catch error to start app on success or failure
            catchError(() => of())
        );
}
 

Auth Guard

Path: /src/app/_helpers/auth.guard.ts

The auth guard is an angular route guard that prevents unauthenticated users from accessing restricted routes, it does this by implementing the CanActivate interface which allows the guard to decide if a route can be activated with the canActivate() method. If the method returns true the route is activated (allowed to proceed), otherwise if the method returns false the route is blocked.

The auth guard uses the authentication service to check if the user is logged in, if they are logged in it returns true from the canActivate() method, otherwise it returns false and redirects the user to the login page.

Angular route guards are attached to routes in the router config, this auth guard is used in app-routing.module.ts to protect the home page route.

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

import { AuthenticationService } from '@app/_services';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
    constructor(
        private router: Router,
        private authenticationService: AuthenticationService
    ) { }

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        const user = this.authenticationService.userValue;
        if (user) {
            // logged in so return true
            return true;
        } else {
            // not logged in so redirect to login page with the return url
            this.router.navigate(['/login'], { queryParams: { returnUrl: state.url } });
            return false;
        }
    }
}
 

Http Error Interceptor

Path: /src/app/_helpers/error.interceptor.ts

The Error Interceptor intercepts http responses from the api to check if there were any errors. If the response is 401 Unauthorized or 403 Forbidden the user is automatically logged out of the application, all other errors are logged to the console and re-thrown up to the calling service to be handled.

It is implemented using the HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to catch all error responses from the server in a single location.

HTTP interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

import { AuthenticationService } from '@app/_services';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(catchError(err => {
            if ([401, 403].includes(err.status) && this.authenticationService.userValue) {
                // auto logout if 401 or 403 response returned from api
                this.authenticationService.logout();
            }

            const error = (err && err.error && err.error.message) || err.statusText;
            console.error(err);
            return throwError(() => error);
        }))
    }
}
 

Fake Backend Provider

Path: /src/app/_helpers/fake-backend.ts

In order to run and test the Angular application without a real backend API, the example uses a fake backend that intercepts the HTTP requests from the Angular app and sends back "fake" responses. This is done by a class that implements the Angular HttpInterceptor interface, for more information on Angular HTTP Interceptors see https://angular.io/api/common/http/HttpInterceptor or this article.

The fake backend contains a handleRoute function that checks if the request matches one of the faked routes in the switch statement, at the moment this includes requests for handling authentication, refreshing tokens, revoking tokens, and getting all users. Matching requests are intercepted and handled by one of the below // route functions, non-matching requests are sent through to the real backend by calling next.handle(request);. Below the route functions there are // helper functions for returning different response types and performing other tasks such as generating and validating jwt and refresh tokens.

For more info on angular fake backends see Angular 14 - Fake Backend API to Intercept HTTP Requests in Development.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS, HttpHeaders } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';

// array in local storage for users
const usersKey = 'angular-14-jwt-refresh-token-users';
const users: any[] = JSON.parse(localStorage.getItem(usersKey)!) || [];

// add test user and save if users array is empty
if (!users.length) {
    users.push({ id: 1,  firstName: 'Test', lastName: 'User', username: 'test', password: 'test', refreshTokens: [] });
    localStorage.setItem(usersKey, JSON.stringify(users));
}

@Injectable()
export class FakeBackendInterceptor implements HttpInterceptor {
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const { url, method, headers, body } = request;

        // wrap in delayed observable to simulate server api call
        return of(null)
            .pipe(mergeMap(handleRoute))
            .pipe(materialize()) // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648)
            .pipe(delay(500))
            .pipe(dematerialize());

        function handleRoute() {
            switch (true) {
                case url.endsWith('/users/authenticate') && method === 'POST':
                    return authenticate();
                case url.endsWith('/users/refresh-token') && method === 'POST':
                    return refreshToken();
                case url.endsWith('/users/revoke-token') && method === 'POST':
                    return revokeToken();
                case url.endsWith('/users') && method === 'GET':
                    return getUsers();
                default:
                    // pass through any requests not handled above
                    return next.handle(request);
            }    
        }

        // route functions

        function authenticate() {
            const { username, password } = body;
            const user = users.find(x => x.username === username && x.password === password);
            
            if (!user) return error('Username or password is incorrect');

            // add refresh token to user
            user.refreshTokens.push(generateRefreshToken());
            localStorage.setItem(usersKey, JSON.stringify(users));

            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                jwtToken: generateJwtToken()
            })
        }

        function refreshToken() {
            const refreshToken = getRefreshToken();
            
            if (!refreshToken) return unauthorized();

            const user = users.find(x => x.refreshTokens.includes(refreshToken));
            
            if (!user) return unauthorized();

            // replace old refresh token with a new one and save
            user.refreshTokens = user.refreshTokens.filter((x: any) => x !== refreshToken);
            user.refreshTokens.push(generateRefreshToken());
            localStorage.setItem(usersKey, JSON.stringify(users));

            return ok({
                id: user.id,
                username: user.username,
                firstName: user.firstName,
                lastName: user.lastName,
                jwtToken: generateJwtToken()
            })
        }

        function revokeToken() {
            if (!isLoggedIn()) return unauthorized();
            
            const refreshToken = getRefreshToken();
            const user = users.find(x => x.refreshTokens.includes(refreshToken));
            
            // revoke token and save
            user.refreshTokens = user.refreshTokens.filter((x: any) => x !== refreshToken);
            localStorage.setItem(usersKey, JSON.stringify(users));

            return ok();
        }

        function getUsers() {
            if (!isLoggedIn()) return unauthorized();
            return ok(users);
        }

        // helper functions

        function ok(body?: any) {
            return of(new HttpResponse({ status: 200, body }))
        }

        function error(message: string) {
            return throwError(() => ({ error: { message } }));
        }

        function unauthorized() {
            return throwError(() => ({ status: 401, error: { message: 'Unauthorized' } }));
        }

        function isLoggedIn() {
            // check if jwt token is in auth header
            const authHeader = headers.get('Authorization') || '';
            if (!authHeader.startsWith('Bearer fake-jwt-token'))
                return false;

            // check if token is expired
            const jwtToken = JSON.parse(atob(authHeader.split('.')[1]));
            const tokenExpired = Date.now() > (jwtToken.exp * 1000);
            if (tokenExpired)
                return false;

            return true;
        }

        function generateJwtToken() {
            // create token that expires in 15 minutes
            const tokenPayload = { exp: Math.round(new Date(Date.now() + 15*60*1000).getTime() / 1000) }
            return `fake-jwt-token.${btoa(JSON.stringify(tokenPayload))}`;
        }

        function generateRefreshToken() {
            const token = new Date().getTime().toString();

            // add token cookie that expires in 7 days
            const expires = new Date(Date.now() + 7*24*60*60*1000).toUTCString();
            document.cookie = `fakeRefreshToken=${token}; expires=${expires}; path=/`;

            return token;
        }

        function getRefreshToken() {
            // get refresh token from cookie
            return (document.cookie.split(';').find(x => x.includes('fakeRefreshToken')) || '=').split('=')[1];
        }
    }
}

export let fakeBackendProvider = {
    // use fake backend in place of Http service for backend-less development
    provide: HTTP_INTERCEPTORS,
    useClass: FakeBackendInterceptor,
    multi: true
};
 

JWT Interceptor

Path: /src/app/_helpers/jwt.interceptor.ts

The JWT Interceptor intercepts http requests from the application to add a JWT auth token to the Authorization header if the user is logged in and the request is to the application api url (environment.apiUrl).

It is implemented using the HttpInterceptor interface included in the HttpClientModule, by implementing the HttpInterceptor interface you can create a custom interceptor to catch all error responses from the server in a single location.

Http interceptors are added to the request pipeline in the providers section of the app.module.ts file.

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

import { environment } from '@environments/environment';
import { AuthenticationService } from '@app/_services';

@Injectable()
export class JwtInterceptor implements HttpInterceptor {
    constructor(private authenticationService: AuthenticationService) { }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // add auth header with jwt if user is logged in and request is to the api url
        const user = this.authenticationService.userValue;
        const isLoggedIn = user?.jwtToken;
        const isApiUrl = request.url.startsWith(environment.apiUrl);
        if (isLoggedIn && isApiUrl) {
            request = request.clone({
                setHeaders: { Authorization: `Bearer ${user.jwtToken}` }
            });
        }

        return next.handle(request);
    }
}
 

User Model

Path: /src/app/_models/user.ts

The user model is a small class that defines the properties of a user.

export class User {
    id?: number;
    username?: string;
    password?: string;
    firstName?: string;
    lastName?: string;
    jwtToken?: string;
}
 

Authentication Service

Path: /src/app/_services/authentication.service.ts

The authentication service handles communication between the angular app and the backend api for everything related to authentication. It contains methods for login, logout and refresh token, and contains properties for accessing the current user.

How RxJS is used by the service

RxJS Subjects and Observables are used to store the current user object and notify other components when the user logs in and out of the app. Angular components can subscribe() to the public user: Observable property to be notified of changes, and notifications are sent when the this.userSubject.next() method is called in the login(), logout() and refreshToken() methods, passing the argument to each subscriber.

The RxJS BehaviorSubject is a special type of Subject that keeps hold of the current value and emits it to any new subscribers as soon as they subscribe, while regular Subjects don't store the current value and only emit values that are published after a subscription is created. For more info on component communication with RxJS see Angular 14 - Communicating Between Components with RxJS Observable & Subject.

Auth service methods and properties

The user property exposes an RxJS observable (Observable<User>) so any component can subscribe to be notified when a user logs in, logs out or has their token refreshed.

The userValue getter allows other components to quickly get the value of the current user without having to subscribe to the user observable.

The login() method POSTs the username and password to the API for authentication, on success the api returns the user details and a JWT token which are published to all subscribers with the call to this.userSubject.next(user), the api also returns a refresh token cookie which is stored by the browser. The method then starts a countdown timer by calling this.startRefreshTokenTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires so the user stays logged in.

The logout() method makes a POST request to the API to revoke the refresh token that is stored in a browser cookie, cancels the silent refresh running in the background by calling this.stopRefreshTokenTimer(), logs the user out by publishing a null value to all subscriber components (this.userSubject.next(null)), and finally redirects the user to the login page.

The refreshToken() method is similar to the login() method, they both perform authentication, but this method does it by making a POST request to the API that includes a refresh token cookie. On success the api returns the user details and a JWT token which are published to all subscribers with the call to this.userSubject.next(user), the api also returns a new refresh token cookie which replaces the old one in the browser. The method then starts a countdown timer by calling this.startRefreshTokenTimer() to auto refresh the JWT token in the background (silent refresh) one minute before it expires so the user stays logged in.

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

@Injectable({ providedIn: 'root' })
export class AuthenticationService {
    private userSubject: BehaviorSubject<User | null>;
    public user: Observable<User | null>;

    constructor(
        private router: Router,
        private http: HttpClient
    ) {
        this.userSubject = new BehaviorSubject<User | null>(null);
        this.user = this.userSubject.asObservable();
    }

    public get userValue() {
        return this.userSubject.value;
    }

    login(username: string, password: string) {
        return this.http.post<any>(`${environment.apiUrl}/users/authenticate`, { username, password }, { withCredentials: true })
            .pipe(map(user => {
                this.userSubject.next(user);
                this.startRefreshTokenTimer();
                return user;
            }));
    }

    logout() {
        this.http.post<any>(`${environment.apiUrl}/users/revoke-token`, {}, { withCredentials: true }).subscribe();
        this.stopRefreshTokenTimer();
        this.userSubject.next(null);
        this.router.navigate(['/login']);
    }

    refreshToken() {
        return this.http.post<any>(`${environment.apiUrl}/users/refresh-token`, {}, { withCredentials: true })
            .pipe(map((user) => {
                this.userSubject.next(user);
                this.startRefreshTokenTimer();
                return user;
            }));
    }

    // helper methods

    private refreshTokenTimeout?: NodeJS.Timeout;

    private startRefreshTokenTimer() {
        // parse json object from base64 encoded jwt token
        const jwtBase64 = this.userValue!.jwtToken!.split('.')[1];
        const jwtToken = JSON.parse(atob(jwtBase64));

        // set a timeout to refresh the token a minute before it expires
        const expires = new Date(jwtToken.exp * 1000);
        const timeout = expires.getTime() - Date.now() - (60 * 1000);
        this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(), timeout);
    }

    private stopRefreshTokenTimer() {
        clearTimeout(this.refreshTokenTimeout);
    }
}
 

User Service

Path: /src/app/_services/user.service.ts

The user service contains a single method for getting all users from the api, I included it to demonstrate accessing a secure api endpoint using a JWT token after logging in to the application, the token is added to the authorization header of the http request by the JWT Interceptor. The secure endpoint in the example is implemented in the fake backend.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { environment } from '@environments/environment';
import { User } from '@app/_models';

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

    getAll() {
        return this.http.get<User[]>(`${environment.apiUrl}/users`);
    }
}
 

Home Component Template

Path: /src/app/home/home.component.html

The home component template contains html and Angular 14 template syntax for displaying a simple welcome message and a list of users from a secure api endpoint.

<div class="card mt-4">
    <h4 class="card-header py-3">You're logged in with Angular 14 + JWT & Refresh Tokens!!</h4>
    <div class="card-body">
        <h6>Users from secure api end point</h6>
        <div *ngIf="loading" class="spinner-border spinner-border-sm"></div>
        <ul *ngIf="users">
            <li *ngFor="let user of users">{{user.firstName}} {{user.lastName}}</li>
        </ul>
    </div>
</div>
 

Home Component

Path: /src/app/home/home.component.ts

The home component defines an Angular 14 component that gets all users from the user service and makes them available to the template via a users array property.

import { Component } from '@angular/core';
import { first } from 'rxjs/operators';

import { User } from '@app/_models';
import { UserService } from '@app/_services';

@Component({ templateUrl: 'home.component.html' })
export class HomeComponent {
    loading = false;
    users?: User[];

    constructor(private userService: UserService) { }

    ngOnInit() {
        this.loading = true;
        this.userService.getAll().pipe(first()).subscribe(users => {
            this.loading = false;
            this.users = users;
        });
    }
}
 

Login Component Template

Path: /src/app/login/login.component.html

The login component template contains a login form with username and password fields. It displays validation messages for invalid fields when the submit button is clicked. The form submit event is bound to the onSubmit() method of the login component.

The component uses reactive form validation to validate the input fields, for more information about angular reactive form validation see Angular 14 - Reactive Forms Validation Example.

<div class="col-md-6 offset-md-3 mt-5">
    <div class="alert alert-info">
        Username: test<br />
        Password: test
    </div>
    <div class="card">
        <h4 class="card-header py-3">Angular 14 JWT Auth with Refresh Tokens</h4>
        <div class="card-body">
            <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
                <div class="mb-3">
                    <label class="form-label">Username</label>
                    <input type="text" formControlName="username" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.username.errors }" />
                    <div *ngIf="submitted && f.username.errors" class="invalid-feedback">
                        <div *ngIf="f.username.errors.required">Username is required</div>
                    </div>
                </div>
                <div class="mb-3">
                    <label class="form-label">Password</label>
                    <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                        <div *ngIf="f.password.errors.required">Password is required</div>
                    </div>
                </div>
                <button [disabled]="loading" class="btn btn-primary">
                    <span *ngIf="loading" class="spinner-border spinner-border-sm mr-1"></span>
                    Login
                </button>
                <div *ngIf="error" class="alert alert-danger mt-3 mb-0">{{error}}</div>
            </form>
        </div>
    </div>
</div>
 

Login Component

Path: /src/app/login/login.component.ts

The login component uses the authentication service to login to the application. If the user is already logged in they are automatically redirected to the home page.

The loginForm: FormGroup object defines the form controls and validators, and is used to access data entered into the form. The FormGroup class is part of the Angular Reactive Forms module and is bound to the login template above with the [formGroup]="loginForm" directive.

import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';

import { AuthenticationService } from '@app/_services';

@Component({ templateUrl: 'login.component.html' })
export class LoginComponent implements OnInit {
    loginForm!: FormGroup;
    loading = false;
    submitted = false;
    error = '';

    constructor(
        private formBuilder: FormBuilder,
        private route: ActivatedRoute,
        private router: Router,
        private authenticationService: AuthenticationService
    ) { 
        // redirect to home if already logged in
        if (this.authenticationService.userValue) { 
            this.router.navigate(['/']);
        }
    }

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

    // convenience getter for easy access to form fields
    get f() { return this.loginForm.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.loginForm.invalid) {
            return;
        }

        this.loading = true;
        this.authenticationService.login(this.f.username.value, this.f.password.value)
            .pipe(first())
            .subscribe({
                next: () => {
                    // get return url from route parameters or default to '/'
                    const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
                    this.router.navigate([returnUrl]);
                },
                error: error => {
                    this.error = error;
                    this.loading = false;
                }
            });
    }
}
 

App Routing Module

Path: /src/app/app-routing.module.ts

Routing for the Angular app is configured as an array of Routes, each component is mapped to a path so the Angular Router knows which component to display based on the URL in the browser address bar. The home route is secured by passing the AuthGuard to the canActivate property of the route.

The routes array is passed to the RouterModule.forRoot() method which creates a routing module with all of the app routes configured, and also includes all of the Angular Router providers and directives such as the <router-outlet></router-outlet> directive. For more information on Angular Routing and Navigation see https://angular.io/guide/router.

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

import { HomeComponent } from './home';
import { LoginComponent } from './login';
import { AuthGuard } from './_helpers';

const routes: Routes = [
    { path: '', component: HomeComponent, canActivate: [AuthGuard] },
    { path: 'login', component: LoginComponent },

    // otherwise redirect to home
    { path: '**', redirectTo: '' }
];

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

App Component Template

Path: /src/app/app.component.html

The app component template is the root component template of the application, it contains the main nav bar which is only displayed for authenticated users, and a <router-outlet> directive for displaying the contents of each view based on the current route / path.

<!-- nav -->
<nav class="navbar navbar-expand navbar-dark bg-dark px-3" *ngIf="user">
    <div class="navbar-nav">
        <a class="nav-item nav-link" routerLink="/" routerLinkActive="active">Home</a>
        <button class="btn btn-link nav-item nav-link" (click)="logout()">Logout</button>
    </div>
</nav>

<!-- main app container -->
<div class="container">
    <router-outlet></router-outlet>
</div>
 

App Component

Path: /src/app/app.component.ts

The app component is the root component of the application, it defines the root tag of the app as <app-root></app-root> with the selector property of the @Component() decorator.

It subscribes to the user observable in the authentication service so it can reactively show/hide the main navigation bar when the user logs in/out of the application. Usually it's best practice to unsubscribe when the component is destroyed to avoid memory leaks. This isn't necessary in the root app component because it will only be destroyed when the entire app is closed.

The app component contains a logout() method which is called from the logout link in the main nav bar above to log the user out and redirect them to the login page.

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

import { AuthenticationService } from './_services';
import { User } from './_models';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    user?: User | null;

    constructor(private authenticationService: AuthenticationService) {
        this.authenticationService.user.subscribe(x => this.user = x);
    }

    logout() {
        this.authenticationService.logout();
    }
}
 

App Module

Path: /src/app/app.module.ts

The app module defines the root module of the application along with metadata about the module. The imports specify which other angular modules are required by this module, the declarations state which components belong to this module, and the providers configure dependency injection for the module including HTTP_INTERCEPTORS and the APP_INITIALIZER.

The fake backend API is an HTTP interceptor configured in the providers section, to switch to a real backend simply remove the fakeBackendProvider located below the comment // provider used to create fake backend.

For more info on Angular modules see https://angular.io/docs/ts/latest/guide/ngmodule.html.

import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';

// used to create fake backend
import { fakeBackendProvider } from './_helpers';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';

import { JwtInterceptor, ErrorInterceptor, appInitializer } from './_helpers';
import { AuthenticationService } from './_services';
import { HomeComponent } from './home';
import { LoginComponent } from './login';

@NgModule({
    imports: [
        BrowserModule,
        ReactiveFormsModule,
        HttpClientModule,
        AppRoutingModule
    ],
    declarations: [
        AppComponent,
        HomeComponent,
        LoginComponent
    ],
    providers: [
        { provide: APP_INITIALIZER, useFactory: appInitializer, multi: true, deps: [AuthenticationService] },
        { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
        { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },

        // provider used to create fake backend
        fakeBackendProvider
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
 

Production Environment Config

Path: /src/environments/environment.prod.ts

The production environment config contains variables required to run the application in production. This enables you to build the application with a different configuration for each different environment (e.g. production & development) without updating the app code.

When you build the application for production with the command ng build --configuration production, the output environment.ts is replaced with environment.prod.ts.

export const environment = {
    production: true,
    apiUrl: 'http://localhost:4000'
};
 

Development Environment Config

Path: /src/environments/environment.ts

The development environment config contains variables required to run the application in development.

Environment config is accessed by importing the environment object into any Angular service or component with the line import { environment } from '@environments/environment' and accessing properties on the environment object, see the user service for an example.

export const environment = {
    production: false,
    apiUrl: 'http://localhost:4000'
};
 

Main Index Html File

Path: /src/index.html

The main index.html file is the initial page loaded by the browser that kicks everything off. The Angular CLI (with Webpack under the hood) bundles all of the compiled javascript files together and injects them into the body of the index.html page so the scripts can be loaded and executed by the browser.

<!DOCTYPE html>
<html>
<head>
    <base href="/" />
    <title>Angular 14 - JWT Authentication Tutorial & Example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- bootstrap css -->
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-Zenh87qX5JnK2Jl0vWa8Ck2rdkQ2Bzep5IDxbcnCeuOxjzrPF/et3URy9Bv1WTRi" crossorigin="anonymous">
</head>
<body>
    <app-root></app-root>
</body>
</html>
 

Main (Bootstrap) File

Path: /src/main.ts

The main file is the entry point used by angular to launch and bootstrap the application.

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
    enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.error(err));
 

Polyfills

Path: /src/polyfills.ts

Some features used by Angular 14 are not yet supported natively by all major browsers, polyfills are used to add support for features where necessary so your Angular 14 application works across all major browsers.

This file is generated by the Angular CLI when creating a new project with the ng new command, I've excluded the comments in the file for brevity.

import 'zone.js';
 

Package.json

Path: /package.json

The package.json file contains project configuration information including package dependencies that get installed when you run npm install and scripts that are executed when you run npm start or npm run build etc. Full documentation is available at https://docs.npmjs.com/files/package.json.

{
    "name": "angular-jwt-refresh-tokens",
    "version": "0.0.0",
    "scripts": {
        "ng": "ng",
        "start": "ng serve --open",
        "build": "ng build",
        "watch": "ng build --watch --configuration development",
        "test": "ng test"
    },
    "private": true,
    "dependencies": {
        "@angular/animations": "^14.2.0",
        "@angular/common": "^14.2.0",
        "@angular/compiler": "^14.2.0",
        "@angular/core": "^14.2.0",
        "@angular/forms": "^14.2.0",
        "@angular/platform-browser": "^14.2.0",
        "@angular/platform-browser-dynamic": "^14.2.0",
        "@angular/router": "^14.2.0",
        "rxjs": "~7.5.0",
        "tslib": "^2.3.0",
        "zone.js": "~0.11.4"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^14.2.8",
        "@angular/cli": "~14.2.8",
        "@angular/compiler-cli": "^14.2.0",
        "@types/jasmine": "~4.0.0",
        "jasmine-core": "~4.3.0",
        "karma": "~6.4.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.2.0",
        "karma-jasmine": "~5.1.0",
        "karma-jasmine-html-reporter": "~2.0.0",
        "typescript": "~4.7.2"
    }
}
 

TypeScript tsconfig.json

Path: /tsconfig.json

The tsconfig.json file contains the base TypeScript compiler configuration for all projects in the Angular workspace, it configures how the TypeScript code will be compiled / transpiled into JavaScript that is understood by the browser. For more info see https://angular.io/config/tsconfig.

Most of the file is unchanged from when it was generated by the Angular CLI, only the paths property has been added to map @app and @environments to the /src/app and /src/environments directories. This allows imports to be relative to the app and environments folders by prefixing import paths with aliases instead of having to use long relative paths (e.g. import MyComponent from '../../../MyComponent').

/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitOverride": true,
        "noPropertyAccessFromIndexSignature": false,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2020",
        "module": "es2020",
        "lib": [
            "es2020",
            "dom"
        ],
        "paths": {
            "@app/*": ["src/app/*"],
            "@environments/*": ["src/environments/*"]
        }
    },
    "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
    }
}

 


Need Some Angular 14 Help?

Search fiverr for freelance Angular 14 developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by