1. Code
  2. Cloud & Hosting

Token-Based Authentication With Angular and Node

Scroll to top

Authentication is one of the most important parts of any web application. This tutorial discusses token-based authentication systems and how they differ from traditional login systems. At the end of this tutorial, you'll see a fully working demo written in Angular and Node.js.

Traditional Authentication Systems

Before proceeding with a token-based authentication system, let's have a look at a traditional authentication system first.

  1. The user provides a username and password in the login form and clicks Log In.
  2. After the request is made, validate the user on the back-end by querying the database. If the request is valid, create a session by using the user information fetched from the database, and then return the session information in the response header in order to store the session ID in the browser.
  3. Provide the session information for accessing restricted endpoints in the application.
  4. If the session information is valid, let the user access the specified endpoints, and respond with the rendered HTML content.

Everything is fine up to this point. The web application works well, and it is able to authenticate users so that they may access restricted endpoints. However, what happens when you want to develop another client, say for Android, for your application? Will you be able to use the current application to authenticate mobile clients and serve restricted content? As it currently stands, no. There are two main reasons for this:

  1. Sessions and cookies do not make sense for mobile applications. You cannot share sessions or cookies created on the server-side with mobile clients.
  2. In the current application, the rendered HTML is returned. In a mobile client, you need something like JSON or XML to be included as the response.

In this case, you need a client-independent application.

Token-Based Authentication

In token-based authentication, cookies and sessions will not be used. A token will be used for authenticating a user for each request to the server. Let's redesign the first scenario with token-based authentication.

It will use the following flow of control:

  1. The user provides a username and password in the login form and clicks Log In.
  2. After a request is made, validate the user on the back end by querying in the database. If the request is valid, create a token by using the user information fetched from the database, and then return that information in the response header so that we can store the token browser in local storage.
  3. Provide token information in every request header for accessing restricted endpoints in the application.
  4. If the token fetched from the request header information is valid, let the user access the specified endpoint, and respond with JSON or XML.

In this case, we have no returned session or cookie, and we have not returned any HTML content. That means that we can use this architecture for any client for a specific application. You can see the architecture schema below:

architecture schemaarchitecture schemaarchitecture schema

So what is this JWT?

JWT

JWT stands for JSON Web Token and is a token format used in authorization headers. This token helps you to design communication between two systems in a secure way. Let's rephrase JWT as the "bearer token" for the purposes of this tutorial. A bearer token consists of three parts: header, payload, and signature.

  • The header is the part of the token that keeps the token type and encryption method, which is also encrypted with base-64.
  • The payload includes the information. You can put in any kind of data like user info, product info, and so on, all of which is stored with base-64 encryption.
  • The signature consists of combinations of the header, payload, and secret key. The secret key must be kept securely on the server-side.

You can see the JWT schema and an example token below:

 JWT schema and an example token  JWT schema and an example token  JWT schema and an example token

You do not need to implement the bearer token generator as you can find established packages in several languages. You can see some of them below:

Node.js https://github.com/auth0/node-jsonwebtoken 
PHP http://github.com/firebase/php-jwt
Java http://github.com/auth0/java-jwt
Ruby https://github.com/jwt/ruby-jwt
.NET https://github.com/auth0/java-jwt
Python http://github.com/progrium/pyjwt/

A Practical Example

After covering some basic information about token-based authentication, we can now proceed with a practical example. Take a look at the following schema, after which we'll analyze it in more detail:

schemaschemaschema
  1. The requests are made by several clients, such as a web application or a mobile client, to the API for a specific purpose.
  2. The requests are made to a service like https://api.yourexampleapp.com. If lots of people use the application, multiple servers may be required to serve the requested operation.
  3. Here, the load balancer is used for balancing requests to best suit the application servers at the back-end. When you make a request to https://api.yourexampleapp.com, first the load balancer will handle a request, and then it will redirect the client to a specific server.
  4. There is one application, and this application is deployed to several servers (server-1, server-2, ..., server-n). Whenever a request is made to https://api.yourexampleapp.com, the back-end application will intercept the request header and extract token information from the authorization header. A database query will be made by using this token. If this token is valid and has the required permission to access the requested endpoint, it will continue. If not, it will return a 403 response code (which indicates a forbidden status).

Advantages

Token-based authentication comes with several advantages that solve serious problems. Here are a few of them:

Client-Independent Services

In token-based authentication, a token is transferred via request headers, instead of keeping the authentication information in sessions or cookies. This means there is no state. You can send a request to the server from any type of client that can make HTTP requests.

Content Delivery Networks (CDNs)

In most current web applications, views are rendered on the back-end, and HTML content is returned to the browser. Front-end logic depends on back-end code.

There is no need to make such a dependency. This comes with several problems. For example, if you are working with a design agency that implements your front-end HTML, CSS, and JavaScript, you need to take that front-end code and migrate it into your back-end code in order to do some rendering or populating operations. After some time, your rendered HTML content will differ greatly from what the code agency implemented.

In token-based authentication, you can develop a front-end project separately from the back-end code. Your back-end code will return a JSON response instead of rendered HTML, and you can put the minified, gzipped version of the front-end code into the CDN. When you go to your web page, the HTML content will be served from the CDN, and the page content will be populated by API services using the token in the authorization headers.

No Cookie-Session (or No CSRF)

CSRF is a major problem in modern web security because it doesn't check whether a request source is trusted or not. To solve this problem, a token pool is used for sending that token on every form post. In token-based authentication, a token is used in authorization headers, and CSRF does not include that information.

Persistent Token Store

When a session read, write, or delete operation is made in the application, it will make a file operation in the operating system's temp folder, at least for the first time. Let's say that you have multiple servers, and a session is created on the first server. When you make another request and your request drops in another server, session information will not exist and will get an "unauthorized" response. I know, you can solve that with a sticky session. However, in token-based authentication, this case is solved naturally. There is no sticky session problem because the request token is intercepted on every request on any server.

Those are the most common advantages of token-based authentication and communication. That's the end of the theoretical and architectural talk about token-based authentication. Time for a practical example.

An Example Application

You will see two applications to demonstrate token-based authentication:

  1. token-based-auth-backend
  2. token-based-auth-frontend

In the back-end project, there will be service implementations, and service results will be in JSON format. There is no view returned in services. In the front-end project, there will be an Angular project for front-end HTML and then the front-end app will be populated by Angular services to make requests to the back-end services.

token-based-auth-backend

In the back-end project, there are three main files:

  • package.json is for dependency management.
  • models/User.js contains a User model that will be used for making database operations about users.
  • server.js is for project bootstrapping and request handling.

That's it! This project is very simple, so that you can understand the main concept easily without doing a deep dive.

1
{
2
    "name": "angular-restful-auth",
3
    "version": "0.0.1",
4
    "dependencies": {
5
        "body-parser": "^1.20.2",
6
        "express": "4.x",
7
        "express-jwt": "8.4.1",
8
        "jsonwebtoken": "9.0.0",
9
        "mongoose": "7.3.1",
10
        "morgan": "latest"
11
    },
12
    "engines": {
13
        "node": ">=0.10.0"
14
    }
15
}
 

package.json contains dependencies for the project: express for MVC, body-parser for simulating post request handling in Node.js, morgan for request logging, mongoose for our ORM framework to connect to MongoDB, and jsonwebtoken for creating JWT tokens by using our User model. There is also an attribute called engines that says that this project is made using Node.js version >= 0.10.0. This is useful for PaaS services like Heroku. We will also cover that topic in another section.

1
const mongoose = require('mongoose');
2
const Schema = mongoose.Schema;
3
4
const UserSchema = new Schema({
5
  email: String,
6
  password: String,
7
  token: String
8
});
9
10
module.exports = mongoose.model('User', UserSchema);
 

We said that we would generate a token by using the user model payload. This model helps us to make user operations on MongoDB. In User.js, the user-schema is defined and the User model is created by using a mongoose model. This model is ready for database operations.

Our dependencies are defined, and our user model is defined, so now let's combine all those to construct a service for handling specific requests.

1
// Required Modules

2
const express    = require("express");
3
const morgan     = require("morgan");
4
const bodyParser = require("body-parser");
5
const jwt        = require("jsonwebtoken");
6
const mongoose   = require("mongoose");
7
const app        = express();
 

In Node.js, you can include a module in your project by using require. First, we need to import the necessary modules into the project:

1
const port = process.env.PORT || 3001;
2
const User     = require('./models/User');
3
4
// Connect to DB
5
mongoose.connect(process.env.MONGO_URL);
 

Our service will serve through a specific port. If any port variable is defined in the system environment variables, you can use that, or we have defined port 3001. After that, the User model is included, and the database connection is established in order to do some user operations. Do not forget to define an environment variable—MONGO_URL—for the database connection URL.

1
app.use(bodyParser.urlencoded({ extended: true }));
2
app.use(bodyParser.json());
3
app.use(morgan("dev"));
4
app.use(function(req, res, next) {
5
    res.setHeader('Access-Control-Allow-Origin', '*');
6
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
7
    res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type, Authorization');
8
    next();
9
});
10
 

In the above section, we've made some configurations for simulating an HTTP request handling in Node by using Express. We are allowing requests to come from different domains in order to develop a client-independent system. If you do not allow this, you will trigger a CORS (Cross Origin Request Sharing) error in the web browser.

  • Access-Control-Allow-Origin allowed for all domains.
  • You can send POST and GET requests to this service.
  • X-Requested-With and content-type headers are allowed.
1
app.post('/authenticate', async function(req, res) {
2
    try {
3
      const user = await User.findOne({ email: req.body.email, password: req.body.password }).exec();
4
      if (user) {
5
        res.json({
6
          type: true,
7
          data: user,
8
          token: user.token
9
        });
10
      } else {
11
        res.json({
12
          type: false,
13
          data: "Incorrect email/password"
14
        });
15
      }
16
    } catch (err) {
17
      res.json({
18
        type: false,
19
        data: "Error occurred: " + err
20
      });
21
    }
22
  });
 

We have imported all of the required modules and defined our configuration, so now it's time to define request handlers. In the above code, whenever you make a POST request to /authenticate with username and password, you will get a JWT token. First, the database query is processed by using a username and password. If a user exists, the user data will be returned with its token. But what if there is no such user matching the username and/or password?

1
 app.post('/signin', async function(req, res) {
2
    try {
3
      const existingUser = await User.findOne({ email: req.body.email }).exec();
4
      if (existingUser) {
5
        res.json({
6
          type: false,
7
          data: "User already exists!"
8
        });
9
      } else {
10
        const userModel = new User();
11
        userModel.email = req.body.email;
12
        userModel.password = req.body.password;
13
        const savedUser = await userModel.save();
14
        savedUser.token = jwt.sign(savedUser.toObject(), process.env.JWT_SECRET);
15
        const updatedUser = await savedUser.save();
16
        res.json({
17
          type: true,
18
          data: updatedUser,
19
          token: updatedUser.token
20
        });
21
      }
22
    } catch (err) {
23
      res.json({
24
        type: false,
25
        data: "Error occurred: " + err
26
      });
27
    }
28
  });
 

When you make a POST request to /signin with username and password, a new user will be created by using posted user information. On the 14th line, you can see that a new JSON token is generated by using the jsonwebtoken module, which has been assigned to the jwt variable. The authentication part is OK. What if we try to access a restricted endpoint? How can we manage to access that endpoint?

1
app.get('/me', ensureAuthorized, async function(req, res) {
2
    try {
3
      const user = await User.findOne({ token: req.token }).exec();
4
      res.json({
5
        type: true,
6
        data: user
7
      });
8
    } catch (err) {
9
      res.json({
10
        type: false,
11
        data: "Error occurred: " + err
12
      });
13
    }
14
  });
 

When you make a GET request to /me, you will get the current user info, but in order to continue with the requested endpoint, the ensureAuthorized function will be executed.

1
function ensureAuthorized(req, res, next) {
2
    var bearerToken;
3
    var bearerHeader = req.headers["authorization"];
4
    if (typeof bearerHeader !== 'undefined') {
5
        var bearer = bearerHeader.split(" ");
6
        bearerToken = bearer[1];
7
        req.token = bearerToken;
8
        next();
9
    } else {
10
        res.send(403);
11
    }
12
}
 

In this function, request headers are intercepted, and the authorization header is extracted. If a bearer token exists in this header, that token is assigned to req.token in order to be used throughout the request, and the request can be continued by using next(). If a token does not exist, you will get a 403 (Forbidden) response. Let's go back to the handler /me, and use req.token to fetch user data with this token. Whenever you create a new user, a token is generated and saved in the user model in DB. Those tokens are unique.

We have only three handlers for this simple project. After that, you will see:

1
process.on('uncaughtException', function(err) {
2
    console.log(err);
3
});
 

The Node.js app may crash if an error occurs. With the above code, that crash is prevented and an error log is printed in the console. And finally, we can start the server by using the following code snippet.

1
// Start Server

2
app.listen(port, function () {
3
    console.log( "Express server listening on port " + port);
4
});
 

To sum up:

  • Modules are imported.
  • Configurations are made.
  • Request handlers are defined.
  • Middleware is defined in order to intercept restricted endpoints.
  • The server is started.

We are done with the back-end service. So that it can be used by multiple clients, you can deploy this simple server application to your servers, or maybe you can deploy in Heroku. There is a file called Procfile in the project's root folder. Let's deploy our service in Heroku.

Heroku Deployment

You can clone the back-end project from this GitHub repository.

I will not be discussing how to create an app in Heroku; you can refer to this article for creating a Heroku app if you have not done this before. After you create your Heroku app, you can add a destination to your current project by using the following command:

1
git remote add heroku <your_heroku_git_url>

Now you have cloned a project and added a destination. After git add and git commit, you can push your code to Heroku by performing git push heroku master. When you successfully push a project, Heroku will perform the npm install command to download dependencies into the temp folder on Heroku. After that, it will start your application, and you can access your service by using the HTTP protocol.

token-based-auth-frontend

In the front-end project, you will see an Angular project. Here, I'll only mention the main sections in the front-end project, because Angular is not something that can be covered within a single tutorial.

You can clone the project from this GitHub repository. In this project, you will see the following folder structure:

folder structurefolder structurefolder structure

We have three components—sign up, profile, and sign in—and an auth service.

Your app.component.html looks like this:

1
<!doctype html>
2
<html lang="en">
3
  <head>
4
    <meta charset="utf-8">
5
    <meta name="viewport" content="width=device-width, initial-scale=1">
6
    <title>Bootstrap demo</title>
7
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
8
  </head>
9
  <body>
10
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
11
        <div class="container-fluid">
12
          <a class="navbar-brand" href="#">Home</a>
13
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
14
            <span class="navbar-toggler-icon"></span>
15
          </button>
16
          <div class="collapse navbar-collapse" id="navbarNav">
17
            <ul class="navbar-nav">
18
              
19
              <li class="nav-item"><a class="nav-link" routerLink="/profile">Me</a></li>
20
              <li class="nav-item"><a class="nav-link" routerLink="/login">Signin</a></li>
21
              <li class="nav-item"><a class="nav-link" routerLink="/signup">Signup</a></li>
22
              <li class="nav-item"><a class="nav-link" (click)="logout()">Logout</a></li>
23
            </ul>
24
          </div>
25
        </div>
26
      </nav>
27
28
    <div class="container">
29
        <router-outlet></router-outlet>
30
    </div> 
31
32
  </body>
33
</html>
 

In the main component file, the <router-outlet></router-outlet> defines the routes for individual components.

In the auth.service.ts file, we define the AuthService class, which handles authentication by making API calls to sign in, authenticate, and me API endpoints of the Node.js app. 

1
import { Injectable } from '@angular/core';
2
import { HttpClient,HttpHeaders } from '@angular/common/http';
3
import { Observable } from 'rxjs';
4
import { tap } from 'rxjs/operators';
5
6
@Injectable({
7
  providedIn: 'root'
8
})
9
export class AuthService {
10
  private apiUrl = 'your_node_app_url';
11
  public token: string ='';
12
13
14
  constructor(private http: HttpClient) {
15
    
16
  }
17
18
19
  signin(username: string, password: string): Observable<any> {
20
    const data = { username, password };
21
    return this.http.post(`${this.apiUrl}/signin`, data);
22
  }
23
24
 
25
26
  authenticate(email: string, password: string): Observable<any> {
27
    const data = { email, password };
28
    console.log(data)
29
30
    return this.http.post(`${this.apiUrl}/authenticate`, data)
31
      .pipe(
32
        tap((response:any) => {
33
          this.token = response.data.token; // Store the received token

34
          localStorage.setItem('token',this.token)
35
          console.log(this.token)
36
        })
37
      );
38
  }
39
40
  profile(): Observable<any> {
41
    const headers = this.createHeaders();
42
    return this.http.get(`${this.apiUrl}/me`,{ headers });
43
  }
44
45
46
  private createHeaders(): HttpHeaders {
47
    let headers = new HttpHeaders({
48
      'Content-Type': 'application/json',
49
    });
50
51
    if (this.token) {
52
      headers = headers.append('Authorization', `Bearer ${this.token}`);
53
    }
54
55
    return headers;
56
  }
57
58
  logout(): void {
59
    
60
    localStorage.removeItem('token');
61
  }
62
 
63
  
64
}
65
66

In the authenticate() method, we send a POST request to the API and authenticate the user. From the response, we extract the token and store it in both the service's this.token property and the browser's localStorage, and then return the response as an Observable.

In the profile() method, we make a GET request by including the token in the Authorization header to fetch the user details. 

The createHeaders() method creates HTTP headers that include the authentication token when making authenticated API requests. It adds an Authorization header when the user has a valid token. The token allows the back-end API to authenticate the user.

If authentication is successful, the user token is stored in local storage for subsequent requests. The token is also made available to all components. If authentication fails, we display an error message. 

Do not forget to put the service URL in baseUrl in the above code. When you deploy your service to Heroku, you will get a service URL like appname.herokuapp.com. In the above code, you will set var baseUrl = "appname.herokuapp.com".

The logout function removes the token from local storage.

In the signup.component.ts file, We implement the signup () method, which gets the user-submitted email and password and creates a new user. 

1
import { Component } from '@angular/core';
2
import { AuthService } from '../auth.service';
3
4
5
6
@Component({
7
  selector: 'app-signup',
8
  templateUrl: './signup.component.html',
9
  styleUrls: ['./signup.component.css']
10
})
11
export class SignupComponent {
12
  password: string = '';
13
  email: string = '';
14
  
15
16
  constructor(private authService:AuthService){}
17
18
  signup(): void {
19
    this.authService.signin(this.email, this.password).subscribe(
20
      (response) => {
21
        // success response

22
        console.log('Authentication successful', response);
23
       
24
      },
25
      (error) => {
26
        // error response

27
        console.error('Authentication error', error);
28
      }
29
    );
30
  }
31
}
 
 The login.component.ts file will look similar to the signup component.
 
1
import { Component } from '@angular/core';
2
import { AuthService } from '../auth.service';
3
4
5
6
@Component({
7
  selector: 'app-login',
8
  templateUrl: './login.component.html',
9
  styleUrls: ['./login.component.css']
10
})
11
export class LoginComponent {
12
    
13
  email: string = '';
14
  password: string = '';
15
16
  constructor(private authService: AuthService) {}
17
18
  login(): void {
19
    this.authService.authenticate(this.email, this.password).subscribe(
20
      (response) => {
21
        // success response

22
        console.log('Signin successful', response);
23
       
24
      },
25
      (error) => {
26
        // error response

27
        console.error('Signin error', error);
28
      }
29
    );
30
  }
31
}

The profile component uses the user token to fetch the user's details. Whenever you make a request to a service in the back end, you need to put this token in the headers. The profile.component.ts looks like this:

1
import { Component } from '@angular/core';
2
import { AuthService } from '../auth.service';
3
@Component({
4
  selector: 'app-profile',
5
  templateUrl: './profile.component.html',
6
  styleUrls: ['./profile.component.css']
7
})
8
9
export class ProfileComponent {
10
  myDetails: any;
11
12
  constructor(private authService: AuthService) { }
13
14
  ngOnInit(): void {
15
    this.getProfileData();
16
  }
17
  getProfileData(): void {
18
    this.authService.me().subscribe(
19
      (response: any) => {
20
        this.myDetails = response;
21
        console.log('User Data:', this.myDetails);
22
      },
23
      (error: any) => {
24
        console.error('Error retrieving profile data');
25
      }
26
    );
27
  }
 

In the above code, every request is intercepted, and an authorization header and value are put in the headers. We then pass the user details to the profile.component.html template.

1
<h2>User profile </h2>
2
3
<div class="row">
4
    <div class="col-lg-12">
5
        <p>{{myDetails.data.id}}</p>
6
        <p>{{myDetails.data.email}}</p>
7
    </div>
8
</div>

Finally, we define the routing in app.routing.module.ts.

1
import { NgModule } from '@angular/core';
2
import { RouterModule, Routes } from '@angular/router';
3
import { LoginComponent } from './login/login.component';
4
import { ProfileComponent } from './profile/profile.component';
5
import { SignupComponent } from './signup/signup.component';
6
7
const routes: Routes = [
8
  {path:'signup' , component:SignupComponent},
9
  {path:'login' , component:LoginComponent},
10
  { path: 'profile', component: ProfileComponent },
11
];
12
13
@NgModule({
14
  imports: [RouterModule.forRoot(routes)],
15
  exports: [RouterModule]
16
})
17
export class AppRoutingModule { }
 

As you can easily understand in the above code, when you go to /, the app.component.html page will be rendered. Another example: if you go to /signup, signup.component.html will be rendered. This rendering operation will be done in the browser, not on the server-side.

Conclusion

Token-based authentication systems help you to construct an authentication/authorization system while you are developing client-independent services. By using this technology, you will just focus on your services (or APIs).

The authentication/authorization part will be handled by the token-based authentication system as a layer in front of your services. You can access and use services from any client like web browsers, Android, iOS, or a desktop client.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.