Authentication and Authorization using Redis

Pankaj Panigrahi
Better Programming
Published in
14 min readMar 4, 2019

--

This article is the 6th of the article series which will help you grasp different concepts behind Node.js and will empower you to create production ready applications. This article expects the reader to know babel and how to set it up. Please read this article if you need to know the same.

When you are building the backend for your website/web-app or a generic backend which will act as the backend for both mobile and web apps, you need to protect your apis with an authentication layer. These apis should only be accessed by logged in user and should return result based on the user.

In this modern era, everyone is going for token based authentication instead of session based cookies. OAuth , JWT and Random Token With memcached / redis are the most popular forms of token based authentication.
There is a nice spreadsheet which compares some of the common authentication methods:

Disclaimer: Spreadsheet has been written by some other author.

Both JWT and Random Token With in-memory storage have their advantages and disadvantages. You should use them according to the project’s requirements. You can find the below article comparing both of them:

When it comes to redis and memcached, i prefer redis for the following reasons:

a) More data types support

b) Default disk persistence

c) Larger key value size

You can read more in the article below:

https://www.linkedin.com/pulse/memcached-vs-redis-which-one-pick-ranjeet-vimal/

Now that we have decided to go for token based authentication with the help of redis, let’s start.

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps etc. Two of the most popular usages of redis are building a caching layer or act as a session storage system.

Please follow the following articles to get started with redis.

In this article, we will be using a local installation of redis. But you can also get a free server from redis labs.

Let us start from where we left in the article #5. It is recommended that you read the 5th article before reading this article. But if you have some experience with node.js and can find your own way through the code, you can start with the boilerplate below:

You can find the code here:

https://github.com/pankaj805/medium-05_mongo_client

We will be using the following npm module as redis client library in our project

So, let us install the following libraries in our project first.

npm install redis --save

and another library to create random unique ids

npm install node-uuid --save

In this tutorial, we will build two apis: The first api is “login” api, which should return a new session token and other api would be to update password which will need a valid session token of a logged in user.

let us first add a service method to update the user’s password

export const updateUserPassword = (db, userName,pwd) => {
return db.collection('user').updateOne({'username': userName }, {
$set: {password:pwd}
})
.then((r) => {
return Promise.resolve(r.matchedCount);
})
.catch((err) => {
return Promise.reject(err);
})
}

So finally services/UserService.js should look like this:

export const getUserDetails = (db, userName) => {
return new Promise((resolve, reject) =>
db.collection('user')
.find({ 'username': userName })
.toArray((err, docs) => {
if(docs && docs.length>0){
resolve(docs[0]);
}else{
reject();
}
});
});
}
export const updateUserPassword = (db, userName,pwd) => {
return db.collection('user').updateOne({'username': userName }, {
$set: {password:pwd}
})
.then((r) => {
return Promise.resolve(r.matchedCount);
})
.catch((err) => {
return Promise.reject(err);
})
}

Let us create a class which we will use to store the session data. Let us create a file inside the common folder.

class Session {
constructor() {
this.userData = {};
this.sessionID = '';
}

set(obj) {
this.userData = obj;
}
save(client){
if(this.sessionID){
client.set(this.sessionID, JSON.stringify(this.userData));
client.expire(this.sessionID, 60 * 60 * 2);
}
}
destroy(client) {
client.del(this.sessionID);
}
}
export default Session;

This file is pretty self-explanatory except the save and destroy method. The save method is passed one argument which should be the reference to the redis client instance.

client.set(this.sessionID, JSON.stringify(this.userData));

The above line is used to update the redis key-value pair with the user data.

client.expire(this.sessionID, 60 * 60 * 2);

and client.del() is to remove a certain key-value pair from redis.

Now let us edit our common/authUtils.js file to handle the desired scenarios.

import uuid from 'node-uuid';const newSessionRoutes = [{ path: '/user/login', method: 'POST' }];const authRoutes = [{ path: '/user/password', method: 'PUT' }];

Import the uuid module and define two constants, the first one containing array of the api methods which need new session and the later containing array of api methods which need valid session.

export const isNewSessionRequired = (httpMethod, url) => {
for (let routeObj of newSessionRoutes) {
if (routeObj.method === httpMethod && routeObj.path === url) {
return true;
}
}
return false;
}
export const isAuthRequired = (httpMethod, url) => {
for (let routeObj of authRoutes) {
if (routeObj.method === httpMethod && routeObj.path === url) {
return true;
}
}
return false;
}

Add above two methods to check if the passed api needs new session or old session.

export const generateRandomSessionID = () => {
return uuid.v4();
}

Add above method to create random uuid string. We will use it to create a unique key for our redis key-value pair.

We also need a method to read the value from redis server. According to the documentation we need to do the following:

client.get("redis-key", function (err, reply) {
console.log(reply.toString());
});

Let us define a method which encapsulates the above method inside a promise:

export const getRedisSessionData = (redisClient,sessionId) => {
return new Promise((resolve, reject) => {
redisClient.get(sessionId, function (err, data) {
if (err) {
reject(err);
}
resolve(data);
});
})
}

So our authUtils file should look like this:

import {getClientDetails} from '../services/ClientService';
import uuid from 'node-uuid';
const newSessionRoutes = [{ path: '/user/login', method: 'POST' }];
const authRoutes = [{ path: '/user/password', method: 'PUT' }];
export const clientApiKeyValidation = async (req,res,next) => {
let clientApiKey = req.get('api_key');
if(!clientApiKey){
return res.status(400).send({
status:false,
response:"Missing Api Key"
});
}
try {
let clientDetails = await getClientDetails(req.db, clientApiKey);
if (clientDetails) {
next();
}
} catch (e) {
console.log('%%%%%%%% error :', e);
return res.status(400).send({
status: false,
response: "Invalid Api Key"
});
}
}export const isNewSessionRequired = (httpMethod, url) => {
for (let routeObj of newSessionRoutes) {
if (routeObj.method === httpMethod && routeObj.path === url) {
return true;
}
}
return false;
}
export const isAuthRequired = (httpMethod, url) => {
for (let routeObj of authRoutes) {
if (routeObj.method === httpMethod && routeObj.path === url) {
return true;
}
}
return false;
}
export const generateRandomSessionID = () => {
return uuid.v4();
}
export const getRedisSessionData = (redisClient,sessionId) => {
return new Promise((resolve, reject) => {
redisClient.get(sessionId, function (err, data) {
if (err) {
reject(err);
}
resolve(data);
});
})
}

Before starting on the next file here are some notes:

This is not a production quality code. We have over-simplified things and have ignored couple of best practises. Here is a list of things which are over-simplified to limit the focus of the article:

i) apis authorisation config has been put in constant variables. Ideally they should be fetched from db or should be put in json file. Even the config can be more complex to handle user role based authentication.

ii) Passwords should be kept in encrypted manner. This can be easily achieved by using various npm libraries like bcrypt etc

Now let us edit the app.js file.

import {clientApiKeyValidation} from './common/authUtils';

Update the above line as below:

import { clientApiKeyValidation, isNewSessionRequired, isAuthRequired, generateRandomSessionID, getRedisSessionData } from './common/authUtils';

And another two imports

import redis from 'redis';
import Session from './common/Session';

Add the following code after/before the mongo connect code.

let redisClient = null;
redisClient = redis.createClient({
prefix: 'node-sess:',
host: 'localhost'
});

We are instantiating a redis client connection and assigning it to a global variable. The prefix value will be used in all the keys written/read by this client. The default port being used here is 6379.

Now add the following code just after the line where we have injected clientApiKeyValidation.

app.use(async (req, res, next) => {
var apiUrl = req.originalUrl;
var httpMethod = req.method;
if (isNewSessionRequired(httpMethod, apiUrl)) {
let sessionID = generateRandomSessionID()
req.session = new Session();
req.session.sessionID = sessionID;
req.sessionID = sessionID;
} else if (isAuthRequired(httpMethod, apiUrl)) {
let sessionID = req.header('Authorization');
if (sessionID) {
let redisData = await getRedisSessionData(redisClient, sessionID);
if (redisData) {
redisData = JSON.parse(redisData);
req.session = new Session();
req.sessionID = sessionID;
req.session.sessionID = sessionID;
req.session.userData = redisData;
} else {
return res.status(401).send({
ok: false,
error: {
reason: "Invalid Sessiontoken",
code: 401
}
});
}
} else {
return res.status(401).send({
ok: false,
error: {
reason: "Missing Sessiontoken",
code: 401
}
});
}
}
next();
})

With the above code we are injecting another request handler which will handle session related stuff for all apis. First we are checking if a new session is required for the invoked api. In that case, we create a random uuid and new session object and assign it as a property of express request object. But we have not sent this data to our redis server yet. We will do it later.

let sessionID = generateRandomSessionID()
req.session = new Session();
req.session.sessionID = sessionID;
req.sessionID = sessionID;

In the other condition we check if the api needs a valid session. A very important note here. It is a common practice to pass authorisation token header in the following manner:

Authorization: Bearer a1194981-b541-416e-935

The header field should be “Authorization” and the first part of the value should be the keyword “Bearer” and then the second part separated by a space should contain the token.

This is a standard given by W3C. The keyword “Bearer” was used to distinguish this from the existing Basic auth. But in this article we will use a custom header just to showcase that we can also do it using a custom header field. We will be using Bearer Authorization header in the next article.

So we are expecting the client to send session token in the request header with the field name being sessiontoken

let sessionID = req.header('sessiontoken');

If the sessionID is missing we return an error in the api. But if the sessionID is present in the header, we try to hit the redis server and get the record for the passed key.

let redisData = await getRedisSessionData(redisClient, sessionID);

If redisData is null, then we return an error in the api saying the token is invalid. This can happen if the session got expired and deleted from the redis server or if a random invalid value was passed in the header.

But if we get the user details from redis. We parse the same data. { We will be storing our data as a string in redis}. We do a similar thing we did for new session except we retain the old values.

        redisData = JSON.parse(redisData);
req.session = new Session();
req.sessionID = sessionID;
req.session.sessionID = sessionID;
req.session.userData = redisData;

Now after all our route injections, let us add a response handler for all our apis too.

app.use((req, res, next) => {
if (!res.data) {
return res.status(404).send({
ok: false,
error: {
reason: "Invalid Endpoint", code: 404
}
});
}
if (req.session && req.sessionID) {
try {
req.session.save(redisClient);
res.setHeader('sessiontoken', req.sessionID);
res.data['sessiontoken'] = req.sessionID;
} catch (e) {
console.log('Error ->:', e);
}
}
res.status(res.statusCode || 200)
.send({ ok: true, response: res.data });
})

In this response handler. We are first checking if the response object has the data property. If the data property is missing, we sent a 404 error in our api response.

But if data is set we send the response in a defined format with the required http status code.

res.status(res.statusCode||200).send({ok: true, response:res.data});

And if the express request object has a sessionID field we perform two actions:

i) First we save this data in our redis server using Session.save() method.

ii) We add the sessiontoken value in both response header and response payload.

So our app.js code looks like below:

import express from 'express';
import bodyParser from 'body-parser';
import user from './routes/user';
import {MongoClient} from 'mongodb';
import { clientApiKeyValidation, isNewSessionRequired, isAuthRequired, generateRandomSessionID, getRedisSessionData } from './common/authUtils';
import redis from 'redis';
import Session from './common/Session';
const CONN_URL = 'mongodb://localhost:27017';let mongoClient = null;MongoClient.connect(CONN_URL,{ useNewUrlParser: true }, function (err, client) {
mongoClient = client;
})
let redisClient = null;
redisClient = redis.createClient({
prefix: 'node-sess:',
host: 'localhost'
});
let app = express();// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }));
// parse application/json
app.use(bodyParser.json());
app.use((req,res,next)=>{
req.db = mongoClient.db('test');
next();
})
app.get('/',(req,res,next)=>{
res.status(200).send({
status:true,
response:'Hello World!'
});
});
app.use(clientApiKeyValidation);app.use(async (req, res, next) => {
var apiUrl = req.originalUrl;
var httpMethod = req.method;
if (isNewSessionRequired(httpMethod, apiUrl)) {
let sessionID = generateRandomSessionID()
req.session = new Session();
req.session.sessionID = sessionID;
req.sessionID = sessionID;
} else if (isAuthRequired(httpMethod, apiUrl)) {
let sessionID = req.header('sessiontoken');
if (sessionID) {
let redisData = await getRedisSessionData(redisClient, sessionID);
if (redisData) {
redisData = JSON.parse(redisData);
req.session = new Session();
req.sessionID = sessionID;
req.session.sessionID = sessionID;
req.session.userData = redisData;
} else {
return res.status(401).send({
ok: false,
error: {
reason: "Invalid Sessiontoken",
code: 401
}
});
}
} else {
return res.status(401).send({
ok: false,
error: {
reason: "Missing Sessiontoken",
code: 401
}
});
}
}
next();
})
app.use('/user',user);app.use((req, res, next) => {
if (!res.data) {
return res.status(404).send({
ok: false,
error: {
reason: "Invalid Endpoint", code: 404
}
});
}
if (req.session && req.sessionID) {
try {
req.session.save(redisClient);
res.setHeader('sessiontoken', req.sessionID);
res.data['sessiontoken'] = req.sessionID;
} catch (e) {
console.log('Error ->:', e);
}
}
res.status(res.statusCode || 200)
.send({ ok: true, response: res.data });
})
app.listen(30006,()=>{
console.log(' ********** : running on 30006');
})
process.on('exit', (code) => {
mongoClient.close();
console.log(`About to exit with code: ${code}`);
});
process.on('SIGINT', function() {
console.log("Caught interrupt signal");
process.exit();
});
module.exports = app;

Now let’s move to our routes/user.js file. Before adding two new api routes let us edit the previous route “/hello” to work with the new response handler.

The earlier code used to be as below:

router.post('/hello',async (req,res)=>{
let uname = req.body.username;
let userDetails = await getUserDetails(req.db,uname)
res.status(200).send({
status:true,
response:userDetails
});
});

Here on successful retrieval of the data, we used to directly send the response. Now let us edit the above method to the one below:

router.post('/hello', async (req, res, next) => {
let uname = req.body.username;
let userDetails = await getUserDetails(req.db, uname)
res.data = {
status: true,
response: userDetails
};
next();
});

Here we just set the data attribute of the response object and invoke the next() method.

Update the import statement to include updateUserPassword method too

import { getUserDetails, updateUserPassword } from '../services/UserService';

The code for login api is as below:

router.post('/login', async (req, res, next) => {
let uname = req.body.username;
let pwd = req.body.password;
let userDetails = await getUserDetails(req.db, uname);
if (userDetails) {
let { password } = userDetails;
if (pwd === password) {
res.data = {
status: true,
response: userDetails
};
req.session.set(userDetails);
} else {
res.statusCode = 400;
res.data = {
status: false,
error: 'Invalid Password'
};
}
} else {
res.statusCode = 400;
res.data = {
status: false,
error: 'Invalid Username'
};
}
next();
});

Here we can also add another condition to check if username or password are missing from the request data. First, we try to get the user details with the username being passed. In case of missing data, we send an error stating Invalid username. Otherwise we try to compare passwords. In case of password mismatch we send an error. Otherwise we update the user data in both the response data object and request session object. As the request session object now has userdata , the save() function which is being invoked in our response handler will save the data to our redis.

Now lets add code for the update password api.

router.put('/password', async (req, res, next) => {
try {
let oldPwd = req.body.old_password;
let newPwd = req.body.new_password;
if (!oldPwd && !newPwd) {
res.statusCode = 400;
res.data = {
status: false,
error: 'Invalid Parameters'
}
}
let uname = req.session.userData.username;
let userDetails = await getUserDetails(req.db, uname);
if (oldPwd !== userDetails.password) {
res.statusCode = 400;
res.data = {
status: false,
error: "Old Password doesn't match"
}
} else {
let updateRes = await updateUserPassword(req.db,uname,newPwd)
res.data = {
status: true,
response:updateRes,
message: "Password updated successfully"
}
}
next();
} catch (e) {
next(e)
}
})

This is interesting part of the code. Here we will see how get all the user details from the session token. The user details or the username was not mentioned anywhere in the request. But our session handler which we injected as a request handler must have injected the userdata in our request session object.

let uname = req.session.userData.username;

We get the username from the request session object and then we perform all actions needed to update the user’s password.

Phew !!!!!

Photo by Kendal James on Unsplash

Now let us test our code. Run the project using node index.js command.

Let us first hit our old api “/hello” to check our new response handler.

Let us hit the login api now:

We can see the session token in the api response. We can also check the redis instance to confirm that we have the user details.

Now let us try to hit the update password api with an invalid token:

Now let us copy the session token we got earlier in the login api response and pass it in the header of the update password api.

I hope you liked the article and in the next article we will see how to implement authentication using JWT.

You can find the code here:

https://github.com/pankaj805/medium-06_redis

If you liked the article, you can 👏 the story and share it with others. You can find the entire article series here.

Done for the day !

--

--