In any realistic interactive Web application you will come across the need of retaining information between individual pages. This is referred to as “maintaining state” or as the “persistence of data”. HTTP protocol does not support this, it is a stateless protocol - every page request starts in a blank state with no knowledge of data that was available on the previous page. Since HTTP is stateless by nature, web applications need to implement their own methods for maintaining state. There is several common strategies to implement this on the server-side.
Choosing the right method for maintaining state depends on the specific requirements of your application, such as the type of data being stored, security considerations, scalability, and whether the state is user-specific or shared across users. It’s common to use a combination of these methods to address different aspects of application state.
sequenceDiagram
participant Client
participant Server
Client->>Server: HTTP Request (initial visit)
Note over Server: Server decides to set a cookie
Server->>Client: HTTP Response with Set-Cookie header
Note over Client: Client stores the cookie
Client->>Server: Subsequent HTTP Request with cookie
Note over Server: Server reads cookie and personalizes response
Server->>Client: Personalized HTTP Response
sequenceDiagram
participant Client
participant Server
participant SessionStore
Client->>Server: HTTP Request (initial visit)
Note over Server: Server creates a session
Server->>SessionStore: Create new session data
Note over SessionStore: Stores session data
Server->>Client: HTTP Response with session ID ( e.g. cookie)
Note over Client: Client stores session ID
Client->>Server: Subsequent HTTP Request with session ID
Server->>SessionStore: Retrieve session data
Note over Server: Server personalizes response
Server->>Client: Personalized HTTP Response
https://example.com/app?user=johndoe&theme=dark.Storing state information in hidden fields within HTML forms. For example:
<form action="https://example.com/app">
<input type="hidden" name="user" value="johndoe">
<input type="hidden" name="theme" value="dark">
<input type="submit" value="Submit">
</form>
The process of authentication using JWT and subsequent requests with the token in typical client-server interaction:
sequenceDiagram
participant Client
participant Server
participant AuthServer
Client->>Server: HTTP Request with Credentials (login)
Note over Server: Validates credentials
Server->>AuthServer: Request for JWT
Note over AuthServer: Creates and signs JWT
AuthServer->>Server: JWT
Server->>Client: HTTP Response with JWT
Note over Client: Stores JWT (usually in memory)
Client->>Server: Subsequent HTTP Request with JWT in header
Note over Server: Verifies JWT
alt JWT valid
Server->>Client: Authorized Response
else JWT invalid or expired
Server->>Client: Unauthorized Response
end
Express is a minimalistic framework that does not provide any built-in mechanisms for maintaining state. However, it is easy to implement your own state management using cookies, sessions, or other methods.
res.cookie to set cookies in the response headers. (Tutorialspoint cookie example).In web applications, authentication is typically done by verifying a username and password combination. Authorization is typically done by checking the user’s role or permissions for the requested resource.
.env file: JWT_SECRET=...
crypto module to generate a random stringPOST /api/auth/login that accepts a username and password in the request body.
routes/auth-router.js, controller method to controllers/auth-controller.js, and use the user model to query the database or create a new model for authentication.In the user model implement a method for verifying the username and password combination and returning the user object if found:
...
const selectUserByNameAndPassword = async (user) => {
try {
const sql = `SELECT user_id, username, email, user_level_id
FROM Users WHERE username = ? AND password = ?`;
...
...
In the auth controller implement token generation for the logged in user, something like this:
import jwt from 'jsonwebtoken';
import {selectUserByNameAndPassword} from '../models/user-model.js';
import 'dotenv/config';
const postLogin = async (req, res) => {
console.log('postLogin', req.body);
const user = await login(req.body);
if (user) {
const token = jwt.sign(user, process.env.JWT_SECRET, {expiresIn: '24h'});
res.json({...user, token});
} else {
res.sendStatus(401);
}
};
...
jsonwebtoken is used to generate a JWT token and then token is sent back to the client along with the user object.Create a middleware for handling requests to endpoints where authentication is needed middlewares/authentication.js
import jwt from 'jsonwebtoken';
import 'dotenv/config';
const authenticateToken = (req, res, next) => {
console.log('authenticateToken', req.headers);
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
console.log('token', token);
if (token == undefined) {
return res.sendStatus(401);
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(403).send({message: 'invalid token'});
}
};
export {authenticateToken};
Add a new route handler for GET /api/auth/me that returns the user object based on the token in the request header (this can be used to check if the token stored on the client-side is valid and get the user info):
// router:
...
import {getMe, postLogin} from '../controllers/auth-controller.js';
import {authenticateToken} from '../middlewares/authentication.js';
...
authRouter.route('/me').get(authenticateToken, getMe);
...
// controller:
const getMe = async (req, res) => {
console.log('getMe', req.user);
if (req.user) {
res.json({message: 'token ok', user: req.user});
} else {
res.sendStatus(401);
}
};
Test login and the protected route with VS Code REST Client (or Postman):
### Post login
POST http://localhost:3000/api/auth/login
content-type: application/json
{
"username": "JohnDoe",
"password": "to-be-hashed-pw1"
}
### Get my user info
GET http://localhost:3000/api/auth/me
Authorization: Bearer <put-your-token-from-login-response-here>
getMe in authRouter).
req.user object. For example: // user-router.js
...
userRouter.route('/:id').put(authenticateToken, putUser);
...
// user-controller.js
...
const putUser = async (req, res) => {
// get user id from token
const token_user_id = req.user.user_id;
// get user id from request
const user_id = req.params.id;
// check that user is updating own data
if (token_user_id !== user_id) {
return res.status(403).json({error: 403, message: 'forbidden'});
}
...
...
npm i bcryptjs (bcrypt is another option but it has more dependencies)import bcrypt from 'bcryptjs';const salt = await bcrypt.genSalt(10);const hashedPassword = await bcrypt.hash(myPlaintextPassword, salt);bcrypt for password check in auth-controller.js
const match = await bcrypt.compare(myPlaintextPassword, hash); (returns a boolean)authentication
POST /api/auth/loginPUT /api/entries/:id - only entry owner can update entryDELETE /api/entries/:id - only entry owner can delete entryPUT /api/users/ - users can update only their own user infoREADME.mdGET /api/entries - list only user’s own entries: get all entries by user id from tokenGET /api/entries/stats - get some statistics about entries like sleep time average, etc.DELETE and UPDATE SQL queries in models so that queries will also check that the owner of the item (user_id) matches the user_id property in the req.user object. req.user is decoded from the token and needs to be passed as a parameter from controller to corresponding model method.DELETE and UPDATE SQL queries into model functions that do not check the user_id property. Instead, you need to check that the user id from the token belongs to an admin user.Returning: See related assignment in Oma.