JWT Configuration
You’ve used JWT tokens before when working with Noroff APIs from the front-end - sending Authorisation headers with Bearer tokens.
JWT (JSON Web Token) is a standard way to prove a user is authenticated. When a user logs in successfully, we create a JWT token containing their user ID that expires after a set time. The user sends this token back in every subsequent request until it expires, proving they’re still authenticated without having to log in again.
JWT tokens need to be signed with a secret key to prevent tampering. This secret acts like a password that only our server knows - it’s used to create and verify tokens.
Add a JWT secret to our .env file:
JWT_SECRET=our-super-secret-key-change-this-in-production
Why We Need This
- Token security: The secret signs each token, so we can verify it wasn’t tampered with.
- Environment-specific: Different secrets for development, staging, and production.
- Never in code: Secrets must be in environment variables, never hardcoded.
- Long and random: Production secrets should be long, random strings.
Important: Never use the example secret above in production. Generate a strong, unique secret for each environment.
Creating JWT Utility Functions
We’ll create two helper functions:
generateToken: Creates JWT tokens when users log in.verifyToken: Checks if tokens are valid for authenticated requests.
Create a new file utils/jwt.ts:
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET!;
export function generateToken(userId: number) {
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: "24h" });
}
export function verifyToken(token: string) {
try {
return jwt.verify(token, JWT_SECRET) as { userId: number };
} catch (error) {
return null;
}
}
How These Functions Work
generateToken: Takes a user ID and creates a JWT token that expires in 24 hours.verifyToken: Takes a token string, validates it, and returns the user ID (or null if invalid/expired).- JWT_SECRET: The secret key that signs (adds a digital fingerprint to) and verifies all tokens to prevent tampering.
Creating the Login Route
Add the login endpoint to our routes/auth.ts file:
import { generateToken } from "../utils/jwt";
// User login
router.post("/login", validateLogin, async (req, res) => {
try {
const { email, password } = req.body;
// Find user by email
const [rows] = await pool.execute(
"SELECT id, username, email, password FROM users WHERE email = ?",
[email]
);
const users = rows as User[];
if (users.length === 0) {
return res.status(401).json({
error: "Invalid email or password",
});
}
const user = users[0];
// Verify password using bcrypt
const validPassword = await bcrypt.compare(password, user.password!);
if (!validPassword) {
return res.status(401).json({
error: "Invalid email or password",
});
}
// Generate JWT token
const token = generateToken(user.id);
// Return user info and token
const userResponse: UserResponse = {
id: user.id,
username: user.username,
email: user.email,
};
res.json({
message: "Login successful",
user: userResponse,
token,
});
} catch (error) {
console.error("Login error:", error);
res.status(500).json({
error: "Failed to log in",
});
}
});
Understanding the Login Process
Step-by-step Breakdown
- Input validation: The
validateLoginmiddleware validates the request data. - Database lookup: We search the database for a user with the provided email address. If no user exists, login fails.
- Password verification: bcrypt compares the plain text password from the request with the stored hash - this is secure because we never store actual passwords.
- Authentication decision: If the password matches, the user is authenticated; if not, we return a 401.
- Token generation: For successful logins, we create a JWT token containing the user’s ID and set it to expire in 24 hours.
- Secure response: We return the user’s information (excluding the password hash) and the JWT token for use in future authenticated requests.
When users log in successfully, we return a JWT token that the front-end will store and send in subsequent requests that require authentication, proving the user is authenticated.
Testing the Login Endpoint
Test the auth/login endpoint with a user you’ve already registered:
POST /auth/login
Content-Type: application/json
{
"email": ...
"password": ...
}
For Successful Login, Expect
- Status 200.
- Response containing user info (without the password).
- JWT token in the response - this will be on the
tokenproperty.
Test Login Failures
- Wrong email or password: Should return 401 “Invalid email or password”.
- Invalid email format: Should return 400 with validation error.