My HP

30th September, 2021 | Tutorials |

Node.js Authentication – A Complete Guide with Passport and JWT

Looking for Node.js hosting? Check out our optimised packages.

Get Node.js Hosting

Truth be told, it's difficult for a web application that doesn't have some kind of identification, even if you don't see it as a security measure in and of itself. The Internet is a kind of lawless land, and even on free services like Google's, authentication ensures that abuses will be avoided or at least controlled.

That said, today we'll look at how to authenticate web applications with Node.js using Passport and web API authentication with JWT.

Authentication in Node.js with Passport

The Web Application

This is going to be long, so to save time here, I'll use the express-generator to create the application for us. Run the commands below and be happy (you will need admin permission):

npm i -g express-generator
express -e --git nodejs-passport-login
cd nodejs-passport-login
npm i

With that, we will have a ready-made Express application using EJS as a view-engine and with a route (and view) index and another user.

The first step of preparing our application to have authentication: create a login screen!

Create a new view in your application called views/login.ejs with the following HTML inside:

<!doctype html>
<html>
  <body>
    <h1>Login</h1>
    <form action="/login" method="POST">
      <input name="username" type="text" /><br />
      <input name="password" type="password" /><br />
      <input type="submit" value="Login" />
    </form>
    <% if(message) { %>
      <p>
        <label style="color:Red"><%= message %></label>
      </p>
    <%}%>
    <p>
      <a href="/users/forgot">Forgot your password?</a> | <a href="/users/signup">Have not registred?</a>
    </p>
  </body>
</html>

Note that I left some server code checking for a message in the model, to render an error label down there. We will use this later to warn of authentication errors. I also left two links, one for password recovery and another for registration, and later we will create these two screens and how they work.

Now create a new route file called routes/login.js and add the code below so that it renders our login view.

const express = require('express');
const router = express.Router();

/* GET login page. */ router.get('/', (req, res, next) => { res.render('login', { title: 'Login', message: null }); });

module.exports = router;

And in your app.js, add a call to this route file. We will improve several points throughout the tutorial, but run your application and go to localhost:3000/login and you will see the login screen. Obviously, this doesn't inhibit anyone from accessing internal pages via the browser's URL, but one problem at a time, please! We will program its operation later on.

Choosing Authentication Technology

In this tutorial, I will use the Passport module, which is the default when it comes to authentication in Express, the web framework which is the default when it comes to web apps in Node.js, the platform which is the default when it comes to… (and so on).

However, a Passport is far from being a silver bullet when it comes to security in its applications. Passport is just a security middleware that requires developers to know what they're doing and to program security however they want, being very flexible, extensible, and in common use on the market.

In this tutorial, I will try to be as professional as possible in terms of security considering that many readers tend to use the codes I provide/teach on their systems, however, it is worth noting to always be critical and skeptical about Internet codes, especially those that they can damage your system as the security layer.

Other authentication options exist a lot on the Internet, mainly those based on web standards and through APIs, which often exempt you from the responsibility of a series of bureaucratic tasks of having authentication in your application, such as authenticating, storing passwords, registering users, password recovery, etc. Names like Google Account, Microsoft Account, Facebook Authentication, Auth0, and even Firebase have solutions for this, not to mention Github, Twitter and the list goes on. They are all excellent options but they eventually lock you in some issues and make you dependent on the companies in question.

Therefore, I will choose Passport here not because it is the best authentication solution on the market, but because it is open, extensible, flexible, etc. And for having a good market adhesion when it comes to Node.js.

Programming Your Authentication Strategy

Now we can define our authentication strategy. Let's do this in a new file called auth.js at the root of our project.

Remember when I said Passport was cool because it was extensible? The authentication strategy is completely configurable, from third-party standards to open standards like OAuth or the classic username/password. Here we will install the passport-local module, which defines as a strategy the use of the username and password stored in our own bank to work (classic username/password):

npm i passport-local bcryptjs

And we will also install the bcryptjs module to keep our passwords secure in the database. Storing password hashes instead of plain text in the database is a minimal policy for information security.

Now let's go back to our auth.js and load these two modules, besides defining our module.exports which will return a passport function (which is the authentication middleware) that we will create later on:

const bcrypt = require('bcryptjs');
const LocalStrategy = require('passport-local').Strategy;

const users = [{ _id: 1, username: "adm", password: "$2a$06$HT.EmXYUUhNo3UQMl9APmeC0SwoGsx7FtMoAWdzGicZJ4wR1J8alW", email: "contact@xyz.com" }];

module.exports = function(passport){ //we will configure the passport here }

Note that I created an array of users and that I have already left a user created, with id 1, username “adm”, password hash “123” and my email that may be useful in the future. Also, notice the comment there inside the exported function: that's where we're going to work now.

First, create the exported function inside there, another two user search functions in our database, which for now will be an in-memory array. We need both to make our authentication work, the first function searches and returns a user by username. The second search and returns by id:

module.exports = function(passport){

function findUser(username){ return users.find(user => user.username === username); }

function findUserById(id){ return users.find(user => user._id === id); } }

For security and convenience reasons at the same time, the authentication cookie (which I'll talk about later) will not contain session information, they will remain in the database (our array for now) and we will only have the id locally on the clients. So, we need to configure the user serialization and deserialization functions still within that module's export space:

passport.serializeUser((user, done) => {
        done(null, user._id);
    });

passport.deserializeUser((id, done) => { try { const user = findUserById(id); done(null, user); } catch (err) { done(err, null); } });

The function done is “native” to the passport and serves to signal an error and pass information from the user to the passport. In the serializeUser function, we pass the primary key to be serialized in the user's cookie. In deserializeUser, we receive the id and go with it in the database to return the entire user.

And finally, let's define the authentication strategy itself, called LocalStrategy, at the end of the module.exports space:

passport.use(new LocalStrategy({
    usernameField: 'username',
    passwordField: 'password'
},
    (username, password, done) => {
        try {
            const user = findUser(username);

// non-existent user if (!user) { return done(null, false) }

const isValid = bcrypt.compareSync(password, user.password); if (!isValid) return done(null, false)

return done(null, user) } catch (err) { done(err, false); } } ));

Let's go by parts here because it's a lot:

With that, we finish our module with the authentication strategy. A sturdier version of this module could count the number of wrong attempts to ask the user to solve a Captcha, block the user, warn the user when a new IP login happens, etc.

Setting Up the Project for Authentication

Basically, when a user enters your webapp homepage, he is not authenticated, he is starting an anonymous session. In order for him to be able to enter the secure pages of your webapp, he must be authenticated, in an authenticated session.

This authentication takes place during the login process, where using valid credentials (username and password, for example), our user's session receives a cookie identifying him. In other accesses, this cookie is checked to grant or not an authorization to navigate between pages.

The use of cookies and sessions is the most common way to ensure that a user is authenticated on web pages and is what we will use here. Before messing with them, let's add the passport module in our project, with the command below (curious as we've talked a lot about it but only now added it to the project):

npm i passport express-session

To handle the session of web applications made using Express we will use the express-session module, which we installed along with the previous command.

For a more scalable and professional approach, our session cookie will not store all session data, but only your ID, while the rest of the data will be in our database/array.

express-session allows you to store your session data in different persistence mechanisms, as long as you use the appropriate connectors. By default, it saves to memory anyway, but in future examples, I can show the use of these connectors

Now let's call some of the dependencies in our app.js:

const passport = require('passport');
const session = require('express-session');

This just declares and starts our newly installed modules. Now let's add these lines to use them in app.js, right before the app.use of your routes:

require('./auth')(passport);
app.use(session({  
  secret: '123',
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 30 * 60 * 1000 }//30min
}))
app.use(passport.initialize());
app.use(passport.session());

Here I am loading our auth.js module passing the passport object to it to configure the authentication strategy. Then I tell express-session which secret it will use for some internal encryption and how long the cookie will live (and hence the session).

The resave and saveUnitialized variables are additional settings that tell if the session should be saved on all requests (resave) and if even anonymous users should have a session saved (saveUnitialized).

And with that, we leave our application 'ready' to receive authentication, although if you run this project now, there is no security at all yet.

Making the Login Work

With that, we can now go back to the login.js file that will receive the posts from the login screen and will carry out the username and password verification with the passport (which will use the strategy we configured earlier). Remember that any system with minimal security must be using SSL so that the data posted by the form is not easily captured and read over the network.

First, let's load the passport into login.js, in the beginning, and let's change the GET route of this file and add one more POST route:

const express = require('express');
const router = express.Router();
const passport = require('passport');

/* GET login page. */ router.get('/', (req, res, next) => { if (req.query.fail) res.render('login', { message: 'Incorrect username or password!' }); else res.render('login', { message: null }); });

/* POST login page */ router.post('/', passport.authenticate('local', { successRedirect: '/', failureRedirect: '/login?fail=true' }) );

module.exports = router;

The first is a GET /login that returns an appropriate message depending on whether or not the error flag in the querystring exists. You can fine-tune this behavior for different messages according to different error codes or the like.

The second function captures POST /login requests passing to the passport the task of authenticating using the local strategy. The configuration object passed as the second parameter of the authenticate function defines the success and failure URLs, according to the result defined by the passport. Note that there the success redirects to '/' (index), change to the appropriate path of your application.

Back to app.js, let's add a function that will be very useful to not let anonymous users access the application's internal pages:

function authenticationMiddleware(req, res, next) {
  if (req.isAuthenticated()) return next();
  res.redirect('/login?fail=true');
}

We will call this authenticationMiddleware function every time a request requests a non-public page (such as a login). Basically, it checks if the request is authenticated (isAuthenticated), otherwise, it throws the anonymous user to the login screen with the failure flag.

Now, we just need to ensure that no anonymous users can access the system's splash screen, using this function that we just created as middleware between requests for our application's private screens. In the app.js file, we must also change the access route to the homepage and users as below (change it according to your application, but always let login first):

app.use('/login', loginRouter);
app.use('/users', authenticationMiddleware, usersRouter);
app.use('/', authenticationMiddleware,  indexRouter);

For now, you can register users manually (or use the adm/123 we pre-registered before) using some online bcrypt tool to generate the password hash.

Then test your login screen to see if it accepts your credentials, if it redirects correctly, try accessing the chat screen (or the private screen you set) without logging in first, or even lower your session TTL to see if it expires on its own and sends you back to login.

JSON Web Token (JWT) Authentication in Node.js

JSON Web Tokens

JWT, in short, is a string of characters that, if client and server are on HTTPS, allows that only the server that knows the 'secret' can validate the content of the token and thus confirm the authenticity of the client. The token is not “encrypted”, but “signed”, so that only with the secret this signature can be verified, which prevents attackers from “creating” tokens on their own.

In practical terms, when a user authenticates to the system or web API (with username and password), the server generates a token with an expiration date for him. During subsequent client requests, the JWT is sent in the request header and, if valid, the API will allow access to the requested resources, without the need to authenticate again.

JWT content is a JSON payload that can contain any information you want, which allows you to later grant authorization to certain resources to certain users. Minimally it will have the authenticated user or session ID (if you are working with this concept), but it can contain much more than that, as you need it, although storing “sensitive” content inside is not a good idea as I said before, it is not encrypted.

Structuring the API

Before we start this Node.js API using JWT it is worth noting that the focus here is to show how JWT works and not how a real API works. If you already have a web API, skip this section. Otherwise, use the fake API below, which we will mock the data returned by the API and the initial authentication credentials to go straight to the generation and subsequent verification of the tokens.

Save the following JavaScript code to a /jwt/index.js file:

//index.js
const http = require('http');
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());

app.get('/', (req, res, next) => { res.json({message: "Everything is okay!"}); })

app.get('/clientes', (req, res, next) => { console.log("Returned all customers!"); res.json([{id:1,nome:'luiz'}]); })

const server = http.createServer(app); server.listen(3000); console.log("server listening on port 3000...")

With this JS in hand, let's install some dependencies in our application to make it work:

npm i express body-parser

Run the API with “node index” and when accessing localhost:3000 it should only list an OK message and when accessing the /clients path in the browser, it should list a JSON array. This shows that the API is working on both routes and there is no security, after all, we didn't have to authenticate to do the GETs we did.

Adding the JWT

Now, let's install two new dependencies to improve our project, allowing us to add authentication via JWT:

npm i jsonwebtoken dotenv-safe

To know:

Let's start using dotenv-safe, creating two files. First, the .env.example file, with the template environment variables we'll need:

# .env.example, commit to repo
SECRET=

And then the .env file, with the secret value of your choice:

#.env, don't commit to repo
SECRET=mysecret

This secret will be used by the jsonwebtoken library to sign the token so that only the server can validate it, so it is good manners that it be a strong secret.

In order for this environment variable file to be loaded as soon as the application starts, add the following line right at the beginning of your API's index.js file, taking the opportunity to also insert the lines of the new packages that we are going to work on:

require("dotenv-safe").config();
const jwt = require('jsonwebtoken');

This leaves our API minimally prepared to actually handle authentication and authorization.

Authentication

In case you don't know the difference, authentication is about proving that you are yourself. Authorization is proving that you have permission to do or see what you are trying to do.

Before generating the JWT, the user must go through traditional authentication, usually with a username and password. This information provided is validated against a database and only if it is ok we generate the JWT for it.

So, let's create a new /login route that will receive a hypothetical username and password and, if ok, will return a JWT to the client:

//authentication
app.post('/login', (req, res, next) => {
    if(req.body.user === 'abc' && req.body.password === '123'){
      //auth ok
      const id = 1; //this id would come from the database
      const token = jwt.sign({ id }, process.env.SECRET, {
        expiresIn: 300 // expires in 5min
      });
      return res.json({ auth: true, token: token });
    }

res.status(500).json({message: 'Invalid Login!'}); })

Here we have the following scenario: the client posts a user and a pwd in the URL /login, which simulates a trip to the bank merely checking if a user is equal to “abc” and if pwd is equal to “123”. If ok, the bank would return the ID of this user, which I simulated with a constant.

This ID is being used as the payload of the JWT being signed, but it could have more information as you need it. In addition to the payload, the SECRET is passed, which is stored in an environment variable as required by good security practices. Finally, I added a 5-minute expiration for this token, which means that the authenticated user can make their requests for 5 minutes before the system or Web API asks them to authenticate again.

If the user and password do not match, an error will be returned to the user.

Let's take advantage of the momentum and let's create a route for the logout:

app.post('/logout', function(req, res) {
    res.json({ auth: false, token: null });
})

Here we only tell the requester to nullify the token, although this logout route is completely optional since on the client-side it is possible to destroy the authentication cookie or local storage and with that, the user is automatically logged out. If neither the client-side nor the server-side destroys the token, it will expire on its own in 5 minutes.

If you want to add an extra layer of security, you can have a blacklist of tokens that are logged out, to prevent reuse of them after they log out. Just remember to clear this list after they expire or use a Redis cache with automatic expiration.

Authorisation

Let's create a verification function in our index.js, in order to, given an incoming request, we check if it has a valid JWT:

function verifyJWT(req, res, next){
    const token = req.headers['x-access-token'];
    if (!token) return res.status(401).json({ auth: false, message: 'No token provided.' });

jwt.verify(token, process.env.SECRET, function(err, decoded) { if (err) return res.status(500).json({ auth: false, message: 'Failed to authenticate token.' });

// if everything is ok, save to request for later use req.userId = decoded.id; next(); }); }

Here I got the token from the x-access-token header, which if it doesn't exist, generates an error right away.

If it exists, we verify the authenticity of this token using the verify function, using the environment variable with SECRET. If it fails to verify the token, it will generate an error. Then we call the next function which moves to the next stage of execution of the functions in the Express middleware pipeline, but not before saving the user id information for the request, in order to be used by the next stage.

Just insert your reference in the GET /clients calls that already existed in our API:

app.get('/clientes', verifyJWT, (req, res, next) => {
    console.log("Returned all customers!");
    res.json([{id:1,nome:'luiz'}]);
})

Thus, before answering client GETs, the API will create this middle layer of authorisation based on JWT, which will obviously block requests that are not authenticated and authorized, according to its rules for this.

Note that I didn't say to put this security middleware in the GET/call because we're going to make it public, always accessible even to anonymous users.

But back to our clients’ route, in order to access it, the API client must first get a token by authenticating with a valid user in the POST login route. This request will have to be done using POSTMAN, Insomnia, or similar (cURL, etc).

In the application that will consume your web API, this generated token must be collected and all requests to the protected endpoints must be made passing it in the request header. To simulate this, we will use POSTMAN again, passing the JWT content in the x-access-token header, and finally, as this token is scheduled to expire 5 minutes after its creation, requests can be made using it during this time, without a new login but as soon as it expires, we will receive it in return.

And with that, I close today's tutorial. Note that I focused on using the JWT, without going into too much detail on how you can structure your authentication from the database (login and password table/collection) nor how you can structure your authorisation model (access profiles, for example).

Looking for Node.js hosting? Check out our optimised packages.

Get Node.js Hosting

Comments