-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
233 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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="<div>Credentials valid?</div>" 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="<font style="font-size: 9px;">{Credentials}</font>" 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="<font style="font-size: 9px;">{ Session id }</font>" 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> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters