Burak Akdemir
8 min readMay 4, 2022

JWT Authentication along with CSRF prevention on Node.js Express

Github: https://github.com/kbrk/express_csrf_jwt_study

In this text, CSRF prevention and authentication with JWT are described with a simple example regardless of database and front-end implementations. All server-side operations are being handled through Postman. Postman is a testing application that is used as an HTTP client. An HTTP request can be sent without a frontend setup. https://www.postman.com/

Dev Dependencies:

nodemon: A tool that helps develop node.js based applications by restarting automatically the application when file changes are detected in the directory.

Dependencies:

csurf: A Node.js middleware that is used to prevent CSRF attacks. (Note: This module has been deprecated since September 2022 due to the security vulnerability reports. https://github.com/expressjs/csurf)

dotenv: A module for environmental configuration variables.

express: A backend web application framework for Node.js.

express-session: A session middleware for Express.

jsonwebtoken: An implementation of JWT for Node.js.

Environmental Configurations:

There should be a .env file in ./backend and its variables are;

./backend/.env

DOMAIN= http://localhost
PORT = 8080

CSRFT_SESSION_SECRET = somesupersecretcsrfsessionsecret
JWT_SECRET = somesupersecret
JWT_REFRESH_SECRET = somesupersecretrefresh

# jwt and csrf token expires in milliseconds
CSRFT_EXPIRESIN = 2400000
JWT_EXPIRESIN = 60000
JWT_REFRESH_EXPIRESIN = 240000

Let’s keep going within the server.js.

The Most Basic Configurations for server.js.

./backend /server.js

const express = require(“express”);
require(‘dotenv’).config();
const app = express();app.use(express.json()); //Used to parse incoming request with JSON payload.const port = process.env.PORT
const domain = process.env.DOMAIN
app.listen(port, ()=> {
console.log(`Listening at http://${domain}:${port}`)
});

When the server is started after the first configurations, the result must be like below.

npm run start:dev
npm run start:dev

If so, the next step is to create route and controller files and connect them.

Testing of Connection Between Routes and Controllers.

./backend/controllers/test.js

exports.test = (req, res, next) => {
res.status(200).json({result: true, message: 'Success..'});
}

./backend/routes/test.js

const express = require('express');
const router = express.Router();
const testController = require('../controllers/test');
router.post('/test', testController.test);module.exports = router;

./backend/server.js

// ** Routes requirements

const testRoutes = require("./routes/test");
//...

// ** Routes usage

app.use('/test', testRoutes);

If the response of the post request is like in the image below, it means that the connection between the route and the controller is set correctly.

Response to test request.

Then we can get to the main objectives.

Cross-Site Request Forgery (CSRF)

Cross-Site Request Forgery (CSRF) is a type of attack that occurs when a malicious website, email, blog, instant message, or program causes a user’s web browser to perform an unwanted action on a trusted site when the user is authenticated. Browser requests include session cookies, therefore, when a user is authenticated on a system, the system cannot decide if the request is sent from a trusted or a fake source. To mitigate the risk, CSRF tokens are added to all state-changing requests.

Purpose of CSRF Token in Using JWT

Some articles are focusing on server-side implementation for storing JWT. However, if client-side is chosen for storing, a JWT needs to be stored in relatively a safe place inside the browser and the local storage is not a perfect place for this. It’s vulnerable to any script inside the page. To keep it secure it can be stored inside an httpOnly cookies. This kind of cookie cannot be reached by javascript. This is also mentioned by the OWASP community.

It’s recommended not to store sensitive information in local storage.”, “Do not store session identifiers in local storage as the data is always accessible by JavaScript. Cookies can mitigate this risk using the httpOnly flag.”

Note: Do not forget that the limit of the cookies is about 4KB. Therefore, cookies are not appropriate for big JWTs

Although using the httpOnly cookie, you are not completely safe. Therefore, before the JWT implementation, CSRF protection has to be provided.

Csurf is a Node.js protection middleware in the Express framework. To generate a CSRF token, a token secret is necessary and there are two ways to store this. One of these is using cookies, which is enforce the Double Submit Cookie technique and requires the cookie-parser package, and the other is using session which requires one of express-session or cookie-session packages. For the session option, the secret is stored on the server-side. In this example, the session option is being used.

Session Configuration

The session middleware should be configured before the app uses the routes.

./backend/server.js

const session = require('express-session');//...const sessionConfig = session({
secret: process.env.CSRFT_SESSION_SECRET,
keys: ['some random key'],
resave: false,
saveUninitialized: false,
cookie: {
maxAge: parseInt(process.env.CSRFT_EXPIRESIN), // Used for expiration time.
sameSite: 'strict', // Cookies will only be sent in a first-party context. 'lax' is default value for third-parties.
httpOnly: true, //Ensures the cookie is sent only over HTTP(S)
domain: process.env.DOMAIN, //Used to compare against the domain of the server in which the URL is being requested.
secure: false // Ensures the browser only sends the cookie over HTTPS. false for localhost.
}
});
app.use(sessionConfig);
// ** Routes requirements
//...

The session middleware with the given options set how the session is generated and stored. This generated session id is stored in the cookie.

Note: By default, the generated session data is stored in MemoryStore on the server-side. However, the MemoryStore is not for a production environment. For the compatible session stores, the list below can be checked.

Configurations can be changed according to the use-case scenarios. Therefore, it is always better to see the documentation https://github.com/expressjs/session.

Generate and Check CSRF Tokens

The following code is an example for generating and checking CSRF tokens.

./backend/routes/test.js

router.get('/setCSRFToken', csrfProtection, (req, res, next) => {
const token = req.csrfToken();
res.send({csrfToken: token});
});

router
.post('/checkCSRFToken', csrfProtection, function (req, res) {
res.send({msg: 'CSRF Token is valid.'})
}); // If the token is invalid, it throws a 'ForbiddenError: invalid csrf token' error.

When setCSRFToken is run, a token is generated and sent in the response data and the session id is set in cookies. connect.sid is a default session id name given by the middleware. The common recommendation is not to use the default name of these types of data. Because default names can give clues about the framework or package.

Response to setCSRFToken request.

With the checkCSRFToken post request, the validation of the produced token is checked by the function of csrfProtection. For the validation control, the token value should be sent in the request through one of the options in https://github.com/expressjs/csurf#value. Here, it is sent in CSRF-Token header. If the token is valid, the process is continued with the next operation in the router but if not, it throws a ‘ForbiddenError: invalid csrf token’ error.

checkCSRFToken request and its response.

Note: If the token is sent in the body of the request, body-parser package can be used for parsing.

When the router uses the csrfProtection function, CSRF token validation control is provided and this control is used for every request located after itself.

./backend/routes/test.js

// ...
router
.use(csrfProtection);
// ...

JWT - Authorization through Access and Refresh Tokens

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

Using JWT for authorization is the most common scenario. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access all parts that are permitted by that token.

For more information: https://jwt.io/

It is generally recommended that the expiration time of the access token for authorization be as soon as possible. Thus, a refresh token that has a long expiration time is necessary for continuous authorization.

Generate JWT

generate-token.js is a middleware for generating JWT. The purpose is to produce access and refresh tokens according to the tokenType parameter.

/backend/middleware/generate-token.js

generate-token.js

For signin request, controller and route functions are defined. In the controller function, a user variable is manually assigned for testing.

./backend/controllers/test.js

const GT = require('../middleware/generate-token');//...exports.signin = async (req, res, next) => {
try {
const user = {
"_id": "someId123", "email": "test@mail.com"
} // user data is assigned manually for testing.
const resultAccess = await GT.generateToken(user, 1);
const resultRefresh = await GT.generateToken(user, 2);
const refresh_secret = process.env.JWT_REFRESH_SECRET;

await jwt.verify(resultRefresh.token, refresh_secret); // If the token is not valid, it throws an error.

res.status(200)
.cookie('access_token', resultAccess.token, resultAccess.cookie)
.cookie('refresh_token', resultRefresh.token, resultRefresh.cookie)
.json({
result: true, token: resultAccess.token, email: user.email, userId: user._id.toString()
});

} catch (err) {
res.status(500).json({result: false, message: "Something went wrong."});
return;
}
};

Remember, the router function should be located after the csrfProtection function if CSRF prevention is required.

.backend/routes/test.js

router.post(‘/signin’, testController.signin);

The response should be like below. In the cookie tab, besides the session id, there is an access token and a refresh token.

Response to signing request.

Through the authorizedSubmit request, authorization can be checked. If this request is made before access token generation, it returns an “Authentication failed” response.

The access_token and refresh_token data should be written into the Cookie header of the request.

authorizedSubmit request.

Validation of these data is controlled through another middleware called check-auth.

./backend/middleware/check-auth.js

check-auth.js

When the access token cannot be verified because of malformation or expiration, an error is thrown and in the catch block, a process for a new access token is begun if the token is expired. Otherwise, the process has to be finalised with an error in the condition of the malformed token. As long as the refresh token is valid, this process keeps running when the access token is invalid. At the end of the process, there is a new access token generated and this token is sent into the cookie section of the response as well. Thus, the authentication continues without interruption.

If both the access token and the refresh token are expired, the response is like below.

Invalid or Expired Token

That’s it for this post. It can be varied depending on the scenarios and circumstances. As you know, there is no single method for these kinds of subjects. I tried to explain one of them. I hope it was helpful. If you have any questions or recommendations, please share them with me.

Thank you for reading.