1. Introduction đ
Authentication and authorization sit at the heart of nearly every application that handles user data. This tutorial covers the strategies, standards, and libraries used to implement both securely in Node.js â from session cookies and JWTs to OAuth, MFA, and role-based access control.
Warning
2. Authentication vs Authorization đ
These two terms are often confused but answer fundamentally different questions.
| Concept | Question It Answers | Example |
|---|---|---|
| Authentication | Who are you? | Logging in with email & password |
| Authorization | What are you allowed to do? | Only admins can delete users |
Information
3. Authentication Strategies đ§
Node.js applications commonly implement one or more of these strategies, often combined for different use cases.
- Session-based â server stores session state, client holds a session ID cookie.
- Token-based (JWT) â stateless, self-contained tokens verified without a server-side lookup.
- OAuth 2.0 / OIDC â delegated authentication via a trusted third-party provider.
- API keys â long-lived credentials for machine-to-machine communication.
4. Session-Based Authentication đĒ
In session-based auth, the server creates a session record and gives the client a cookie referencing it.
src/auth/session.ts
import session from "express-session";
app.use(session({
secret: process.env.SESSION_SECRET as string,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, maxAge: 1000 * 60 * 30 },
}));
app.post("/login", async (req, res) => {
const user = await authenticate(req.body.email, req.body.password);
req.session.userId = user.id;
res.json({ success: true });
});Tip
5. Token-Based Authentication đī¸
Token-based auth issues a self-contained credential the client sends with every request, removing the need for server-side session lookups.
Note
6. JWT Authentication đĢ
JSON Web Tokens encode claims in a signed, compact format that can be verified without a database round-trip.
src/auth/jwt.ts
import jwt from "jsonwebtoken";
function issueToken(userId: string): string {
return jwt.sign({ sub: userId }, process.env.JWT_SECRET as string, {
algorithm: "HS256",
expiresIn: "15m",
});
}
function verifyToken(token: string) {
return jwt.verify(token, process.env.JWT_SECRET as string, { algorithms: ["HS256"] });
}Danger
7. OAuth 2.0 đ
OAuth 2.0 is an authorization framework that lets users grant limited access to their data on one service to another, without sharing credentials.
src/auth/oauth.ts
import { Router } from "express";
import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
callbackURL: "/auth/google/callback",
},
(accessToken, refreshToken, profile, done) => done(null, profile)
));
const router = Router();
router.get("/auth/google", passport.authenticate("google", { scope: ["profile", "email"] }));
router.get("/auth/google/callback", passport.authenticate("google"), (req, res) => res.redirect("/dashboard"));Reference
8. OpenID Connect (OIDC) đĒĒ
OIDC is a thin identity layer built on top of OAuth 2.0, adding a standardized ID Token that carries verified user identity claims.
- OAuth 2.0 handles authorization (access to resources).
- OIDC adds authentication (verified identity) via the id_token.
- Common providers: Google, Microsoft Entra ID, Auth0, Okta.
Important
9. API Keys đī¸
API keys are simple, long-lived credentials typically used for server-to-server or third-party integrations rather than end-user login.
src/middleware/apiKey.ts
function requireApiKey(req: Request, res: Response, next: NextFunction) {
const key = req.header("x-api-key");
if (!key || !isValidApiKey(key)) {
return res.status(401).json({ error: "Invalid API key" });
}
next();
}Caution
10. Password Hashing đ¨
Passwords must always be hashed with a slow, adaptive algorithm before storage â never in plain text, and never with a fast general-purpose hash.
| Algorithm | Notes |
|---|---|
| bcrypt | Battle-tested, widely supported default. |
| argon2 | Modern, memory-hard, recommended for new projects. |
11. bcrypt đ§
bcrypt is a widely adopted, slow hashing algorithm with a built-in salt, making it resistant to rainbow-table attacks.
src/auth/bcrypt.ts
import bcrypt from "bcrypt";
async function hashPassword(plain: string): Promise<string> {
return bcrypt.hash(plain, 12);
}
async function verifyPassword(plain: string, hash: string): Promise<boolean> {
return bcrypt.compare(plain, hash);
}Tip
12. argon2 đ
argon2 won the Password Hashing Competition and is considered the strongest general-purpose password hashing algorithm available today.
src/auth/argon2.ts
import argon2 from "argon2";
async function hashPassword(plain: string): Promise<string> {
return argon2.hash(plain, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2 });
}
async function verifyPassword(hash: string, plain: string): Promise<boolean> {
return argon2.verify(hash, plain);
}Best Practice
13. Refresh Tokens âģī¸
Refresh tokens are long-lived credentials used to obtain new access tokens without requiring the user to log in again.
src/auth/refresh.ts
async function refreshAccessToken(refreshToken: string): Promise<string> {
const stored = await findRefreshToken(refreshToken);
if (!stored || stored.revoked || stored.expiresAt < new Date()) {
throw new Error("Invalid refresh token");
}
return issueAccessToken(stored.userId);
}Caution
14. Access Tokens đĢ
Access tokens are short-lived credentials sent with each API request to prove the caller's identity and permissions.
src/auth/access-token.ts
function issueAccessToken(userId: string): string {
return jwt.sign({ sub: userId, type: "access" }, process.env.JWT_SECRET as string, {
expiresIn: "15m",
});
}Note
15. Cookie-Based Authentication đĒ
Storing tokens in HttpOnly cookies protects them from being read by client-side JavaScript, mitigating XSS-based theft.
src/auth/cookie.ts
res.cookie("accessToken", token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 15 * 60 * 1000,
});Best Practice
16. Multi-Factor Authentication (MFA) đą
MFA adds a second verification factor â something the user has or is â on top of a password, significantly reducing account takeover risk.
src/auth/totp.ts
import { authenticator } from "otplib";
const secret = authenticator.generateSecret();
const token = authenticator.generate(secret);
const isValid = authenticator.verify({ token: userInput, secret });- TOTP (authenticator apps) â most common, works offline.
- SMS/Email codes â convenient but more vulnerable to interception.
- WebAuthn / Passkeys â phishing-resistant, hardware-backed.
17. Social Login đ
Social login lets users authenticate via an existing account (Google, GitHub, etc.) rather than creating a new password.
src/auth/github-strategy.ts
import { Strategy as GitHubStrategy } from "passport-github2";
passport.use(new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
callbackURL: "/auth/github/callback",
},
async (accessToken, refreshToken, profile, done) => {
const user = await findOrCreateUserFromGitHub(profile);
done(null, user);
}
));Tip
18. Role-Based Access Control (RBAC) đ
RBAC assigns users one or more roles, each granting a predefined set of permissions.
src/middleware/rbac.ts
type Role = "admin" | "editor" | "viewer";
function requireRole(...allowed: Role[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!allowed.includes(req.user.role)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
router.delete("/posts/:id", requireRole("admin", "editor"), deletePost);Information
19. Permission-Based Access Control đ
Permission-based (or attribute-based) access control checks fine-grained capabilities directly, rather than relying on broad role labels.
src/middleware/permissions.ts
function requirePermission(permission: string) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user.permissions.includes(permission)) {
return res.status(403).json({ error: "Forbidden" });
}
next();
};
}
router.post("/reports/export", requirePermission("reports:export"), exportReport);Best Practice
20. Authentication Middleware đ§Š
Authentication middleware verifies a token or session before a request reaches protected route handlers.
src/middleware/authenticate.ts
function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const token = req.cookies.accessToken;
if (!token) return res.status(401).json({ error: "Unauthenticated" });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET as string);
req.user = payload as AuthUser;
next();
} catch {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
router.get("/profile", authenticate, getProfile);21. Session Management đī¸
Beyond simple login/logout, robust session management includes tracking active sessions and allowing users to revoke them individually.
src/auth/sessions.ts
async function listActiveSessions(userId: string) {
return db.session.findMany({ where: { userId, revoked: false } });
}
async function revokeSession(sessionId: string) {
await db.session.update({ where: { id: sessionId }, data: { revoked: true } });
}Tip
22. Secure Logout đĒ
Logout must invalidate the session or token server-side â clearing a cookie client-side alone isn't sufficient for token-based auth.
src/auth/logout.ts
router.post("/logout", authenticate, async (req: AuthRequest, res: Response) => {
await revokeRefreshToken(req.user.refreshTokenId);
res.clearCookie("accessToken");
res.clearCookie("refreshToken");
res.json({ success: true });
});Warning
23. Account Verification âī¸
Verifying ownership of an email address (or phone number) prevents fake signups and confirms the user can be reached.
src/auth/verify-email.ts
async function sendVerificationEmail(userId: string, email: string) {
const token = crypto.randomBytes(32).toString("hex");
await storeVerificationToken(userId, token, { expiresInMinutes: 60 });
await sendEmail(email, `Verify your account: https://app.example.com/verify?token=${token}`);
}Tip
24. Password Reset đ
Password reset flows must be designed carefully to avoid leaking whether an email exists in the system.
src/auth/password-reset.ts
router.post("/forgot-password", async (req, res) => {
const user = await findUserByEmail(req.body.email);
if (user) {
const token = crypto.randomBytes(32).toString("hex");
await storeResetToken(user.id, token, { expiresInMinutes: 15 });
await sendEmail(user.email, `Reset your password: https://app.example.com/reset?token=${token}`);
}
// â
Always return the same response, regardless of whether the user exists
res.json({ message: "If that email exists, a reset link has been sent." });
});Important
25. Authentication Best Practices â
- Hash passwords with argon2 or bcrypt â never store or log plain-text passwords.
- Use short-lived access tokens paired with rotating refresh tokens.
- Store tokens in HttpOnly, Secure, SameSite cookies.
- Rate-limit login, signup, and password-reset endpoints.
- Offer MFA, especially for accounts with elevated privileges.
26. Security Considerations đĄī¸
- Use generic error messages for failed logins to avoid revealing whether the email or password was wrong.
- Enforce HTTPS everywhere â tokens sent over plain HTTP can be intercepted.
- Log authentication events (logins, failed attempts, password changes) for auditing.
- Regularly rotate signing secrets and support key rotation without downtime.
Danger
27. Common Mistakes â ī¸
| Mistake | Consequence |
|---|---|
| Storing JWTs in localStorage | Vulnerable to theft via XSS |
| Long-lived access tokens with no rotation | Large exposure window if leaked |
| Revealing "email not found" on login | Enables account enumeration |
| No rate limiting on login endpoints | Vulnerable to brute-force & credential stuffing |
| Trusting client-supplied roles/permissions | Privilege escalation |
28. Frequently Asked Questions â
Question
Answer
Question
Answer
Question
Answer
Question
Answer
29. Summary đ
Authentication verifies identity, and authorization controls what that identity can do â both must be implemented carefully to keep users and data safe. From password hashing and JWTs to OAuth, MFA, and RBAC, a robust Node.js auth system layers multiple protections together.
- Hash passwords with argon2 or bcrypt, never store them in plain text.
- Use short-lived access tokens with rotating refresh tokens.
- Store credentials in HttpOnly cookies, not localStorage.
- Implement RBAC or permission-based checks server-side for every protected action.
- Layer in MFA and rate limiting for sensitive endpoints.