NodeJS Authentication with Password and JWT in Express

Authentication is the basis of any user facing, non-trivial application. It forms the primary boundary to things like enforcing user permissions in addition to logging a user into your application.

Just as a note, we will only be going over the back end of the equation. We won’t be going into how to connect this process to a front end.

The problem is this: there are so many different tutorials out there for NodeJS authentication using passport, but none of them provide a clear consistent method of authentication. They also don’t seem to go further than just demonstrating the steps. There are also a ton of different opinions and advice about authentication that can be pretty daunting for the self-taught web developer barreling through a large set of curriculum already.

So, let’s start by mapping out what we want in an authentication system. First, we will want to be able to have people register for an account. This will require some sort of personal identifier. Either a username or an email would do us well, and actually it would be beneficial to have both! We want an email address for the purpose of contacting the user later in other parts of our app, and a username in addition to an email address allows us to use the username as a public ID for our users in the app without compromising their email address which would inundate the user with spam. Second, to log-in we would need to have a password or some sort of challenge for users signing in to pass in order to be Authorized for the application. Let’s take a few notes:

Requirements:
1. User Identifier - User Name.
2. Email Address - for contact purposes only.
3. Password - challenge part of authentication.

I believe that in the common terminology, Authentication is the act of challenging a user’s sign-in attempt. Authorization is the reward for passing the challenge. That’s the most basic concept. Also, we need to salt and hash the password when we save it to the database so that it’s not stored in a recoverable way. When we compare the user’s entered password to the stored one, we will use methods that actually salt and hash the entered password to compare to the stored password.

Process 1: AuthenticationRequired parameters: Username, Password.1. Grab User from Database.
2. Salt/Hash Entered password from User
3. Compare Entered vs. Stored passwords.
3.1 if password hashes match, generate a token and send to the user
3.2 if password hashes don't match, return an error.

We now have a process for the Authentication challenge. Let’s take a second to also write out our procedure for registering a user. We’ll need the user’s email address, user name, their desired password, and a confirmation of that password. First, we’ll want to check to see if the user already exists in the database then compare the two password fields to make sure they match each other. Then we’ll salt and hash the password field to store in the database. Let’s document this process quickly.

Process 1: RegistrationRequired Parameters: Username, Email, Password, Password21. Check the database for an existing user by Email and Username.
1.1 If there is not a user already, move on.
1.2 If there is a user already, return an error.
2. Compare the password fields.
2.1 If they match, move on.
2.2 If they don't, return an error
3. Salt and Hash the password
4. Save User to Database.
5. Return success to the client.

Now that we have our procedures listed out with some basic steps, let’s take a look at tokens, what they are, and why they’re desirable in Authorization. A token, in the simplest sense is a claim that the bearer, or owner of the token, has the right to access a resource.

The token is Base64 encoded JSON object that securley transfers information between services. The token can be signed by synchronous methods such as a secret string that is used as the encryption key with the HMAC algorithm, or asynchronously with private and public key pairs with RSA or ECDSA. Synchronous simply means that anyone that needs to verify the token is valid, must do so with the same key. In Asynchronous signing, the Authorizing Entity, our Authentication server, signs the token with its private key, and the Client Entity verifies the signature of the token with the public key.

The base token itself has three components: the header, the payload, and the signature. The header usually only has two parts, a type that describes the type of token which will always be JWT, and an algorithm that defines the type of encryption used to sign the token. This is where we will define whether we want to use HMAC’s SHA265 algorithm for synchronous signing, or RSA signing in asynchronous.

The payload of the token is all the information that needs to be sent to the client application. This can be the information relating to our user. This information is setup in the form of “Claims” these claims can be either Registered, Public, or Private. Registered Claims are those that are predefined by the JWT standard. Some simple ones are iss or issuer, exp or expiration, sub or subject. (To get in deeper with the JWT Registered claims, check here.)

The next type of claims are public claims. These are defined at will by JWT users, but should be defined here. Alternatively, you can define your public claims in your own namespace, but that is beyond the scope of this article.

Finally, we get to have private claims, which is where you would want to place your information for your client application. A private claim is simply one that two parties agree to, and requires no setup, and can be just used. This is how we’ll get our specific application information to the client. Let’s just quickly review the Claim Types:

  1. Registered Claims: Registered Claims that are provided to basic, consistent information within the token.

Just as a note, please do not put any data that can be considered sensitive in the JWT. Decoding a JWT from Base64 is trivial. Treat the token as clear text. The purpose of signing the token is not to mask its contents, but to verify that the signing party believes the claims of the token to be true. AGAIN DO NOT PLACE SENSITIVE INFORMATION IN THE TOKEN.

Now that we have a basic understanding of the processes we will have to code and the claims we need or want to provide to the client process, we should really dive into some code to see how this could be implemented! CODE TIME!

First let’s have our basic express app with a mongoose model for the user.

//index or server.jsconst express = require('express');
const cp = require('cookie-parser');
const bp = require('body-parser');
require('dotenv').config();
//reads in configuration from a .env file
const mongoose = require('mongoose');
const passport = require('passport');
const app = express();const port = process.env.PORT || 3000;
const dbPort = process.env.DB_PORT || 27017;
const dbUrl = process.env.DB_URL || "localhost";
const dbCollection = process.env.DB_COLLECTION || "auth-test";
//sets the required variables from Environment Variables.
mongoose.set('useCreateIndex', true);
//fixes an issue with a depricated default in Mongoose.js
mongoose.connect(`mongodb://${dbUrl}/${dbCollection}`, {useNewUrlParser: true})
.then(_ => console.log('Connected Successfully to MongoDB');)
.catch(err => console.error(err););
app.use(passport.initialize());
//initializes the passport configuration.
require('./passport-config')(passport);
//imports our configuration file which holds our verification callbacks and things like the secret for signing.
app.use(cp());
app.use(bp.urlencoded({extended: false});
app.use(bp.json());
//custom Middleware for logging the each request going to the API
app.use((req,res,next) => {
if (req.body) log.info(req.body);
if (req.params) log.info(req.params);
if(req.query) log.info(req.query);
log.info(`Received a ${req.method} request from ${req.ip} for ${req.url}`);
next();
});
app.use('/users', require('./routes/user');
//registers our authentication routes with Express.
app.listen(port, err => {
if(err) console.error(err);
console.log(`Listening for Requests on port: ${port}`);
});

Okay! That was some work, and let’s break it down. We’re bringing in Express, Mongoose, Passport and some other standard libraries in order to get express working. There are four things I would like to discuss about this code, and here they are:

  1. require('dotenv').config() is a tool that I commonly use in projects to provide Environment Variables accessed via the process.env object. These variables are ones that should be different across instances of an application. These are also things that you wouldn’t want to check into source control. Our Secret for signing tokens is one of them!

Now, let’s take a crack at that User Model!

// models/user.jsconst mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UserSchema = new Schema({
name: String,
emailAddress:{
type: String,
required: true,
unique: true,
},
userName: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
});
module.exports = User = mongoose.model('User', UserSchema);

This get’s our user model together with a name, email address, user name, and a password. The Email and User Name are required to be unique, the real name of the user isn’t all that important and we’ve used a shorthand to simply state it’s supposed to be a string. The password is going to be the interesting part, but we’ll dive into that in just a bit. Let’s take a look at that passport config file.

//passport-config.js
//Let's import some things!
const {Strategy, ExtractJwt} = require('passport-jwt');//this is using ES6 Destructuring. If you're not using a build step,
//this could cause issues and is equivalent to
// const pp-jwt = require('passport-jwt');
// const Strategy = pp-jwt.Strategy;
// const ExtractJwt = pp-jwt.ExtractJwt;
require('dotenv').config();const secret = process.env.SECRET || 'some other secret as default';const mongoose = require('mongoose');const User = require('./models/user');const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: secret
};
//this sets how we handle tokens coming from the requests that come
// and also defines the key to be used when verifying the token.
module.exports = passport => {
passport.use(
new Strategy(opts, (payload, done) => {
User.findById(payload.id)
.then(user => {
if(user){
return done(null, {
id: user.id,
name: user.name,
email: user.email,
});
}
return done(null, false);
}).catch(err => console.error(err));
});
);
};

The function checks if the user is a legitimate user in the database once the token is decoded. If the user is real, it passes the user to done, otherwise done will return false.

Now, let’s take a look at our authentication routes!

// routes/user.jsconst router = require('express').Router()//Initializes an instance of the Router class.const User = require('../models/user');
const bcrypt = require('bcryptjs');
//imports the user model and the BcryptJS Library
// BcryptJS is a no setup encryption tool
require('dotenv').config();
const secret = process.env.SECRET || 'the default secret';
//gives us access to our environment variables
//and sets the secret object.
const passport = require('passport');
const jwt = require('jsonwebtoken');
//imports Passport and the JsonWebToken library for some utilitiesrouter.post('/register', (req,res) => {
User.findOne({emailAddress: req.body.emailAddress})
.then(user => {
if(user){
let error = 'Email Address Exists in Database.';
return res.status(400).json(error);
} else {
const newUser = new User({
name: req.body.name,
emailAddress: req.body.emailAddress,
password: req.body.password
});
bcrypt.genSalt(10, (err, salt) => {
if(err) throw err;
bcrypt.hash(newUser.password, salt,
(err, hash) => {
if(err) throw err;
newUser.password = hash;
newUser.save().then(user => res.json(user))
.catch(err => res.status(400).json(err));
});
});
}
});
});

This first function takes in the request, checks the database against the email address, and then if it’s found, returns an error, and if not, returns the new user as stored in the database. Nice!

// routes/user.js -continued...router.post('/login', (req,res) => {
const email = req.body.email;
const password = req.body.password;
User.findOne({ email })
.then(user => {
if (!user) {
errors.email = "No Account Found";
return res.status(404).json(errors);
}
bcrypt.compare(password, user.password)
.then(isMatch => {
if (isMatch) {
const payload = {
id: user._id,
name: user.userName
};
jwt.sign(payload, secret, { expiresIn: 36000 },
(err, token) => {
if (err) res.status(500)
.json({ error: "Error signing token",
raw: err });
res.json({
success: true,
token: `Bearer ${token}` });
});
} else {
errors.password = "Password is incorrect";
res.status(400).json(errors);
}
});
});
});

Okay, and this is where the magic happens! We verify the user’s password, and then if successfully matched, we return the token. Now, let’s talk a little bit about how to verify the token, and protect some routes.

First, we will need a mechanism for triggering passport on a route. For this, we’ll simply add some middle ware from passport on the route, or route import. For example, if we want to protect a certain route, we would define the route as this:

router.method('/path', passport.authenticate('jwt', {session: false}), (req,res) => ... );

This informs passport that we’re using the JWT strategy we provided, and that we should not keep any session open longer than it takes to return the data. Another option would be to protect a group of routes as imported in our server or index.js like this:

app.use('/path', passport.authenticate('jwt', {session:false}),require('path/to/route/file'));

And that’s it! a basic authentication procedure, and some basic theory!

If you’ve made it this far, congratulations. Authentication in JavaScript isn’t as easy as in some languages, and there really should be better, more defined ways of doing things. Security is one of those things that it pays to follow facts and best practices.

Feel like I’ve simply glossed over something? Want to start a discussion about JavaScript? Just want to chat? Be sure to message me on Social Media, and have a lot of fun!

Learning React/Redux, and trying to make full stack apps with MERN. Learning Data Analytics at WGU, and monitoring databases for Alcoholics Anonymous.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store