A user session refers to a period of interaction between a user and a software application, or website. During a user session, a user performs certain actions or tasks within the system or application. The session begins when the user logs in and ends when the user logs out, closes the application or when the session expires.
In a multi user application, it is important to recognize which user is making the request and perform the required actions for the user. For example, in a web shop application, it is crucial to know which user is now checking out so we can send him the correct items to the correct address.
In this section, we will learn how to create new sessions (login), use them (protected endpoints) and destroy them (logout).
When our backend application receives a HTTP request from the client, how can the application tell which user sent this request?
One solution for this problem is to let the browser send the user's credentials (username and password) in every request. This way, we can authenticate and identify the user in every request. Sending the secret credentials in every request can be implemented with Basic HTTP Authentication. It is generally not a good idea to use this method due to security concerns.
A better solution is for the application to issue a secret session ID
(Also called token
or access token
) after a successful login. This session ID is a long random string that does not makes sense to the user. The server has a connection between the session ID and the user itself. After login, the user will send only their session ID to the server in order to prove their identity.
Using a session ID is very flexible because we can delete it and add features like an expiration date, making the user log in again after their session expires.
First, let's try to implement the /login
endpoint and allow our users to authenticate to the application and receive the session ID.
In the following diagram, we show the general login process to implement:
Note: We assume that there is already a way for users to register and we have a user database to check for valid passwords. We explain that part in the User Registration section.
const sessions = {};
app.post('/login', async (req, res) => {
// 1. get login details from the body
const { username, password } = req.body;
// 2. Check if the username / password combination is correct.
if(!checkPassword(username, password)) {
res.status(401).json({ message: 'Invalid username / password combination' }).end();
return;
}
// 3. The password is correct - create a new user session
const sessionId = crypto.randomUUID();
// 4. Add the new session to the session database
sessions[sessionId] = username;
// 5. Return the session ID to the client
res.status(200).json({ sessionId }).end();
});
On a successful login, we will receive back a response with the unique session ID:
{
"sessionId": "67e607ff-9df0-48e7-837f-13c3ad5e267f"
}
This unique ID was generated by the built-in randomUUID()
function which is part of Node.JS crypto module. You can use any other method to generate your own random string as long as it's very difficult to guess.
This unique session ID is saved in our session database and connected to a specific user. The connection is made with this line: sessions[sessionId] = username;
.
For the next request, the user is no longer required to send their credentials, it is enough to send the session ID and the server will know the connected username.
The client (browser in our case), should save the session ID for future requests. This session ID is secret and should not be shared with anyone. The most common option is to save the session ID in localStorage
or sessionStorage
.
Now that we have a login process and we keep track of all the sessions, we can now identify users from their HTTP request.
We can now protect our endpoints and allow only authenticated users to user those endpoints. One example for a protected endpoint is the following GET /profile
:
app.get('/profile', async (req, res) => {
// 1. Get session ID from the request
const sessionId = getSessionId(req);
// 2. Get the username from the session database
const username = sessions[sessionId];
if(!sessionId || !username) {
res.status(401).json({ message: 'Not logged in' }).end();
return;
}
// 3. Send a message to the client
const message = `Hello, you are logged in as ${username}!`;
res.status(200).json({ message }).end();
});
This code is using the session ID sent by the client to connect the HTTP request to a specific user. After the connection is made, this endpoint can perform personalized action for this specific user like returning their private messages. In our case, we just return a nice success message with the username.
There are many ways that the client can send us back the session ID. Some applications use cookies
, others use HTTP Headers
. You can even pass the session ID in the URL.
In our example, we will use a special HTTP Authorization header. This header follows the following format:
Authorization: <auth-scheme> <authorization-parameters>
- Authentication scheme can be one of a predefined list.
- Authorization parameters is the secret value that identifies a user like session ID.
And here is how we implemented the getSessionId
function:
// Get session ID from the Authorization HTTP header
// The Authorization header should contain the session ID in the following format:
// Authorization: Bearer <session-id>
const getSessionId = (req) => {
const authorizationHeader = req.headers['authorization'];
if(!authorizationHeader) {
return null;
}
const sessionId = authorizationHeader.replace('Bearer ', '');
return sessionId.trim();
};
We use the common Bearer
scheme to extract the session ID from the HTTP header. It is important that the client application will send the session ID in this specific format.
Lastly, we would like to let the user to securely log out and delete his session ID.
app.post('/logout', async (req, res) => {
// 1. Get session ID from the request
const sessionId = getSessionId(req);
// 2. Check if the user is logged in
if(!sessions[sessionId]) {
res.status(401).json({ message: 'Not logged in' }).end();
return;
}
// 3. Remove the session from the session database
delete sessions[sessionId];
// 4. Send a message to the client
res.status(204).json().end();
});
To logout a user, we simply delete their session ID from the sessions database. Now that the session ID is deleted, it can no longer be used by the client to access any protected endpoint. The client will have to authenticate again to receive a new session ID.
In the previous section, we used a session ID
to identify users. This Session ID is a completely random string which is hard to guess. Because it is completely random and the client cannot get any information out of it, we call this an Opaque token
.
The disadvantage using an Opaque token
is the extra work the server has to do in order to translate the token into the user.
Remember this code?
app.get('/profile', async (req, res) => {
const sessionId = getSessionId(req);
const username = sessions[sessionId];
...
}
In this code, for every GET /profile
request, we search for the token in the sessions
object. In real world applications, we want use an external Database to save all our sessions. Accessing this session database is a lot slower than accessing this simple sessions
object.
What if we could save this extra step of searching in the sessions database?
One solution is to add more details to our session ID
. What if our session ID will now contain the username? for example:
67e607ff-9df0-48e7-837f-13c3ad5e267f___johnsmith
With this improvement, we can now directly get the username from the HTTP request without querying the sessions database. Success? Well not really.
If we do not verify that the session is real, a user can fake the session token and change the username to something else.
JWT (JSON Web Token) is solving this issue of verification. With JWT, it is possible to add information to our token in a way that the server can verify the token without accessing the session database.
Read more about JWT in JWT section