(514) 601-1122
joel.dorrington0@gmail.com
If you’re familiar with using Node.js, Express and MongoDB you’ll likely want to incorporate Angular into the mix and join the cool kids on the frontier of the web. One seriously frustrating thing to translate to single page applications, I found, was user authentication; at least it is when you rely on node packages.
This tutorial isn’t going to go into the details of creating the front end in Angular. We’re going to build a back-end API designed to be easily adaptable for any app you might be working on already. All you really need is a few forms that are set up to send user data to your server’s endpoints. I’ll show you a snippet of my Angular5 code as an example.
User authentication in single page applications generally requires the use of JSON web tokens. I recommend this blog post at medium.com for more information about jwts.
https://medium.com/vandium-software/5-easy-steps-to-understanding-json-web-tokens-jwt-1164c0adfcec
On our server, let’s install the dependencies we’ll need:
npm install express mongoose body-parser jwt-simple --save
First we need to set up the server to use express and configure mongoose. If you’re developing your front-end locally but building your server on a cloud based IDE, like me, your front end files are not served directly from your server so you’ll need to allow Cross Origin Resource Sharing as well.
It’s a good idea to use an environment variable to store the secret. It’s only in the source code here for simplicity.
var express = require('express'),
app = express(),
mongoose = require('mongoose'),
bodyParser = require('body-parser'),
jwt = require('jwt-simple'),
User = require('./models/user');
const secret = "Do not store your secret in the source code!"
// Configuring the app
mongoose.connect('mongodb://localhost/userauth');
mongoose.Promise = global.Promise;
app.use(bodyParser.urlencoded({extended: true}));
// Allowing CORS for development purposes only.
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "*");
res.header("Access-Control-Allow-Methods", "*");
next();
});
When you deploy your app, remove the code that allows CORS. It’s a security risk that you probably don’t want to accept in production.
Passport is a convenient NPM package which takes a lot of the tedious stuff about user auth off your plate. However, the abstraction removes the flexibility we need to accommodate a SPA, since the user session is stored in the client. The server knows nothing about the user session or app state.
I decided I had to re-imagine my usual design pattern and handle the authentication myself, and I’m glad I did.
Let’s create a user in the usual way with Mongoose. Define the User schema in user.js. Here we do something that was new to me: defining custom methods for User objects which set and check passwords.
var mongoose = require('mongoose'),
crypto = require('crypto'),
jwt = require('jwt-simple');
const secret = "Do not store your secret in the source code!";
var UserSchema = new mongoose.Schema({
email: {
type: String,
unique: true,
required: true
},
hash: String,
salt: String
});
UserSchema.methods.setPassword = function(password){
this.salt = crypto.randomBytes(16).toString('hex');
this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
return createToken(this._id);
};
UserSchema.methods.authPassword = function(password){
const hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
if(hash === this.hash){
return createToken(this._id);
}
return false;
};
function createToken(userId){
const payload = {
"uId": userId,
"exp": Math.floor(Date.now()/1000) + (60 * 30)
};
return jwt.encode(payload, secret);
}
module.exports = mongoose.model('User', UserSchema);
The setPassword method takes a string that was given on signup and creates the salt and hash values we can safely store in the database. It also returns a jwt – which I’ll explain a little later.
A little extra info:
The salt value is a random number that means nothing in particular. The hash is the result of combining the salt with the correct password and running it through a hash algorithm. This video on youtube https://www.youtube.com/watch?v=8ZtInClXe1Q by Computerphile explains the concept and reasoning simply. I recommend reading up about hashing if you’re unfamiliar with it. Libraries and packages are great, but there is no replacement for underpinning knowledge.
If you’re confused and curious about the crypto.pbkdf2Sync function and all it’s arguments, you can read it’s documentation here.
The authPassword function takes a password as an input, combines it with the salt value, runs it through the hashing algorithm and compares the result with the hash stored in the database, returning a token if it matches, or false if not.
Having done this, we’ve taken control of the user authentication process, giving us freedom to handle the post-authentication functionality as we please.
The next step is creating the jwt that the client will use to authenticate their requests. Definitely read the article on JSON Web Tokens I linked at the top of this page. Having a true understanding of jwts yeilds the ability to customise your them to your needs, while avoiding security risks.
To create jwts, we used the npm package jwt-simple. It handles the low level encoding, decoding and validation. In our jwt’s payload we’ve included the user’s id and an expiry date for the jwt. We passed those values in a javascript object into jwt.encode() along with the secret that our server uses to sign and validate jwts. Jwt-simple handles the rest.
Here’s the snippet we wrote in user.js again:
function createToken(userId){
const payload = {
"uId": userId,
"exp": Math.floor(Date.now()/1000) + (60 * 30) // This evaluates to 30 minutes from the current time.
};
return jwt.encode(payload, secret);
}
You NEVER ever want to release an immortal jwt, it’s a big security risk. Always give them a finite lifespan. We’ve set our jwts to expire after half an hour with the line:
exp: Math.floor(Date.now()/1000) + (60 * 30)
Jwt-simple checks to make sure it’s not expired automatically.
The function to create jwts is a private function inside the user.js file, so it cannot be executed without calling the setPassword or authPassword methods on a user object, as an added layer of security.
On our register and log in routes we’re going to send that jwt back to the client, in my case an angular app.
// Register route
app.post('/register', function(req, res){
User.create({email: req.body.email})
.then((user) => {
const token = user.setPassword(req.body.password);
user.save();
const response = {
user: {
email: user.email,
id: user._id
},
access_token: token
};
res.status(200).json(response);
})
.catch((err) => {
console.log(err);
res.status(400).send(err);
});
});
// Login route
app.post("/login", function(req, res){
User.findOne({email: req.body.email})
.then((user) => {
let token;
if(user){
token = user.authPassword(req.body.password);
}
if(token){
delete user.hash;
delete user.salt;
const response = {
user: user,
access_token: token
};
res.status(200).json(response);
} else {
res.status(401).send({"message": "incorrect email or password"});
}
});
});
In both routes, we store the token returned by the user methods in a variable we aptly named “token”.
Regardless of your framework, in the client JavaScript, you need to JSON.parse() the response body and save the token into a local variable. Then every time you’re making a request, send the jwt as a query parameter by adding “?auth=”+this.token to the end of your http path.
It’s beyond the scope of this article to thoroughly explain the process of making ajax requests, but here are the functions in my Angular5 code that send the requests to the server.
logIn(email: string, password: string){
const headers = new Headers({"content-type": "application/x-www-form-urlencoded"})
const body = 'email='+email+'&password='+password;
this.http.post(this.hostUrl + 'login', body, {headers: headers})
.subscribe((res) => {
const data = JSON.parse(res['_body']);
this.token = data.access_token;
});
}
checkToken(){
this.http.get(this.hostUrl+'secret?auth='+this.token) // results in sending a request to 'http://serverdomain/secret?auth=[token as a string]'
.subscribe((res) => {
console.log(res);
});
}
Next we’ll create a middleware function to parse the jwt out of the request, then decode and verify the signature. Once it’s decoded into a javascript object we’ll add it onto the request object as req.token so we can easily access the user id when we need it.
The jwt.decode function is the reverse of encode. It takes the jwt and the secret as arguments. It checks the signature, decodes the token into a JavaScript object if it’s authentic, and checks the exp property to make sure the token isn’t expired. If everything goes to plan it returns the token as a JavaScript object. If it has an invalid signature, or if it’s expired it will throw an error. For this reason we use a try-catch block to handle the error.
// Middleware
function checkToken(req, res, next){
if(req.query.auth){
try {
req.token = jwt.decode(req.query.auth, secret);
next();
}
catch(err) {
res.status(400).send("Token invalid. Please log in.");
}
} else {
res.status(400).send("No token found.");
}
}
Thats it! Now when you need to verify a user’s identity on the server, just call the middleware function checkToken and you’ll have access to the user’s id in req.token.uId. Then you can use that with all our favourite mongoose methods, for example:
User.findById(req.token.uId, function(user){})
app.get('/secret', checkToken, function(req, res){
res.send({message: "Token valid!", token: req.token});
});
// Now we just kick off the server. Make sure this is the last line in app.js
app.listen(process.env.PORT, process.env.HOST, function(){ // Configure your host and port options here as you need.
console.log('The app is listening...');
});
Now when you run app.js, your server should be ready to receive requests from any client running your app, and create and authenticate users by sending and receiving jwts.
If you’re like me, before reaching this article you may have tried to stitch a bunch of libraries together to avoid learning the scary encryption stuff. I hope this tutorial helped hack through the vines of user authentication in SPA’s for you and proved to you that reading up on your theory and learning the underpinning knowledge will turn your mountains into molehills.