How to build a login flow in Next.js 15 (sessions, cookies, CSRF, and the timing attack nobody talks about)

The login flow is the most critical endpoint in any web application, and it’s the one that attackers probe the most. A single vulnerability in the login process can lead to a catastrophic breach of user credentials, resulting in identity theft, data breaches, and reputational damage.

building a login flow

You may also enjoy reading: "11 Unlikely Ways AI Tools Help Mediocre North Korean Hackers Steal Millions".

Implementing a Secure Login Flow in Next.js 15

To implement a secure login flow in Next.js 15, we need to follow these steps:

1. Use a Robust Hashing Algorithm

Use a robust hashing algorithm, such as bcrypt, to store passwords securely in the database.

Example:

javascript
const bcrypt = require(‘bcrypt’);
const saltRounds = 10;
const hashedPassword = bcrypt.hashSync(password, saltRounds);

2. Create a Session Token

Create a unique session token on the server-side and store it in the database.

Example:

javascript
const sessionToken = crypto.randomBytes(32).toString(‘hex’);
const session = await db.sessions.create({
token: sessionToken,
userId: userId,
expiresAt: expiresAt,
});

3. Set the Session Cookie

Set the session token as a cookie on the client-side, using a secure protocol (HTTPS) and a secure flag.

Example:

javascript
res.cookie(‘session’, sessionToken, {
httpOnly: true,
secure: true,
sameSite: ‘strict’,
});

4. Implement CSRF Protection

Implement a token-based approach to protect against CSRF attacks.

Example:

javascript
const csrfToken = crypto.randomBytes(32).toString(‘hex’);
res.cookie(‘csrfToken’, csrfToken, {
httpOnly: true,
secure: true,
sameSite: ‘strict’,
});

5. Use Constant-Time Comparison

Use a technique called “constant-time comparison” to protect against timing attacks.

Example:

javascript
const comparePassword = (password, storedPassword) => {
const startTime = Date.now();
const result = bcrypt.compareSync(password, storedPassword);
const endTime = Date.now();
return result && (endTime – startTime) <= 10;
};

6. Handle Form Submission

Handle form submission by verifying the user’s credentials and creating a session token if the credentials are valid.

Example:

javascript
const handleFormSubmission = async (req, res) => {
const { email, password } = req.body;
const user = await db.users.findOne({
where: {
email,
},
});
if (!user) {
return res.status(401).json({ error: ‘Invalid email or password’ });
}
const isValidPassword = await comparePassword(password, user.password);
if (!isValidPassword) {
return res.status(401).json({ error: ‘Invalid email or password’ });
}
const sessionToken = crypto.randomBytes(32).toString(‘hex’);
const session = await db.sessions.create({
token: sessionToken,
userId: user.id,
expiresAt: expiresAt,
});
res.cookie(‘session’, sessionToken, {
httpOnly: true,
secure: true,
sameSite: ‘strict’,
});
return res.json({ message: ‘Login successful’ });
};

7. Validate User Input

Validate user input to prevent common web vulnerabilities, such as SQL injection and cross-site scripting (XSS).

Example:

javascript
const validateUserInput = (req, res) => {
const { email, password } = req.body;
if (!email ||!password) {
return res.status(400).json({ error: ‘Email and password are required’ });
}
if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/.test(email)) {
return res.status(400).json({ error: ‘Invalid email format’ });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters long' });
}
};

8. Implement Password Reset

Implement a password reset feature that allows users to reset their passwords in case they forget their current password.

Example:

javascript
const handlePasswordReset = async (req, res) => {
const { email } = req.body;
const user = await db.users.findOne({
where: {
email,
},
});
if (!user) {
return res.status(401).json({ error: ‘Invalid email’ });
}
const resetToken = crypto.randomBytes(32).toString(‘hex’);
const resetTokenHash = await bcrypt.hash(resetToken, saltRounds);
await db.users.update({
resetTokenHash,
}, {
where: {
id: user.id,
},
});
res.json({ message: ‘Password reset link sent to your email’ });
};

9. Implement Two-Factor Authentication (2FA)

Implement 2FA to provide an additional layer of security for users.

Example:

javascript
const handle2FA = async (req, res) => {
const { code } = req.body;
const user = await db.users.findOne({
where: {
id: req.user.id,
},
});
if (!user) {
return res.status(401).json({ error: ‘Invalid user’ });
}
const isValidCode = await verify2FACode(code, user);
if (!isValidCode) {
return res.status(401).json({ error: ‘Invalid code’ });
}
res.json({ message: ‘2FA successful’ });
};

10. Implement Session Expiration

Implement session expiration to prevent users from staying logged in indefinitely.

Example:

javascript
const handleSessionExpiration = async (req, res) => {
const sessionToken = req.cookies.session;
const session = await db.sessions.findOne({
where: {
token: sessionToken,
},
});
if (!session) {
return res.status(401).json({ error: ‘Invalid session token’ });
}
if (session.expiresAt < Date.now()) {
return res.status(401).json({ error: 'Session has expired' });
}
res.json({ message: 'Session has not expired' });
};

11. Implement Session Revocation

Implement session revocation to prevent users from staying logged in after they’ve been logged out.

Example:

javascript
const handleSessionRevocation = async (req, res) => {
const sessionToken = req.cookies.session;
await db.sessions.destroy({
where: {
token: sessionToken,
},
});
res.clearCookie(‘session’);
res.json({ message: ‘Session revoked’ });
};

12. Implement CSRF Token Validation

Implement CSRF token validation to prevent CSRF attacks.

Example:

javascript
const validateCSRFToken = (req, res) => {
const csrfToken = req.cookies.csrfToken;
if (!csrfToken) {
return res.status(401).json({ error: ‘CSRF token is missing’ });
}
const isValidCSRFToken = await verifyCSRFToken(csrfToken);
if (!isValidCSRFToken) {
return res.status(401).json({ error: ‘CSRF token is invalid’ });
}
};

13. Implement Password Hashing

Implement password hashing to store passwords securely in the database.

Example:

javascript
const hashPassword = async (password) => {
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
};

14. Implement Password Verification

Implement password verification to check if the provided password matches the stored password.

Example:

javascript
const verifyPassword = async (password, storedPassword) => {
const isValidPassword = await bcrypt.compare(password, storedPassword);
return isValidPassword;
};

15. Implement Secure Cookie Settings

Implement secure cookie settings to prevent cookie tampering and session hijacking.

Example:

javascript
const setSecureCookie = (res, cookieName, cookieValue) => {
res.cookie(cookieName, cookieValue, {
httpOnly: true,
secure: true,
sameSite: ‘strict’,
});
};

In conclusion, building a secure login flow in Next.js 15 requires a deep understanding of the intricacies involved in handling user credentials, sessions, and cookies. By following the steps outlined in this article, you can create a robust login flow that protects your users’ sensitive information and prevents common web vulnerabilities. Remember to always validate user input, implement password hashing and verification, and use secure cookie settings to prevent cookie tampering and session hijacking.

Add Comment