Skip to content

Commit

Permalink
Added session management section
Browse files Browse the repository at this point in the history
  • Loading branch information
stasel committed Feb 8, 2024
1 parent 6052c8c commit 4f02403
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 4 deletions.
84 changes: 84 additions & 0 deletions node-js/assets/login-diagram.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<mxfile host="app.diagrams.net" modified="2024-02-08T15:55:49.979Z" agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:122.0) Gecko/20100101 Firefox/122.0" etag="EiG4p-vm1pmxPlWUsYUm" version="23.0.2" type="device">
<diagram name="Page-1" id="1Q6fCelEe2z6ASVgYXHQ">
<mxGraphModel dx="903" dy="633" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="PDGte3Nl42UgdPUzoMXS-17" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-1" target="PDGte3Nl42UgdPUzoMXS-12">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-1" value="Login endpoint" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="340" y="270" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-8" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-5" target="PDGte3Nl42UgdPUzoMXS-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-10" value="POST request" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-8">
<mxGeometry x="-0.1" relative="1" as="geometry">
<mxPoint x="-2" y="-10" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-5" value="Client" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1">
<mxGeometry x="170" y="260" width="30" height="60" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-12" value="&lt;div&gt;Credentials valid?&lt;/div&gt;" style="rhombus;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="520" y="250" width="90" height="80" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-15" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0.5;exitY=0;exitDx=0;exitDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-12" target="PDGte3Nl42UgdPUzoMXS-5">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="565" y="180" />
<mxPoint x="185" y="180" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-16" value="Return HTTP error" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-15">
<mxGeometry x="0.0057" y="-1" relative="1" as="geometry">
<mxPoint x="14" y="1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-18" value="No" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-15">
<mxGeometry x="-0.8976" y="-1" relative="1" as="geometry">
<mxPoint y="8" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-20" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-12" target="PDGte3Nl42UgdPUzoMXS-25">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="660" y="290" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-21" value="yes" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-20">
<mxGeometry x="-0.5848" y="-2" relative="1" as="geometry">
<mxPoint x="8" y="-2" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-24" value="&lt;font style=&quot;font-size: 9px;&quot;&gt;{Credentials}&lt;/font&gt;" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;" vertex="1" parent="1">
<mxGeometry x="230" y="300" width="60" height="30" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" edge="1" parent="1" source="PDGte3Nl42UgdPUzoMXS-25">
<mxGeometry relative="1" as="geometry">
<mxPoint x="180" y="340" as="targetPoint" />
<Array as="points">
<mxPoint x="730" y="360" />
<mxPoint x="180" y="360" />
<mxPoint x="180" y="340" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-30" value="Return HTTP success" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="PDGte3Nl42UgdPUzoMXS-29">
<mxGeometry x="-0.4313" y="-1" relative="1" as="geometry">
<mxPoint x="-104" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-25" value="Create new session" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="690" y="270" width="80" height="40" as="geometry" />
</mxCell>
<mxCell id="PDGte3Nl42UgdPUzoMXS-28" value="&lt;font style=&quot;font-size: 9px;&quot;&gt;{ Session id }&lt;/font&gt;" style="shape=document;whiteSpace=wrap;html=1;boundedLbl=1;" vertex="1" parent="1">
<mxGeometry x="460" y="370" width="60" height="30" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Binary file added node-js/assets/login-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 147 additions & 2 deletions node-js/session-management.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,149 @@
# Session management
In this section we will learn how to implement user login, logout and protected endpoints.
## What is a session
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.

## Manage user sessions
In this section, we will learn how to create new sessions (login), use them (protected endpoints) and destroy them (logout).

### How can we identify a user?
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](https://developer.mozilla.org/en-US/docs/Web/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` 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 his session ID to the server in order to proof his identity.

Using a session ID is very flexible because we can delete it and add features like expiration date.

### Creating a session (login)

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:
![Login diagram](/node-js/assets/login-diagram.png)


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](/node-js/user-registration.md) section.

```javascript
import { v4 as generateUUID } from 'uuid';

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 = generateUUID();

// 4. Add the new session to the session database
sessions[newSession] = 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:
```json
{
"sessionId": "67e607ff-9df0-48e7-837f-13c3ad5e267f"
}
```

This unique ID was generated by a very useful library called [uuid](https://www.npmjs.com/package/uuid). But it is not required to use it. You can 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[newSession] = username;`.

For the next request, the user is no longer require to send his credentials, it is enough to send the session ID and the server will know the connected username.

#### What should the client do with the session Id?
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. One option to is to save the session ID in [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) or [`sessionStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).

### Creating protected endpoints

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`:

```javascript
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 his private messages. In our case, we just return a nice success message with the username.

#### How should the client send the session ID?
There are many ways that the client can send us back the session ID. Some applications using `cookies`, others use `HTTP Headers`. You can even pass the session ID in the URL.

In our example, we will a special [HTTP Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization). This header follows the following format:

```Authorization: <auth-scheme> <authorization-parameters>```

* Authentication scheme can be one of a [predefined list](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes).
* Authorization parameters is the secret value that identifies a user like session ID.

And here is how we implemented the `getSessionId` function:
```javascript
// 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.


### Deleting a session (logout)

Lastly, we would like to let the user to securely log out and delete his session ID.

```javascript
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 his 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.

This page is under construction...
4 changes: 2 additions & 2 deletions node-js/user-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ In this example, we store the user passwords in the user database as-is. This is
## Further improvements
Here are few suggestion on how we can improve the registration endpoint:

1. Add a random user ID to the user object. You can use the popular [uuid](https://www.npmjs.com/package/uuid) library.
1. The User database is stored in memory. This means that if we kill the NodeJS application, the user database will be deleted. In order to fix this, save the user database into a file in a JSON format using the [File system](https://nodejs.org/api/fs.html) module.
1. Add additional checks to the `isValidUser` function. Check for the validity of the email, minimum password length, check the user database if the username has been previously registered.
1. Create HTML client with a registration form and use `fetch` to send the POST request to the server.
1. Handle user passwords with care after reading [How to store user passwords](/node-js/storing-passwords.md)

1. Handle user passwords with care after reading [How to store user passwords](/node-js/storing-passwords.md)

0 comments on commit 4f02403

Please sign in to comment.