Home >Web Front-end >JS Tutorial >The Right Way to do Authentication in Node.js [uide]
Authentication is one of the most critical yet often misunderstood aspects of backend development. Due to its complexity, developers frequently turn to third-party solutions like Auth0 or Supabase. While these are excellent tools, building your own authentication system can provide greater flexibility and control.
In this guide, you'll learn how to implement a simple authentication middleware for your Express.js API service with minimal dependencies. By the end, you'll have:
This guide focuses on simplicity and avoids using packages like passport.js to reduce complexity.
First, create a PostgreSQL table to store user accounts:
CREATE TABLE users ( "id" SERIAL PRIMARY KEY, "username" VARCHAR(255) UNIQUE NOT NULL, "password" VARCHAR(255) NOT NULL, "email" VARCHAR(255) UNIQUE, "created_at" TIMESTAMP NOT NULL DEFAULT NOW() );
Next, create a JWT authentication middleware to protect API endpoints. This example uses symmetric encryption. For microservices architectures, consider using asymmetric encryption with a public/private key pair.
import jwt from "jsonwebtoken"; const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY as string; // Randomly generated. Min length: 64 characters export const protectedRoute: RequestHandler = async (req, _, next) => { const authHeader = req.header("authorization"); if (!authHeader) { return next(notAuthenticated()); } const accessToken = authHeader.replace(new RegExp("\b[Bb]earer\s"), ""); try { const { userId } = validateJWT(accessToken); const user = await userRepository.getUserById(parseInt(userId)); if (user) { req.user = user; next(); } else { next(invalidAccessToken()); } } catch (err) { next(invalidAccessToken()); } }; const validateJWT = (token: string, verifyOptions?: jwt.VerifyOptions) => { const jwtVerifyOptions = Object.assign( { algorithms: "HS256" }, verifyOptions, { issuer: "yourAPI.com", audience: "yourAPI.com:client", } ); return jwt.verify(token, JWT_SECRET_KEY, jwtVerifyOptions) as T; };
Use the middleware to secure routes:
import { protectedRoute } from "@/middleware/jwt"; router.get("/user", protectedRoute, async (req, res, next) => { const user = req.user!; res.json({ user }); });
Now, implement controllers for signup and login:
import argon from "argon2"; const signup = async (props) => { const { username, password, email } = props; await userRepo.getUser(username).then((res) => { if (res !== null) throw usernameNotAvailable(); }); const hashedPass = await argon.hash(password, { timeCost: 2, parallelism: 1, memoryCost: 19456, }); const newUser = await createUser({ username, hashedPass, email, }); const refreshToken = await generateRefreshToken(newUser.userId); const accessToken = generateAccessToken(newUser.userId); const { password: _, ...userRes } = newUser; return { user: userRes, accessToken, refreshToken }; };
const login = async (props) => { const { username, password } = props; const user = await getUser(username).then((res) => { if (res === null) throw invalidLoginCredentials(); return res; }); const isOk = await argon.verify(user.password, password); if (isOk) { const refreshToken = await generateRefreshToken(user.userId); const accessToken = generateAccessToken(user.userId); const { password: _, ...userRes } = user; return { user: userRes, accessToken, refreshToken }; } throw invalidLoginCredentials(); };
Refresh tokens provide long-term authentication. Let’s create a database table to store them:
CREATE TABLE refresh_tokens ( "id" SERIAL PRIMARY KEY, "token" UUID NOT NULL DEFAULT gen_random_uuid(), "token_family" UUID NOT NULL DEFAULT gen_random_uuid(), "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, "active" BOOLEAN DEFAULT true, "expires_at" TIMESTAMP NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT NOW() );
import jwt from "jsonwebtoken"; const JWT_SECRET_KEY = process.env.JWT_SECRET_KEY as string; // Randomly generated. Min length: 64 characters const generateAccessToken = (userId: number) => { const jwtSignOptions = Object.assign( { algorithm: "HS256" }, {}, { issuer: "yourAPI.com", audience: "yourAPI.com:client", } ); return jwt.sign({ userId: userId.toString() }, JWT_SECRET_KEY, jwtSignOptions); }; const generateRefreshToken = async (userId: number, tokenFamily?: string) => { const expAt = new Date(new Date().getTime() + 31 * 24 * 60 * 60 * 1000); // Expire in 31 days const refreshTokenExp = expAt.toISOString(); const token = await createTokenQuery({ userId, tokenFamily, expiresAt: refreshTokenExp, }); return token; };
Implement logic to handle refreshing tokens securely:
const refreshToken = async ({ token }: RefreshTokenSchema) => { const tokenData = await getRefreshToken(token); if (!tokenData) throw forbiddenError(); const { userId, tokenFamily, active } = tokenData; if (active) { // Token is valid and hasn't been used yet const newRefreshToken = await generateRefreshToken(userId, tokenFamily); const accessToken = generateAccessToken(userId); return { accessToken, refreshToken: newRefreshToken }; } else { // Previously refreshed token used, invalidate all tokens in family await invalidateRefreshTokenFamily(tokenFamily); throw forbiddenError(); } };
Learn more about refresh tokens and Automatic Reuse Detection in this Auth0 article.
By following this guide, you’ve built a simple, secure authentication system for your Node.js API with minimal dependencies. This approach ensures you have full control and adhere to modern best practices for security.
If you want to save time and effort, check out Vratix. Our open-source CLI can set up a fully functional Node.js project with authentication in seconds. Explore our fully implemented authentication module on GitHub.
Did this guide help you? Let us know in the comments, or connect with us on X!
The above is the detailed content of The Right Way to do Authentication in Node.js [uide]. For more information, please follow other related articles on the PHP Chinese website!