In this post, we will learn how to authenticate users using session cookies in a Node.js server application.
When a user logs into our application, we need to know who they are across all our HTTP methods and routes.
One way to do this is to store the users “session”. A session is started once a user logs in, and expires some time after that.
Each logged in user has some reference to the session (called a cookie), which they send with their requests. We then use this reference to look up the user that it belongs to and return information specific to them.
If you just want to see the source code for this tutorial, you can find it here
Overview
In this post, we will look at how to create and store the session of a logged in user as a cookie on the browser.
We will build an application with a /signin
and a /welcome
route.
- The
/signin
route will accept a users username and password, and set a session cookie if successful. - The
/welcome
route will be a simple HTTPGET
route which will show a personalized message to the currently logged in user.
The session information of the user will be stored in our applications local memory.
We will also assume that the users that are signing in have already created their username-password credentials on our application.
Creating the HTTP Server
Before we begin, we need to install the libraries that we will use for this example:
npm install express body-parser cookie-parser uuid
Let’s create an index.js
file and initialize the HTTP server with the required routes:
const express = require('express')
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser')
const { signinHandler, welcomeHandler, refreshHandler } = require('./handlers')
const app = express()
app.use(bodyParser.json())
app.use(cookieParser())
app.post('/signin', signinHandler)
app.get('/welcome', welcomeHandler)
app.listen(8080)
We can now define the /signin
and /welcome
routes.
Creating Session Tokens
We will be creating a new session token every time a user signs in.
The /signin
route will take the users credentials and log them in.
In order to make this simple, we’re storing the users information as an in-memory object in our code:
const users = {
"user1": "password1",
"user2": "password2"
}
For now, there are only two valid users in our application: user1
, and user2
.
We also need to define an object to store information about each users session:
// each session contains the username of the user and the time at which it expires
class Session {
constructor(username, expiresAt) {
this.username = username
this.expiresAt = expiresAt
}
// we'll use this method later to determine if the session has expired
isExpired() {
this.expiresAt < (new Date())
}
}
// this object stores the users sessions. For larger scale applications, you can use a database or cache for this purpose
const sessions = {}
Next, we can implement the Signin
HTTP handler:
const signinHandler = (req, res) => {
// get users credentials from the JSON body
const { username, password } = req.body
if (!username) {
// If the username isn't present, return an HTTP unauthorized code
res.status(401).end()
return
}
// validate the password against our data
// if invalid, send an unauthorized code
const expectedPassword = users[username]
if (!expectedPassword || expectedPassword !== password) {
res.status(401).end()
return
}
// generate a random UUID as the session token
const sessionToken = uuid.v4()
// set the expiry time as 120s after the current time
const now = new Date()
const expiresAt = new Date(+now + 120 * 1000)
// create a session containing information about the user and expiry time
const session = new Session(username, expiresAt)
// add the session information to the sessions map
sessions[sessionToken] = session
// In the response, set a cookie on the client with the name "session_cookie"
// and the value as the UUID we generated. We also set the expiry time
res.cookie("session_token", sessionToken, { expires: expiresAt })
res.end()
}
If a user logs in successfully, this handler will set a cookie on the client side, and inside its own local memory. Once a cookie is set on a client, it is sent along with every subsequent request.
We can use UUIDs to represent session tokens, since they are uniform in structure, and difficult to guess.
Authenticating Users Through Session Cookies
Now that we have persisted the users session information on their client (in the form of the session_token
cookie) and the server, we can write our /welcome
handler to handle user specific information.
Since logged in clients have session information stored on their end as cookies, we can use it to:
- Authenticate subsequent user requests
- Get information about the user making the request
Let’s write our /welcome
handler to do that:
const welcomeHandler = (req, res) => {
// if this request doesn't have any cookies, that means it isn't
// authenticated. Return an error code.
if (!req.cookies) {
res.status(401).end()
return
}
// We can obtain the session token from the requests cookies, which come with every request
const sessionToken = req.cookies['session_token']
if (!sessionToken) {
// If the cookie is not set, return an unauthorized status
res.status(401).end()
return
}
// We then get the session of the user from our session map
// that we set in the signinHandler
userSession = sessions[sessionToken]
if (!userSession) {
// If the session token is not present in session map, return an unauthorized error
res.status(401).end()
return
}
// if the session has expired, return an unauthorized error, and delete the
// session from our map
if (userSession.isExpired()) {
delete sessions[sessionToken]
res.status(401).end()
return
}
// If all checks have passed, we can consider the user authenticated and
// send a welcome message
res.send(`Welcome ${userSession.username}!`).end()
}
From the code, we can see that our welcome handler gives us an “unauthorized” (or 401
) status under certain circumstances:
- If there is no
session_token
cookie along with the request (which means that the requestor hasn’t logged in) - If the session token is not present in memory (which means that the requestor is sending us an invalid session token)
- If the session has expired
Session based authentication keeps your users sessions secure in a couple of ways:
- Since the session tokens are randomly generated, its near-impossible for a malicious user to brute-force their way into a users session.
- If a users session token is compromised somehow, it cannot be used after its expiry. This is why the expiry time is restricted to small intervals (a few seconds to a couple of minutes)
Refreshing Session Tokens
Since the expiry time of a session token is kept small, we need to issue a new token often to keep the user logged in.
Of course, we cannot expect the user to login every time their token expires. To solve this, we can create another route that accepts the users current session token, and issues a new session token with a renewed expiry time.
Lets define a handler to renew the users session token every time they hit the /refresh
route in our application
const refreshHandler = (req, res) => {
// (BEGIN) The code from this point is the same as the first part of the welcomeHandler
if (!req.cookies) {
res.status(401).end()
return
}
const sessionToken = req.cookies['session_token']
if (!sessionToken) {
res.status(401).end()
return
}
userSession = sessions[sessionToken]
if (!userSession) {
res.status(401).end()
return
}
if (userSession.isExpired()) {
delete sessions[sessionToken]
res.status(401).end()
return
}
// (END) The code until this point is the same as the first part of the welcomeHandler
// create a new session token
const newSessionToken = uuid.v4()
// renew the expiry time
const now = new Date()
const expiresAt = new Date(+now + 120 * 1000)
const session = new Session(userSession.username, expiresAt)
// add the new session to our map, and delete the old session
sessions[newSessionToken] = session
delete sessions[sessionToken]
// set the session token to the new value we generated, with a
// renewed expiration time
res.cookie("session_token", newSessionToken, { expires: expiresAt })
res.end()
}
We can now add this to the rest of our routes:
app.post('/refresh', refreshHandler)
Logging Out Our Users
If the user decides to logout of our application, we need to remove their session token from our storage as well as the users client.
Let’s create a handler to implement this:
const logoutHandler = (req, res) => {
if (!req.cookies) {
res.status(401).end()
return
}
const sessionToken = req.cookies['session_token']
if (!sessionToken) {
res.status(401).end()
return
}
delete sessions[sessionToken]
res.cookie("session_token", "", { expires: new Date() })
res.end()
}
We can now add this to the rest of our routes:
app.get('/logout', logoutHandler)
Running Our Application
To start the application, we can run:
node index.js
Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:
POST http://localhost:8080/signin
{"username":"user2","password":"password2"}
You can now try hitting the welcome route from the same client to get the welcome message:
GET http://localhost:8080/welcome
Hit the refresh route, and then inspect the clients cookies to see the new value of the session_token
:
POST http://localhost:8080/refresh
Finally, call the logout route to clear session data:
GET http://localhost:8080/logout
Calling the welcome and refresh routes after this will result in a 401
error.
You can find the working source code for this example on Github.