Implementing JWT Authentication in an Angular 6 Single Page Application

In this guide, we’ll explore how simple it is to incorporate JSON web token (JWT)](https://jwt.io/introduction/) authentication into your [Angular 6+ single-page application (SPA). First, let’s cover some fundamentals.

What Are JSON Web Tokens, and Why Use Them?

Put simply, they offer convenience, compactness, and security. Let’s break it down:

  1. Convenient: Authentication with a JWT after login involves setting a single HTTP header, easily automated via a function or subclass.
  2. Compact: A JWT is just a base64-encoded string with header fields and an optional payload, usually under 200 bytes even when signed.
  3. Secure: JWTs can be signed with RSA public/private keys or HMAC using a shared secret, ensuring origin and validity.

Essentially, JWT provides secure and efficient user authentication and API endpoint call verification without needing custom data parsing or encryption.

Application Theory

Typical data flow for JWT authentication and usage between front-end and back-end systems

Let’s see how this works in practice. Imagine a Node.js server hosting our API and an Angular 6 SPA todo list. Consider this API structure:

  • /authPOST (submit username and password for authentication and receive a JWT)
  • /todosGET (fetch user’s todo list items)
  • /todos/{id}GET (retrieve a specific todo item)
  • /usersGET (get a list of users)

We’ll build this app soon, but first, let’s focus on the theoretical interaction. A simple login page lets users enter credentials, sent to the /auth endpoint on form submission. The Node server authenticates the user (e.g., database lookup, external service query) and ultimately returns a JWT.

Our example JWT will contain reserved and private claims. Reserved claims are standard JWT key-value pairs for authentication, while private claims are specific to our app:

Reserved Claims

  • iss: Token issuer. Usually the server’s FQDN, but any recognizable value works.
  • exp: Token expiry (seconds since Unix epoch).
  • nbf: Not valid before timestamp (optional, sets a validity start time).

Private Claims

  • uid: Logged-in user’s ID.
  • role: Logged-in user’s role.

This information, base64-encoded and signed with HMAC using the shared key todo-app-super-shared-secret, might look like this:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0b2RvYXBpIiwibmJmIjoxNDk4MTE3NjQyLCJleHAiOjE0OTgxMjEyNDIsInVpZCI6MSwicm9sZSI6ImFkbWluIn0.ZDz_1vcIlnZz64nSM28yA1s-4c_iw3Z2ZtP-SgcYRPQ

This string validates the login, identifies the user, and even indicates their role(s).

Storing this JWT in localStorage or sessionStorage is common practice but not mandatory. How you handle the token is up to you, as long as it’s accessible for future API calls.

Subsequent API calls from the SPA to protected endpoints simply include the token in the Authorization header.

1
Authorization: Bearer {JWT Token}

Note: While common, JWT doesn’t mandate this method; you could append it to the URL or use a cookie.

Upon receiving the JWT, the server decodes it, verifies consistency with the HMAC secret, checks expiry (exp, nbf), and can even validate the issuer (iss).

Once validated, the JWT’s information can be utilized. For example, uid identifies the requesting user, and role can control endpoint access permissions (database lookups might be necessary depending on security requirements).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function getTodos(jwtString)
{
  var token = JWTDecode(jwtstring);
  if( Date.now() < token.nbf*1000) {
    throw new Error('Token not yet valid');
  }
  if( Date.now() > token.exp*1000) {
    throw new Error('Token has expired');
  }
  if( token.iss != 'todoapi') {
    throw new Error('Token not issued here');
  }

  var userID = token.uid;
  var todos = loadUserTodosFromDB(userID);

  return JSON.stringify(todos);
}

Building a Simple Todo App

To proceed, ensure you have recent versions of Node.js (6.x+), npm (3.x+), and angular-cli. For Node.js (which includes npm), follow the instructions here. Then install angular-cli using npm (or yarn):

1
2
3
4
5
# installation using npm
npm install -g @angular/cli

# installation using yarn
yarn global add @angular/cli

Without delving into Angular 6 boilerplate, I’ve created a Github repository with a sample todo app to demonstrate adding JWT authentication. Clone it:

1
2
3
git clone https://github.com/sschocke/angular-jwt-todo.git
cd angular-jwt-todo
git checkout pre-jwt

The command git checkout pre-jwt switches to a release without JWT implemented.

Inside, you’ll find two folders: server (Node API server) and client (Angular 6 app).

The Node API Server

Let’s install dependencies and start the API server.

1
2
3
4
5
6
7
8
9
cd server

# installation using npm
npm install

# or installation using yarn
yarn

node app.js

The following links should return JSON data (currently hardcoded for userID=1 without authentication):

  • http://localhost:4000: Node server test page
  • http://localhost:4000/api/users: List of users
  • http://localhost:4000/api/todos: Tasks for userID=1

The Angular App

Similarly, install dependencies and start the client app’s dev server.

1
2
3
4
5
6
7
8
9
cd client

# using npm
npm install
npm start

# using yarn
yarn
yarn start

Note: Downloading dependencies might take some time.

You should see something like this at http://localhost:4200:

The non-JWT-enabled version of our Angular Todo List app.

Adding Authentication via JWT

We’ll utilize readily available libraries to streamline JWT authentication implementation.

On the client-side, we’ll use a library by Auth0](https://auth0.com/), which enables [cloud-based authentication. Using this library doesn’t necessitate using their services.

1
2
3
4
5
6
7
cd client

# installation using npm
npm install @auth0/angular-jwt

# installation using yarn
yarn add @auth0/angular-jwt

On the server-side, we’ll use body-parser, jsonwebtoken, and express-jwt to handle JSON POST bodies and JWTs.

1
2
3
4
5
6
7
cd server

# installation using npm
npm install body-parser jsonwebtoken express-jwt

# installation using yarn
yarn add body-parser jsonwebtoken express-jwt

API Endpoint for Authentication

First, we need an authentication mechanism. For simplicity, we’ll use a fixed endpoint with a hardcoded username and password. The key is to return a JWT.

Add this line beneath the other require statements in server/app.js:

1
2
3
const bodyParser = require('body-parser');
const jwt = require('jsonwebtoken');
const expressJwt = require('express-jwt');

And add this code snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
app.use(bodyParser.json());

app.post('/api/auth', function(req, res) {
  const body = req.body;

  const user = USERS.find(user => user.username == body.username);
  if(!user || body.password != 'todo') return res.sendStatus(401);
  
  var token = jwt.sign({userID: user.id}, 'todo-app-super-shared-secret', {expiresIn: '2h'});
  res.send({token});
});

This code handles getting the JSON body, finding the user, checking credentials (returning a 401 Unauthorized error if invalid), and, importantly, generating the token.

Let’s break down the token generation (jwt.sign(payload, secretOrPrivateKey, [options, callback])):

  • payload: An object with key-value pairs to encode (decodable with the decryption key). Here, we include user.id to identify the user on subsequent requests.
  • secretOrPrivateKey: Either an HMAC shared secret key (used here for simplicity) or an RSA/ECDSA private key.
  • options: Key-value pairs for encoding options, typically including expiresIn (exp claim) and issuer (iss claim).
  • callback: A function for asynchronous token encoding (optional).

(You can learn more about more details on options and how to use public-key cryptography instead of a shared secret key.)

Angular 6 JWT Integration

Integrating JWT with Angular 6 is straightforward using angular-jwt. In client/src/app/app.modules.ts, add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { JwtModule } from '@auth0/angular-jwt';
// ...
export function tokenGetter() {
  return localStorage.getItem('access_token');
}

@NgModule({
// ...
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    // Add this import here
    JwtModule.forRoot({
      config: {
        tokenGetter: tokenGetter,
        whitelistedDomains: ['localhost:4000'],
        blacklistedRoutes: ['localhost:4000/api/auth']
      }
    })
  ],
// ...
}

That’s mostly it! While we still need to handle authentication logic, angular-jwt automatically sends the token with HTTP requests.

  • tokenGetter(): Provides the JWT string; our implementation retrieves it from localStorage.
  • whiteListedDomains: Restricts JWT sending to specific domains.
  • blackListedRoutes: Excludes specific routes from receiving the JWT.

Making It All Work Together

We now have JWT generation and Angular’s setup to send it. However, we need to enforce authentication in our client and API.

Let’s start by adding a login component, an authentication service, and an Angular Guard to protect routes in our client app.

1
2
3
4
cd client
ng g component login --spec=false --inline-style
ng g service auth --flat --spec=false
ng g guard auth --flat --spec=false

This should create four new files in the client folder:

1
2
3
4
src/app/login/login.component.html
src/app/login/login.component.ts
src/app/auth.service.ts
src/app/auth.guard.ts

Next, update client/src/app/app.modules.ts to provide the authentication service and guard:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';

// ...

providers: [
  TodoService,
  UserService,
  AuthService,
  AuthGuard
],

Modify routing in client/src/app/app-routing.modules.ts to use the authentication guard and add the login route:

1
2
3
4
5
6
7
8
9
// ...
import { LoginComponent } from './login/login.component';
import { AuthGuard } from './auth.guard';

const routes: Routes = [
  { path: 'todos', component: TodoListComponent, canActivate: [AuthGuard] },
  { path: 'users', component: UserListComponent, canActivate: [AuthGuard] },
  { path: 'login', component: LoginComponent},
  // ...

In client/src/app/auth.guard.ts, add the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private router: Router) { }

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    if (localStorage.getItem('access_token')) {
      return true;
    }

    this.router.navigate(['login']);
    return false;
  }
}

This basic implementation checks for a JWT in local storage. In a real app, you’d decode and verify its validity, expiration, etc. (e.g., using JwtHelperService).

Now, our app always redirects to the login page. Let’s fix that. Update client/src/app/auth.service.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

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

  login(username: string, password: string): Observable<boolean> {
    return this.http.post<{token: string}>('/api/auth', {username: username, password: password})
      .pipe(
        map(result => {
          localStorage.setItem('access_token', result.token);
          return true;
        })
      );
  }

  logout() {
    localStorage.removeItem('access_token');
  }

  public get loggedIn(): boolean {
    return (localStorage.getItem('access_token') !== null);
  }
}

This service has login and logout functions:

  • login: POSTs credentials to the backend and stores the received access_token in localStorage.
  • logout: Clears access_token from localStorage.
  • loggedIn: A boolean property indicating login status.

Finally, the login component (client/src/app/login/login.components.html):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<h4 *ngIf="error">{{error}}</h4>
<form (ngSubmit)="submit()">
  <div class="form-group col-3">
    <label for="username">Username</label>
    <input type="text" name="username" class="form-control" [(ngModel)]="username" />
  </div>
  <div class="form-group col-3">
    <label for="password">Password</label>
    <input type="password" name="password" class="form-control" [(ngModel)]="password" />
  </div>
  <div class="form-group col-3">
    <button class="btn btn-primary" type="submit">Login</button>
  </div>
</form>

And client/src/app/login/login.components.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth.service';
import { Router } from '@angular/router';
import { first } from 'rxjs/operators';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})
export class LoginComponent {
  public username: string;
  public password: string;
  public error: string;

  constructor(private auth: AuthService, private router: Router) { }

  public submit() {
    this.auth.login(this.username, this.password)
      .pipe(first())
      .subscribe(
        result => this.router.navigate(['todos']),
        err => this.error = 'Could not authenticate'
      );
  }
}

Here’s our basic Angular 6 login:

The login screen of our sample Angular Todo List app.

You should now be able to log in (using jemma, paul, or sebastian with password todo). However, the navigation is static. Let’s fix that by updating client/src/app/app.component.ts:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  constructor(private auth: AuthService, private router: Router) { }

  logout() {
    this.auth.logout();
    this.router.navigate(['login']);
  }
}

And replace the <nav> section in client/src/app/app.component.html:

1
2
3
4
5
6
  <nav class="nav nav-pills">
    <a class="nav-link" routerLink="todos" routerLinkActive="active" *ngIf="auth.loggedIn">Todo List</a>
    <a class="nav-link" routerLink="users" routerLinkActive="active" *ngIf="auth.loggedIn">Users</a>
    <a class="nav-link" routerLink="login" routerLinkActive="active" *ngIf="!auth.loggedIn">Login</a>
    <a class="nav-link" (click)="logout()" href="#" *ngIf="auth.loggedIn">Logout</a>
  </nav>

Our navigation is now context-aware, displaying elements based on login status.

Securing the API

You might have noticed that all users see the same todo list. This is because our /todos endpoint still returns data for userID=1. Let’s use the JWT to secure it.

Add this line beneath the other app.use() calls in server/app.js:

1
app.use(expressJwt({secret: 'todo-app-super-shared-secret'}).unless({path: ['/api/auth']}));

This uses express-jwt middleware, providing the shared secret and excluding paths that don’t require a JWT.

The middleware assumes the standard Bearer {token} format for the Authorization header but offers customization options (see express-jwt Usage).

express-jwt also decodes the token and populates req.user with the payload, giving us access to the encoded information. We can now use req.user.userID to identify the user.

Update the /todos endpoint in server/app.js:

1
res.send(getTodos(req.user.userID));
Our Angular Todo List app leveraging the JWT to show the todo list of the logged-in user, rather than the one we had hardcoded earlier.

That’s it! Our API now requires authentication, and we can identify the logged-in user. Our client app can now access protected resources.

To see the final result, checkout the repository:

1
git checkout with-jwt

Hopefully, this guide helps you implement JWT authentication in your Angular apps. Thanks for reading!

Licensed under CC BY-NC-SA 4.0