Cookie and Session (III): Simple express-session authentication example

Alysa Chan
10 min readDec 22, 2021

--

My previous two articles about session and cookies:

Cookie and Session (I) — The basic concept
Cookie and Session (II) — How session works in express-session

Complete example code

Here is my GitHub Repo for this example.

Simple express-session authentication example

This article focuses on implementation of authentication with express-session, connect-flash, redis and ejs. Here are the features I build in my example:

  • User login
  • User can visit his profile page which shows his previous login time records
  • If user has logged in before, skip the login page and redirect to his profile page
  • Inside profile page, user can choose to logout. Then we clear his previous login time records. Redirect him to the login page.

Before we start, let’s setup the environment.

Files

  • app.js: for all server code
  • views: all html pages
  • .env: for storing express session secret
  • .prettierrc (optional): prettier setting for formatting our code

Pages we need

In this practice, we have to show two pages:

  • index.ejs: login page
  • profile.ejs: profile page

ejs is a JavaScript template language for generating a HTML file. You can write JavaScript inside ejs file.

Project setup

require('dotenv').config()
const express = require('express')
const flash = require('connect-flash')
const app = express()
const port = 3000
app.set('view engine', 'ejs')
const redis = require('redis')
var session = require('express-session')
let RedisStore = require('connect-redis')(session)
let redisClient = redis.createClient()
redisClient.on('error', (err) =>
console.log(`Fail to connect to redis. ${err}`)
)
redisClient.on('connect', () => console.log('Successfully connect to redis'))
const users = [
{ id: 111, username: 'tom', password: '123' },
{ id: 222, username: 'chris', password: '123' },
{ id: 333, username: 'mary', password: '123' },
]
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
})
)
app.use(flash())

Important note: we should not expose the secret in session. It is the key for signing session ID stored in user's browser. Best practice is mentioned in the express-session documentation:

app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
})
)

To create an en environment variable, install dotenv and create a file named .env:

Fake user data for demonstration

const users = [
{ id: 111, username: 'tom', password: '123' },
{ id: 222, username: 'chris', password: '123' },
{ id: 333, username: 'mary', password: '123' },
]

Be aware that in production level, these information should be stored in database and the password should be hashed. It is strongly not recommended to store password in plain text, i.e: 123, for security reason.

Since this practice focuses on express and session, I simply create a few fake data with JavaScript object instead of storing them in a database.

Let’s start our project!

User login page (homepage)

The homepage is the login page:

app.get('/', (req, res) => {
if (req.session.isAuth) {
return res.redirect('/profile')
}
const showInputError = req.flash('showInputError')[0] || false
res.render('index', { showInputError })
})

Authentication based on isAuth

Firstly, we have to decide whether this user has logged in before, by checking does the session have isAuth key and value.

Before we continue, you may ask why can we access his session if he has not logged in yet?

No matter the user has logged in or not, we can access his session because we set a global session middleware before entering every route:

// session middleware, for creating a session
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
})
)
// login page
app.get('/', (req, res) => {
...
})

As mentioned in my previous article, this middleware creates a new session or get the existing session data from Redis store if the client’s cookie has session ID. Hence, every user has session before entering any routes in our application.

Then, we decide whether the user has logged in or not before, by checking the isAuth key and value in his session object.

if (req.session.isAuth) {
return res.redirect('/profile')
}

How or when should we add isAuth to the session? The answer will be shown in app.post('/login') {...} and explained later. Right now, we only have to understand that isAuth is the property we used for authentication.

At the end, if the user has isAuth in his session, that means he has logged in already. He can skip the login page and be redirected to /profile. Otherwise, we render the homepage for asking the user to login.

app.get('/', (req, res) => {
// ...
const showInputError = req.flash('showInputError')[0] || false
res.render('index', { showInputError })
})

Validate input

After the user clicks on the ‘Sign in’ button, the HTML form sends a POST request to our server. We validate the user's input, by comparing the username and password in the request object sent by the client with the fake data in our server.

Fake data:

const users = [
{ id: 111, username: 'tom', password: '123' },
{ id: 222, username: 'chris', password: '123' },
{ id: 333, username: 'mary', password: '123' },
]

POST request for login

app.post('/', (req, res) => {
const { username, password } = req.body

// Prevent empty input
if (username.trim() === '' || password.trim() === '') {
req.flash('showInputError', true)
return res.redirect('/')
}

// Validate input
const targetUser = users.find(
(user) => user.username === username && user.password === password
)

// Wrong username or password
if (!targetUser) {
req.flash('showInputError', true)
return res.redirect('/')
}

req.session.isAuth = true
req.session.username = targetUser.username
req.session.timestamps = []
res.redirect('/profile')
})

If the user’s username and password match one of our fake data, he is a valid user and signs in successfully. As mentioned at the beginning of this article, we check on isAuth for authentication. After we validate the user, we add isAuth to the user's session for showing that this user has logged in successfully.

Meanwhile, we add username and timestamps in the user's session, for later to be displayed in the user's /profile page.

Let’s try login with the correct username and password

Result:

Show error message with connect-flash

However, if the users inputs the information incorrectly, we have to:

  1. Redirect him back to / (i.e login page)
  2. Render the login page with an error message.

To display the error message in the homepage, we have to decide whether we have to show the error, by using connect-flash.

connect-flash

connect-flash is a library for passing data when redirecting user to a page. The data passed by flash will be removed after the data is rendered on the page.

Important notes:

  • Flash message data is stored in session. You have to add session middleware before adding flash middleware.
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
})
)
app.use(flash())
  • If you call a render function without including flash message data, the data will still be kept and will not be removed.

Implementation

Back to our example, if we receive empty input or incorrect input, we add a flash message to the user’s request object:

// Prevent empty input
if (username.trim() === '' || password.trim() === '') {
req.flash('showInputError', true)
return res.redirect('/')
}
//...// Wrong username or password
if (!targetUser) {
req.flash('showInputError', true)
return res.redirect('/')
}

After the user is redirected to the homepage, we check showInputError for deciding whether we have to display error message on the page:

app.get('/', (req, res) => {  if (req.session.isAuth) {
return res.redirect('/profile')
}
// flash uses array to store data
const showInputError = req.flash('showInputError')[0] || false
res.render('index', { showInputError })
})

Let’s try to have empty input.

Result:

Can we have other approaches?

Feel free to skip this part if you are not interested to know the problems of other approaches.

Before using flash message, I thought of two possible approaches for passing showInputError after res.redirect():

  • Approach 1: Directly store the value of showErrorInput in session without using flash
  • Approach 2: Store the value of showErrorInput in URL query. Instead of redirecting the user to localhost:3000/, redirect him to http://localhost:3000/?showInputError=true

Approach 1: Store showErrorInput in session directly

Before I worked with flash message, I tried another approach, only storing error message in session. Like this:

app.post('/', (req, res) => {
const { username, password } = req.body
// Prevent empty input
if (username.trim() === '' || password.trim() === '') {
// Directly store showInputError in session
req.session.showInputError = true
return res.redirect('/')
}
// Validate input
const targetUser = users.find(
(user) => user.username === username && user.password === password
)
// Wrong username or password
if (!targetUser) {
// Directly store showInputError in session
req.session.showInputError = true
return res.redirect('/')
}
req.session.showInputError = false
req.session.isAuth = true
req.session.username = targetUser.username
req.session.timestamps = []
res.redirect('/profile')
})

After that, the display of error message will be shown based on the showInputError inside stored session:

app.get('/', (req, res) => {  if (req.session.isAuth) {
return res.redirect('/profile')
}
const showInputError = req.session.showInputError || false // Clear session. Otherwise showInputError may still be true all the time
req.session.destroy((err) => {
if (err) {
return res.redirect('/')
}
})
res.clearCookie('connect.sid')
res.render('index', { showInputError })
})

From the code above, we can spot some problems:

  • I have to deliberately destroy the session data. Otherwise showInputError will always be true and stored in the user's session. When the user refreshes page after login fails for his first time, the page will keep showing "Invalid username and password!", instead of clearing the message.
  • Normally we use session to store use’s data, not the data for deciding the content of the page (i.e display of error message). Therefore, this approach may look confusing to other developers.

Approach 2: Encode showErrorInput in URL

app.get('/', (req, res) => {
if (req.session.isAuth) {
return res.redirect('/profile')
}
const showInputError = req.query.showInputError || false
res.render('index', { showInputError })
})
app.post('/', (req, res) => {
const { username, password } = req.body
// Prevent empty input
if (username.trim() === '' || password.trim() === '') {
const query = new URLSearchParams({ showInputError: true }).toString()
return res.redirect(`${req.headers.origin}/?${query}`)
}
// ... // Wrong username or password
if (!targetUser) {
const query = new URLSearchParams({ showInputError: true }).toString()
return res.redirect(`${req.headers.origin}/?${query}`)
}
// ...
})

In this approach, if the user’s input is incorrect, we redirect him to an URL with query showInputError=true. Based on the query, we check if we should render the error message.

This solution is problematic because if the user stays at localhost:3000/?showInputError=true and refreshes page, he will still stay at localhost:3000/?showInputError=true and the page will keep showing the error message.

Profile page

Back to our example, the user is now redirected to /profile page after executing the following code:

app.get('/profile', checkIsAuthAndAddTimestamp, (req, res) => {
const { username, timestamps } = req.session
res.render('profile', { username, timestamps })
})

Before we render /profile, we need to execute a middleware function for authentication. That’s because user may directly access /profile, instead of coming from the homepage.

const checkIsAuthAndAddTimestamp = (req, res, next) => {
if (req.session.isAuth) {
req.session.timestamps.push(new Date().getTime())
next()
} else {
res.redirect('/')
}
}

Render profile page with EJS variables

After going through the middleware, we can render the profile page. It shows the user’s username and his login records, which are stored in his session object before.

app.get('/profile', checkIsAuthAndAddTimestamp, (req, res) => {
const { username, timestamps } = req.session
res.render('profile', { username, timestamps })
})

We pass the variables to 'profile' EJS template and render them:

profile.ejs

<body>
<h2><%= username %></h2>
<p>Login time records:</p>
<ul>
<% for (const timestamp of timestamps) { %>
<li><%= new Date(timestamp) %></li>
<% } %>
</ul>
<form action="/logout" method="post">
<button type="submit">Sign out</button>
</form>
</body>

Get session data from Redis

Imagine we access '/profile' again now. We can get into the '/profile' page without being asked to login. That's because we have stored a session ID in the user's browser. Its session object has the key and value isAuth: true

The process works as mentioned below:

In terminal, we can use redis-cli for checking the existing data in Redis. For example, after I login successfully for the first time, Redis should have store a session data.

The session ID can also be found in my browser’s cookie now:

If I access '/profile' again, the login time record accumulates and all will be displayed on the page.

Log out

We are almost done!

The last feature is to log out when the user clicks on the ‘Sign out’ button. The code here is quite straightforward:

app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.redirect('/profile')
}
})
res.clearCookie('connect.sid')
res.redirect('/')
})

req.session.destroy is a method in express-session for destroying session data and res.clearCookie is a function to clear a specific cookie in the client's browser.

Complete logic of this example

Summary

This is a basic example showing how to implement session-based authentication with express-session, connect-redis and ejs. In the next article, I will share my learning on how to connect with mySQL database for storing username and password.

--

--