Securing Your In-Memory Application
Never, Ever, Store Plain-Text Passwords
If an attacker gains access to your database and you have stored passwords in plain text, you have failed. The attacker now has the keys to your users' digital lives, as people often reuse passwords.
Storing plain-text passwords is the single greatest sign of an amateur developer. We are architects.
A hash function is a one-way street. It's a mathematical algorithm that takes an input (the password) and produces a unique, fixed-length string of characters (the hash).
We store the hash, not the password.
Simple hashing isn't enough (due to "rainbow table" attacks). We need to add a salt—a random string of characters unique to each user—to the password before hashing it.
The `bcrypt` library handles all of this for us automatically and securely. It is the industry standard.
npm install bcrypt
It provides two crucial functions:
const bcrypt = require('bcrypt');
const saltRounds = 10; // The "cost factor". Higher is slower but more secure.
async function registerUser(password) {
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Store `hashedPassword` in your database
console.log('Stored Hash:', hashedPassword);
return hashedPassword;
}
async function loginUser(submittedPassword, storedHash) {
const isMatch = await bcrypt.compare(submittedPassword, storedHash);
// isMatch will be true or false
console.log('Passwords match:', isMatch);
}
The Modern Standard for APIs
Traditional web apps use sessions, where the server stores a user's login state in memory. This violates the REST principle of statelessness.
In a large-scale application with multiple servers, how does Server B know that the user logged in on Server A? This creates a scaling bottleneck.
With token-based authentication, the server does not store anything. After a user logs in, the server gives them a token (a signed piece of data).
For every subsequent request to a protected resource, the client must include this token in the request header. The server can then verify the token's authenticity without needing to look up a session store.
This is truly stateless and infinitely scalable.
JWT (pronounced "jot") is an open standard for creating compact and self-contained tokens for securely transmitting information between parties as a JSON object.
A JWT consists of three parts, separated by dots:
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTYxNjQ0NjE5N30.c_p_4a...
{ "alg": "HS256", "typ": "JWT" }
{ "userId": 1, "role": "admin", "iat": 1616446197 }
The signature is what makes a JWT secure.
When the server receives a token, it re-calculates the signature using the header, the payload, and its secret key. If the re-calculated signature matches the signature on the token, the server knows that the token is authentic and has not been tampered with.
If an attacker changes the payload (e.g., `"role": "user"` to `"role": "admin"`), the signature will no longer match, and the server will reject the token.
Creating and Verifying Tokens
We will use the standard `jsonwebtoken` library.
npm install jsonwebtoken bcrypt
We also need a secret key. This should be a long, random, and private string stored securely as an environment variable (we'll learn more about this later). For now, we'll define it in our code.
When a new user registers, we hash their password before storing it.
const bcrypt = require('bcrypt');
// ...
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
// In a real app, you'd check if the user already exists
const hashedPassword = await bcrypt.hash(password, 10);
const newUser = {
id: nextId++,
name,
email,
password: hashedPassword // Store the hash!
};
users.push(newUser);
res.status(201).send('User registered successfully');
} catch {
res.status(500).send();
}
});
When a user logs in, we compare their submitted password to the stored hash. If they match, we generate and send back a JWT.
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'your_super_secret_key_that_is_very_long';
// ...
router.post('/login', async (req, res) => {
const user = users.find(u => u.email === req.body.email);
if (!user) return res.status(400).send('Invalid email or password.');
const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).send('Invalid email or password.');
// Create the token payload
const payload = { userId: user.id, name: user.name };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
The Authentication Gatekeeper
We will write a custom middleware function that will act as a guard for our protected routes.
The flow for a protected request:
function authMiddleware(req, res, next) {
// The header format is "Bearer TOKEN"
const authHeader = req.header('Authorization');
if (!authHeader) return res.status(401).send('Access denied. No token provided.');
const token = authHeader.split(' ')[1];
if (!token) return res.status(401).send('Access denied. Token format is invalid.');
try {
// jwt.verify will throw an error if the token is invalid or expired
const decodedPayload = jwt.verify(token, JWT_SECRET);
// Add the payload to the request object
req.user = decodedPayload;
next();
} catch (ex) {
res.status(400).send('Invalid token.');
}
}
You can apply the middleware to any route that needs to be protected. Only authenticated users will be able to access it.
// In userRoutes.js
// This route is public
router.get('/', (req, res) => { /* get all users */ });
// This route is now protected. The authMiddleware runs first.
router.get('/:id', authMiddleware, (req, res) => {
// Because of the middleware, we now have access to req.user
console.log('Authenticated user:', req.user);
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).send('User not found');
res.json(user);
});
// We can also protect our DELETE route
router.delete('/:id', authMiddleware, deleteUser);
You will be given the complete, working CRUD API for `users` from last week. Your task is to implement a full authentication system from scratch.