Role Based Access Control using express-session in a node-js app
In this article I refer to an application I created a couple of months ago. It’s about a booking system with which players can book ice-hockey trainings in different locations, the coach can confirm participation in a training session and a club manager can organize training sessions and bill the players for booked trainings. You can see the code on my GitHub Account and read a detailed application description in the style of a user manual on my blog Digitaldocblog.
In my booking system I give users different roles in my app and depending on their role, the users have different authorizations. An admin for example is able to access more sensitive data and functionalities than a normal player or a coach. So my app must know the role of a user to assign different authorizations to the particular user.
Clients, usually browsers send requests the app. The app responds to requests and is solely responsible for ensuring that the client only has access to the data that are intended for it. This request and response game is based on the HTTP protocol. HTTP is a stateless network protocol and requests cannot be related to each other. Each request is isolated and unrelated to previous requests and the server has no chance to recognize clients and does therefore not know their role.
This problem can be solved with sessions and cookies and means that session management must be implemented in the application. The application creates a session and stores session data such as the role of a requestor in this session. The session has a unique ID and the app saves only this ID in a cookie. The cookie is transferred to the browser and stored locally there. From now on, the browser always sends this cookie with the HTTP request and thus identifies itself to the application. The application can check the role of the requestor in the stored session data and control the appropriate access.
Basic setup of the server
First we need a working Server OS. I run Linux Ubuntu in production and have written an article about the basic setup of a production Linux server on my blog site Digitaldocblog. Since I am going to store the sessions in a MongoDB, MongoDB must be installed on the Linux server. I use MongoDB Community Edition but you can also install or upgrade to the MongoDB Enterprise Server version. In the lower part of the article you find the instructions how to install and setup your MongoDB Community Edition on your Linux System. In case you want to read the original documentation go on the MongoDB site and read how to install the MongoDB Community Edition for your OS.
In my express application I use a number of external modules or dependencies that have to be installed for the application in order for the application to run. In the repository of the bookingsystemon my GitHub account you find the package.json file which contains all the necessary dependencies. In principle, it is sufficient if you put this package.json file in your application main directory and install all dependencies with npm install
.
Alternatively, of course, all modules can also be installed individually with
npm install <module> --save
Session Management
I discuss in the first part different code snippets in my application main file booking.js. The goal here is that you understand how session management is basically implemented.
// booking.js
// Load express module and create app
const express = require('express');
const app = express();
// Trust the first Proxy
app.set('trust proxy', 1);
// Load HTTP response header security module
const helmet = require('helmet');
// use secure HTTP headers using helmet with every request
app.use(
helmet({
frameguard: {
action: "deny",
},
referrerPolicy: {
policy: "no-referrer",
},
})
);
// Load envy module to manage environment variables
const envy = require('envy');
const env = envy();
// Set environment variables
const port = env.port
const host = env.host
const mongodbpath = env.mongodbpath
const sessionsecret = env.sessionsecret
const sessioncookiename = env.sessioncookiename
const sessioncookiecollection = env.sessioncookiecollection
// Load server side session and cookie module
const session = require('express-session');
// Load mongodb session storage module
const connectMdbSession = require('connect-mongodb-session');
// Create MongoDB session store object
const MongoDBStore = connectMdbSession(session)
// Create new session store in mongodb
const store = new MongoDBStore({
uri: mongodbpath,
collection: sessioncookiecollection
});
// Catch errors in case session store creation fails
store.on('error', function(error) {
console.log(`error store session in session store: ${error.message}`);
});
// Use session to create session and session cookie
app.use(session({
secret: sessionsecret,
name: sessioncookiename,
store: store,
resave: false,
saveUninitialized: false,
// set cookie to 1 week maxAge
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7,
sameSite: true
},
}));
... //further code not taken into account at this point
I create a server application using the Express-js Web Application Framework. Therefore I load the Express-js module with the require()
function and store the express()
function in the constant app. Because my app is running behind a reverse proxy server I set the app to trust the first proxy server. Then I load the helmet module to use secure response headers in my app. I configure that all browsers should deny iFrames and that my app will set no referrer in the response header.
I use the envy module in my application to manage environment variables. Therefore I load the module with require()
and store the envy()
function in the constant env. With envy you can define your environment variables in your .env and .env.example files. These files must be stored in the application main directory as explained in the envy documentation.
Since my booking app is a real web application running on a web server in production I can not discuss the real environment variables because of security reasons. Therefore let us see how this work and make an example .env file.
// .env
port=myport
host=myhost
mongodbpath=myexamplemongodbpath
sessionsecret=myexamplesecret
sessioncookiename=booking
sessioncookiecollection=col_sessions
These variables have different values in my .env file. In the booking.jsfile above I define the constant env for the envy function with env = envy()
. Then I have access to the environment variables defined in my .env file with env.<variable>. I define constants for each variable and assign the variable from the .env file with env.<variable>
. These constants can now be used as values in the code.
I load the express-session module and the connect-mongodb-session module with the require()
function. The session module stored in the constant session takes over the entire control of the session and cookie management.
The connect-mongodb-session stored in the constant connectMdbSession module is basically responsible for storing the session in the database. That is why we pass session as a parameter in the code and assign the constant MongoDBstore.
const MongoDBstore = connectMdbSession(session)
With new MongoDBStore
I create a new store object. Here I pass the uri of the mongodb path and the collection where sessions should be stored.
// booking.js
...
const store = new MongoDBStore({
uri: mongodbpath,
collection: sessioncookiecollection
});
...
The store object initialized in this way contains all necessary parameters to successfully store a session object in my MongoDB database.
After we have defined the storage of the session object, we take care of the session object itself.
With app.use(session( {... cookie: {...} }))
I create a session object with various options. The session object will be created with each request and also contains a cookie object. I pass the values for cookie: {...}
and then other options likesecret: sessionsecret
, the session object name with name: sessioncookiename
as well as the location where the session object should be stored with store: store
. Furthermore the session object has the option saveUninitialized: false
and resave: false
.
When the saveUninitialized option is set to false the session object is not stored into the store as long as the session is un-initialized. The option resave: false
enforce that a session will not be saved back to the store even if the session is initialized. So we must understand what initialized and un-initialized mean. This must be explained.
A browser send a request to the app. More precisely, the browser sends the request to a defined endpoint in the app. An endpoint defines a path within the app that reacts to HTTP requests and executes code. Depending on the HTTP method GET or POST, the endpoint expects that the requestor requires a document back (GET) or that the requestor wants to send data to the app (POST).
In the example below the browser should send a GET request to GET home endpoint. The endpoint render the HTML template index and send the HTML back to the browser. Then the request is finished. So this process, which starts with the request and ends with the response is the runtime of the request.
In the code snippet below you see 2 GET endpoints in the booking.js file one for the home route and another one for the register route.
// booking.js
... //further code not taken into account at this point
// GET home route only for anonym users. Authenticated users redirected to dashboard
app.get('/', redirectDashboard, (req, res) => {
console.log(req.url);
console.log(req.session.id);
console.log(req.session);
res.render('index', {
title: 'User Login Page',
});
});
// GET register route only for anonym users. Authenticated users redirected to dashboard
app.get('/register', redirectDashboard, (req, res) => {
console.log(req.url);
console.log(req.session.id);
console.log(req.session);
res.render('register', {
title: 'User Registration Page',
});
});
... //further code not taken into account at this point
The code with app.use (session({ ... }))
in my booking.js file ensures that a session object is always generated with each request. As long as a session object is not changed during the runtime of a request a separate session is created for each request and has its own session ID. The option saveUninitialized: false
ensure that the session object will not be stored into the database. Each session object created in this way is un-initialized.
You can see the following output on the console for the home route and for the register route when we log the path, the session-ID and the session object on the console for each route.
/
BmbE8RVoTRcPP9nUnBm5JLE1w1mQiNyt
Session {
cookie: {
path: '/',
_expires: 2021-04-24T04:27:04.265Z,
originalMaxAge: 604800000,
httpOnly: true,
sameSite: true
}
}
/register
awlPO-KpyVM51Gp6UAoeXGGmRWo-QFtP
Session {
cookie: {
path: '/',
_expires: 2021-04-24T05:54:57.439Z,
originalMaxAge: 604800000,
httpOnly: true,
sameSite: true
}
}
The code of my app changes a session object during the runtime of a request by adding a data object when a user has successfully logged in. I will explain the code in detail in the next chapter but at the moment it is enough to know that. Therefore we play through the login of a user as follows.
The browser send a GET request to the home route as explained above, then the index template is rendered and the HTML page with the login form is sent back to the browser. During the runtime of this GET request a session object is created but the session object has not changed. We have already seen this above.
Then the user enters email and password in the login form and click submit. With this submit the browser send a POST request to the POST endpoint /loginusers and again a session object is generated for this POST request. During the runtime of the POST request, the code checks whether the transferred credentials are correct. If the credentials are correct, a data object with user data is generated and attached to the session object. Here the session object is changed during the runtime of the POST request. The existing session created with the POST request is now initialized at that moment. Because of the option saveUninitialized: false
this session object is stored into the database store. When we look into the database store using the tool MongoDB Compass we see that the entire session object has been saved into the colsessions collection including the data object containing the required data of the user.
After the session initialization the code called by the POST endpoint redirect the request and send a new GET request to the /dashboard route. The code with app.use(session({ ... }))
is called again but now there is an initialized session existing in the store. Because of the option resave: false
the existing session object will not be updated and dragged along unchanged with every further request.
You see this in the output on the console when we log the path, the session-ID and the session object on the console for each route. The first output on the console is created when the GET request is sent to the home route. Then, the second output, after the user clicked submit the POST route /loginusers is called and a new session object is created. You see this from the different session IDs. During the runtime of this POST request the data object is added to the session object which initializes the session. Then, the third output, the GET route /dashboard is called and we see the same session object ID but the session object now contain the data object with the user data.
/
TEAZITdX7nLWBDc8uOk2HhXIiMZO7W-4
Session {
cookie: {
path: '/',
_expires: 2021-05-02T07:21:13.236Z,
originalMaxAge: 604800000,
httpOnly: true,
sameSite: true
}
}
/loginusers
gVlKut3bdEMiDHnK455FGjCi6YbPTBuZ
Session {
cookie: {
path: '/',
_expires: 2021-05-02T07:21:35.202Z,
originalMaxAge: 604800000,
httpOnly: true,
sameSite: true
}
}
/dashboard
gVlKut3bdEMiDHnK455FGjCi6YbPTBuZ
Session {
cookie: {
path: '/',
_expires: 2021-05-02T07:21:35.468Z,
originalMaxAge: 604800000,
httpOnly: true,
secure: null,
domain: null,
sameSite: true
},
data: {
userId: 5f716b7439777365c18639f1,
status: 'active',
name: 'Oskar David',
lastname: 'Rottländer',
email: 'oskar@test.com',
role: 'player',
age: 17,
cat: 'youth'
}
}
In summary, session management works as follows: A session object will be created with each request and the session object is only saved in the database when the user is logged in (saveUninitialized: false). As long as the user is logged in, the session object is not changed and the data of the session object in the database are not updated (resave: false).
But what happens to the cookie ? This will be explained in the next chapter.
User login
When the session has been initialized the cookie containing the session ID is stored in the browser of the requestor. With every request the browser provide the cookie to authenticate the requestor. To authenticate the requestor the code app.use(session({...}))
is called and compare the session ID sent by the browser with the session IDs stored in the session store. If a session ID matches, the session object including the data object is attached to the request object to give the app access to the data object. Within the app we now have access to any attribute of the data object with req.session.data.<attribute>. Therefore we can now implement role based authorization by accessing the role of the requestor with req.session.data.role and use this information in conditions in the code to control access depending on the role of the requestor.
But lets start from the beginning with the login of the requestor or user as I call the requestor from now on. In order for a user to be able to login, he or she must first call the login page which can be displayed by calling up the home endpoint.
// booking.js
... // Code not discussed here
// Redirect GET requests from authenticated users to dashboard
const redirectDashboard = (req, res, next) => {
if (req.session.data) {
res.redirect('/dashboard')
} else {
next()
}
}
... // Code not discussed here
// GET home route only for anonym users. Authenticated users redirected to dashboard
app.get('/', redirectDashboard, (req, res) => {
res.render('index', {
title: 'User Login Page',
});
});
... // Code not discussed here
As you see above in the code I have first defined the middleware function redirectDashboard. This middleware ensure that only users who are not logged in see the login page. If we look at the code of the middleware function we can see that req.session.data is used in an if-condition to check whether a data object is attached to the current session object. In case the if-condition is true, the user is logged in and the request is redirected to the dashboard, but in case the if-condition is false, the user is not logged in and the next() function is called.
The GET endpoint has the routingPath to the home route. When a user visits the homepage of my booking application, the GET HTTP request ask for the home routingPath. The middleware function redirectDashboard is put in front of the routingHandler function. If the user is not logged in the routingHandler function render the HTML template index.pugand send the HTML back to the user or more precisely to the user’s browser.
So far so good. We now imagine a not logged in user who sees the index page in front of him or her now wants to login using his or her email and password.
As described above, the index page is nothing more than a login form for entering an email address and a password. When we look at the index.pug file we see that the form action attribute define that the form data email
and password
will be sent to the form handler /loginusers
using the POST method when the Submit button is clicked.
...
form#loginForm.col.s12(
method='post',
action='/loginusers'
)
input.validate(
type='email',
name='email',
autocomplete='username'
required
)
...
input.validate(
type='password',
name='password',
autocomplete='current-password'
required
)
...
button.btn.waves-effect.waves-light(
type='submit',
form='loginForm'
)
...
Note: To understand the autocomplete attributes of the input tags I recommend reading the documentation of the Chromium Project. Most browsers have password management functionalities and automatically fill in the credentials after you provide a master password to unlock your local password store. By using these autocomplete attributes in login forms but also in user registration forms or change password forms you help browsers by using these autocomplete functions to better identify these forms.
When the user has entered his or her email and password in the HTML form and clicked the Submit button, the request body contain the Form Data attributes email and password. Then a POST HTTP request is sent via HTTPS to the POST endpoint /loginusers
defined in my booking.js file (see above).
In the picture below you can see the output of the network analysis in the developer tool of the chrome browser. Here you can see that the Form Data are not encrypted on browser side but you also see that the POST request URL /loginusers
is HTTPS. This mean that when the browser sent the POST request to the server these data are encrypted with SSL/TLS in transit from the browser to the server.
On the server side we have the web application behind a proxy server listening to HTTP requests addressed to the POST endpoint /loginusers
. This POST endpoint is an anonym POST Route which means that the routingHandler controller function is restricted to not logged-in users only. This makes sense because a login function must not be used by already logged in users. So already logged in users can not send data to this POST endpoint. This check is controlled by the middleware function verifyAnonym which is put in front of the routingHandler.
So lets look at the relevant code snippets in booking.js.
// booking.js
...
// Load db controllers and db models
const userController = require('./database/controllers/userC');
...
// Verify POST requests only for anonym users
const verifyAnonym = (req, res, next) => {
if (req.session.data) {
var message = 'You are not authorized to perform this request because you are already logged-in !';
res.status(400).redirect('/400badRequest?message='+message);
} else {
next()
}
}
...
// Anonym POST Route
// Login user available for anonym only
app.post('/loginusers', verifyAnonym, userController.loginUser)
...
// GET bad request route render 400badRequest
app.get('/400badRequest', (req, res) => {
res.status(400).render('400badRequest', {
title: 'Bad Request',
code: 400,
status: 'Bad Request',
message: req.query.message,
})
})
...
At the beginning of the code I refer the constant userController to the user controller file userC.js using the require
method. In userC.js all user functions are defined to control user related operations.
Note: When you look into the userC.js file you see that we export modules using module.exports = {...}
. Using this directive we export in fact an object with various attributes and the values of these attributes are functions. So with module.exports = { loginUser: function(...) ...}
we export the object including the attribute loginUser which contains a function as value. So when we refer the constant userController using the require()
function in the booking.js file we store the complete exported object with all its attributes to the userController constant. Now we have access to any attribute of the exported object from userC.js file with userController.<attribute>. Because the attributes are in fact functions we call these functions with this statement.
In the verifyAnonym function req.session.data is used in the if-condition to check whether a data object is attached to the current session object. In case the if-condition is true, the user is already logged-in and is redirected to the Bad Request GET endpoint /400badRequest
which is the standard route in my application to show the user that something went wrong. The user can see what went wrong from a message that has been attached to the request using the request parameter ?message=+message
. In case the if-condition is false, the user is not logged-in and the next() function forwards the request to the routingHandler controller function that call the loginUser
function using userController.loginUser. This function has access to the attributes email and password of the request body with req.body.email and req.body.password.
So lets look at the relevant code snippets in userC.js file.
// database/controllers/userC.js
// load the bcryptjs module
const bcrypt = require('bcryptjs');
// define hash saltrounds for password hashing
const saltRounds = 10;
// load the relevant Prototype Objects (exported from the models)
...
const User = require('../models/userM');
...
loginUser: function (req, res) {
const inputemail = req.body.email
const email = inputemail.toLowerCase()
console.log(req.url);
console.log(req.session.id);
console.log(req.session);
try {
User.findOne({ email: email }, async function(error, user) {
if (!user) {
var message = 'User not found. Login not possible';
res.status(400).redirect('/400badRequest?message='+message);
} else {
if (user._status !== 'active') {
var message = 'Login not possible. Await User to be activated';
res.status(400).redirect('/400badRequest?message='+message);
} else {
if (bcrypt.compareSync(req.body.password, user.password)) {
var yearInMs = 3.15576e+10;
var currentDate = new Date ()
var currentDateMs = currentDate.getTime()
var birthDateMs = user.birthdate.getTime()
var age = Math.floor((currentDateMs - birthDateMs) / yearInMs)
if (age < 18) {
var cat = 'youth'
} else {
var cat = 'adult'
};
var userData = {
userId: user._id,
status: user._status,
name: user.name,
lastname: user.lastname,
email: user.email,
role: user.role,
age: age,
cat: cat,
}
req.session.data = userData
res.status(200).redirect('/dashboard')
} else {
var message = 'Login not possible. Wrong User password';
res.status(400).redirect('/400badRequest?message='+message);
}
}
}
})
} catch (error) {
// if user query fail call default error function
next(error)
}
// End Module
},
...
In order to authenticate a user, the loginUser function must find a user in the user database with the same email address as the one that was sent by the browser and attached to the request body by the app. If a user was found with the email, the function must check whether the transmitted password matches the password that is stored in the database for this user. If the email and password match the user is authenticated and the login is successful, if not, the login fails.
Passwords are never saved in plain text. Therefore I use the bcryptjs module to hash passwords. The bcryptjs module is loaded into the code with the require()
function and assigned to the constant bcrypt. We set the constant saltRounds to the value of 10. This is the so called cost factor in the bcrypt hashing function and controls how much time bcrypt need to calculate a single bcrypt hash. Increasing the cost factor by 1 doubles the time and the more time bycrypt need to hash the more difficult it is to brute force stored passwords.
Then I load the user model from userM.js using the require()
function and assign the constant User. Here at this point I have to explain the background. To do this, we also need a look at the userM.js file.
Note: I use MongoDB as the database and Mongoose to model the data. If you look in the file userM.js you see that a user object is created with the function new Schema() and saved in the variable userSchema. This userSchema object describes a user with all its attributes. At the end of the file, the mongoose.model() function is used to reference the userSchema to the collection colusers in my MongoDB. This reference is assigned to the variable User and exported using the function module.exports(). With User I have access to the user model meaning to all user objects and attributes in my database that are stored in the colusers collection. So that I can use this access in the code of my userC.js file I load userM.js with the require()
function and assign the constant User. I can now use Mongoose functions for example to query user data from my colusers collection. This is exactly what we do with User.findOne() when we try to find a user with a certain email.
The actual user authentication now takes place in the userFindOne() function.
When we run User.findOne() we check the criteria that do not lead to a successful authentication.
- No user found: We are looking for a user object that matches the email that has been submitted. If no user object is found with that email or the user found is not active, the request is redirected to the 400badRequest route. If we have found an active user, the submitted password string is hashed with bcrypt and compared with the saved password.
- Wrong password: If the password comparison is not successful, the submitted password was wrong and the request is also redirected to the 400badRequest route.
Note: User.findOne() has a query object {email: email}
and a callback function async function(error, user {...})
as parameters. When the async function find a user with the email in the database, this async function returns a user object with all the user attributes and store this object into the user parameter. Within the scope of the async function I have now access to the user attributes using user.<attribute>.
Only in case the user with the email is found and the submitted password is correct the authentication is successful.
If the user is successfully authenticated, the category of the user is calculated based on the current date and the user’s birth date. Then a userData object is created in which various user attributes are stored. The data of the userData object are then attached to the session. More precisely, the object data is attached to the session with req.session.data and the value userData is assigned. Now the session is initialized and the session object is stored in the colsessions collection of the MongoDB.
Then the response is sent back to the browser.
In this response, the browser is instructed to call up a GET request to the GET endpoint /dashboard
. The response is sent using res.status(200).redirect('/dashboard')
. In the Response Header you see that the cookie with the name booking is set in the users browser using the set-cookie
directive. The cookie only contain the session ID which has been signed and encrypted with the secret we provided in app.use(session( {... cookie: {...} }))
.
Then the browser send the GET request to the endpoint /dashboard
. Lets have a look into the booking.js file again.
// booking.js
...
// Redirect GET requests from not authenticated users to login
const redirectLogin = (req, res, next) => {
if (!req.session.data) {
res.redirect('/')
} else {
next()
}
}
...
// GET dashboard route only for authenticated users. Anonym users redirected to home
app.get('/dashboard', redirectLogin, async (req, res) => {
// Check admin authorization and render admin_dashboard
if (req.session.data.role == 'admin') {
const user_query = User.find( {} ).sort({lastname: 1, name: 1});
var users = await user_query.exec();
const training_query = Training.find( {} ).sort({date: 'desc'});
var trainings = await training_query.exec();
const location_query = Location.find( {} ).sort({location: 'desc'});
var locations = await location_query.exec();
const booking_query = Booking.find( {} ).sort({_booktrainingdate: 'desc'});
var bookings = await booking_query.exec();
const invoice_query = Invoice.find( {} ).sort({invoicedate: 'desc'});
var invoices = await invoice_query.exec();
res.status(200).render('admin_dashboard', {
title: 'Admin Dashboard Page',
name: req.session.data.name,
lastname: req.session.data.lastname,
role: req.session.data.role,
data_users: users,
data_trainings: trainings,
data_locations: locations,
data_bookings: bookings,
data_invoices: invoices,
});
// Check player authorization and render player_dashboard
} else if (req.session.data.role == 'player') {
var currentDate = new Date();
console.log('current date: ' +currentDate);
const availabletraining_query = Training.find( { _status: 'active', date: { $gte: currentDate } } ).sort({ date: 'desc' });
var availabletrainings = await availabletraining_query.exec();
const booking_query = Booking.find( { _bookuseremail: req.session.data.email, _bookparticipation: { $ne: 'invoice' } } ).sort({ _booktrainingdate: 'desc' });
var bookings = await booking_query.exec();
const myuser_query = User.findOne( { email: req.session.data.email } );
var myuser = await myuser_query.exec();
const invoice_query = Invoice.find( {invoiceemail: req.session.data.email} ).sort({invoicedate: 'desc'});
var invoices = await invoice_query .exec();
res.status(200).render('player_dashboard', {
title: 'Player Dashboard Page',
name: req.session.data.name,
lastname: req.session.data.lastname,
role: req.session.data.role,
email: req.session.data.email,
data_availabletrainings: availabletrainings,
data_bookings: bookings,
data_myuser: myuser,
data_myinvoices: invoices,
});
// Check coach authorization and render coach_dashboard
} else if (req.session.data.role == 'coach') {
var currentDate = new Date().setHours(00, 00, 00);
console.log('currentDate: ' +currentDate);
const training_query = Training.find( { _status: 'active', date: { $gte: currentDate } } ).sort({ date: 'asc' });
var trainings = await training_query.exec();
res.status(200).render('coach_dashboard', {
title: 'Coach Dashboard Page',
name: req.session.data.name,
lastname: req.session.data.lastname,
role: req.session.data.role,
data_trainings: trainings,
});
} else {
// if user not authorized as admin, player or coach end request and send response
var message = 'You are not authorized. Access prohibited';
res.status(400).redirect('/400badRequest?message='+message);
}
});
...
As you see above in the code we have first defined the middleware function redirectLogin. This middleware ensure that only users who are logged in see the dashboard page. In case the if-condition is true, the user is not logged in and the request is redirected to the home route, but in case the if-condition is false, the user is logged in and the next() function is called.
The GET HTTP request ask for the dashboard routingPath. The middleware function redirectLogin is put in front of the routingHandler function. If the user is not logged in the redirectLogin middleware redirect the request to the home route. In case the user is logged-in the routingHandler function is called using the request object and the response object as parameters.
app.get('/dashboard', redirectLogin, async (req, res) => {...})
If we look at the Request Header of this new GET request in the browser we can see that the cookie is dragged along unchanged with the GET request to the endpoint /dashboard
. This happens from now on with every request until the session expires or until the user logout.
And now within the routingHandler function we do the user authorization check. The if condition check the users role using req.session.data.role. Depending on the role of the user different <role>dashboard HTML templates are rendered and for each role different HTML is sent back to the user’s browser. Various queries are executed beforehand because we need role specific data within each <role>dashboard HTML template. The return values of the different queries find()
and findOne()
are only executed in case one of the if conditions become true. Then in each case the return values of the queries are stored in variables. In case all if conditions are false, meaning we cannot find a user with a role like admin, player or coach in the database for some reason the request is redirected to the Bad Request GET endpoint /400badRequest
using the message as request parameter that this user is not authorized.
Within each if condition and for each role admin, player or coach, we create the response object by first setting the HTTP status to the value of 200 and then using the render method to render the respective HTML template.
res.status(200).render('<role>_dashboard', {...})
Within the render method, we now have the option of transferring a data object with different attributes to the HTML template. Later we can access these data in the respective HTML template and use it within the HTML template. How this works is not part of this article. But of course you can take a closer look at the templates admin dashboard, player dashboardand coach dashboardon my GitHub repository and you will immediately see how this works.
Create Authorizations
As I have already shown in the upper part, I work with middleware functions to control access to GET and POST endpoints in my app. Therefore these middleware functions are the Authorizations and you can find them in the code of my booking.js file.
// booking.js
...
// Authorizations
// Redirect GET requests from not authenticated users to login
const redirectLogin = (req, res, next) => {
if (!req.session.data) {
res.redirect('/')
} else {
next()
}
}
// Redirect GET requests from authenticated users to dashboard
const redirectDashboard = (req, res, next) => {
if (req.session.data) {
res.redirect('/dashboard')
} else {
next()
}
}
// Authorize POST requests only for not authenticated users
const verifyAnonym = (req, res, next) => {
if (!req.session.data) {
next()
} else {
var message = 'You are already logged-in. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
// Authorize POST requests only for anonym and admin users
const verifyAnonymAndAdmin = (req, res, next) => {
if (!req.session.data) {
next()
} else {
if (req.session.data.role == 'admin') {
next()
} else {
var message = 'You are no Admin. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
}
// Authorize POST requests only for admin and player users
const verifyAdminAndPlayer = (req, res, next) => {
if (req.session.data) {
if (req.session.data.role == 'admin') {
next()
} else if (req.session.data.role == 'player') {
next()
} else {
var message = 'You are no Admin, no Player. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
} else {
var message = 'You are not logged-in. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
// Authorize POST requests only for admin users
const verifyAdmin = (req, res, next) => {
if (req.session.data) {
if (req.session.data.role == 'admin') {
next()
} else {
var message = 'You are no Admin. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
} else {
var message = 'You are not logged-in. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
// Authorize POST requests only for player users
const verifyPlayer = (req, res, next) => {
if (req.session.data) {
if (req.session.data.role == 'player') {
next()
} else {
var message = 'You are no Player. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
} else {
var message = 'You are not logged-in. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
// Authorize POST requests only for coach users
const verifyCoach = (req, res, next) => {
if (req.session.data) {
if (req.session.data.role == 'coach') {
next()
} else {
var message = 'You are no Coach. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
} else {
var message = 'You are not logged-in. You are not authorized to perform this request !';
res.status(400).redirect('/400badRequest?message='+message);
}
}
...
As I have already explained above I use redirect functions as a middleware to control access to the GET endpoints home, register and dashboard. These middleware functions basically control access based on whether a user is logged-in or not. The redirect function redirectDashboard allow only not logged-in users access to the home endpoint and to the register endpoint, while users who are already logged-in have no access and would be redirected directly to the dashboard route if they try to access here. The redirectLogin middleware function allow only logged-in users access to the dashboard route while not logged-in users are redirected to the login or better to the home endpoint.
In addition to redirect functions I use verify functions as a middleware to control access to the POST endpoints. With the help of POST requests, data are sent via POST endpoints to the app. That is why it is particularly important to control who is allowed to send data and who is not. I use basically 5 types of POST endpoints.
Anonym POST endpoint. I only have one endpoint here. The loginusers endpoint can only be called by not logged-in users. Therefore the verifyAnonym middleware is set before the routingHandler function to verify if the user is not logged-in.
// booking.js
...
// Anonym POST endpoint
// Login user available for anonym only
app.post('/loginusers', verifyAnonym, userController.loginUser)
...
Shared POST endpoints. The createusers endpoint can be called by not logged-in users and Admin users. The verifyAnonymAndAdmin middleware is set before the routingHandler function to verify if the user is not logged-in or if the user that is logged-in has the role admin. The updateuseremail and setnewuserpassword endpoints can be called only by Admin and Player users. Therefore the verifyAdminAndPlayer middleware is set before the routingHandler function to verify if the user is logged-in and if the users role is admin or player.
// booking.js
...
// Shared POST endpoints
// Create Users available for anonym and admin
app.post('/createusers', verifyAnonymAndAdmin, birthdateFormatValidation, userController.createUser)
// Update User-Email available for admin and player
app.post('/updateuseremail', verifyAdminAndPlayer, userController.updateUserEmail)
// Update User-Password available for admin and player
app.post('/setnewuserpassword', verifyAdminAndPlayer, userController.setNewUserPassword)
...
Admin POST endpoints. I have 19 endpoints here and each of these endpoint can only be called by Admin users. The verifyAdmin middleware is set before the routingHandler function to verify if the user is logged-in and if the users role is admin.
// booking.js
...
// Admin POST endpoints
// Admin User Management
app.post('/callupdateusers', verifyAdmin, userController.callUpdateUsers)
app.post('/updateuser', verifyAdmin, birthdateFormatValidation, userController.updateUser)
app.post('/terminateusers', verifyAdmin, userController.terminateUser)
app.post('/activateusers', verifyAdmin, userController.activateUser)
app.post('/removeusers', verifyAdmin, userController.removeUser)
// Admin Update Training
app.post('/callupdatetrainings', verifyAdmin, trainingController.callUpdateTrainings)
app.post('/updatetraining', verifyAdmin, trainingController.updateTraining)
// Admin Location Management
app.post('/createlocations', verifyAdmin, locationController.createLocation)
app.post('/callupdatelocations', verifyAdmin, locationController.callUpdateLocations)
app.post('/updatelocation', verifyAdmin, locationController.updateLocation)
app.post('/callcreatetrainings', verifyAdmin, trainingController.callCreateTrainings)
app.post('/createtraining', verifyAdmin, trainingController.createTraining)
// Admin Invoice Management
app.post('/createinvoice', verifyAdmin, invoiceController.createInvoiceUser)
app.post('/callcancelinvoice', verifyAdmin, invoiceController.callCancelInvoice)
app.post('/cancelinvoice', verifyAdmin, invoiceController.cancelInvoice)
app.post('/callpayinvoice', verifyAdmin, invoiceController.callPayInvoice)
app.post('/payinvoice', verifyAdmin, invoiceController.payInvoice)
app.post('/callrepayinvoice', verifyAdmin, invoiceController.callRePayInvoice)
app.post('/repayinvoice', verifyAdmin, invoiceController.rePayInvoice)
...
Player POST endpoints. I have 7 endpoints here and each of these endpoint can only be called by Player users. The verifyPlayer middleware is set before the routingHandler function to verify if the user is logged-in and if the users role is player.
// booking.js
...
// Player POST endpoints
// Player Booking Management
app.post('/callbooktrainings', verifyPlayer, bookingController.callBookTrainings)
app.post('/booktrainings', verifyPlayer, bookingController.bookTraining)
app.post('/bookingreactivate', verifyPlayer, bookingController.bookingReactivate)
app.post('/callcancelbookings', verifyPlayer, bookingController.callCancelBooking)
app.post('/cancelbookings', verifyPlayer, bookingController.cancelBooking)
// Player User Management
app.post('/callupdatemyuserdata', verifyPlayer, userController.callUpdateMyUserData)
app.post('/updatemyuserdata', verifyPlayer, birthdateFormatValidation, userController.updateMyUserData)
...
Coach POST endpoints. I have 2 endpoints here and each of these endpoint can only be called by Coach users. The verifyCoach middleware is set before the routingHandler function to verify if the user is logged-in and if the users role is coach.
// booking.js
...
// Coach POST endpoints
// Coach Confirmation Management
app.post('/callparticipants', verifyCoach, bookingController.callParticipants)
app.post('/callconfirmpatricipants', verifyCoach, bookingController.callConfirmPatricipants)
...
User logout
The user initiates a logout himself by clicking on the logout link in the navigation of the application. This sends a GET request to the /logout
endpoint of the application. In this routing definition, the session is first deleted from the database using req.session.destroy()
and then the cookie is removed from the browser and the user is redirected to the 200success site using res.status(200).clearCookie('booking').redirect()
.
// booking.js
...
// GET logout route only for authenticated users. Anonym users redirected to home
app.get('/logout', redirectLogin, (req, res) => {
req.session.destroy(function(err) {
if (err) {
res.send('An err occured: ' +err.message);
} else {
var message = 'You have been successfully logged out';
res.status(200).clearCookie('booking').redirect('/200success?message='+message)
}
});
})
...