Introduction: Why OAuth 2.0 and JWT Are the Modern Authorization Standard
\nIn modern web development, secure authentication and authorization are critically important components of any application. OAuth 2.0 and JWT (JSON Web Tokens) have become the de facto standard for building distributed systems. OAuth 2.0 provides a delegated access protocol, while JWT offers a compact and self-contained token format.
\nIn this article, we will use practical examples in TypeScript to walk through implementing the full authorization cycle: from issuing tokens to their validation and renewal. You will learn how to protect API endpoints, implement refresh tokens, and handle common errors.
\n\n1. Basic Project Setup and Dependency Installation
\nFirst, let's create a Node.js project with TypeScript. Make sure you have Node.js (version 18+) and npm installed.
mkdir oauth-jwt-typescript\ncd oauth-jwt-typescript\nnpm init -y\nnpm install express cors dotenv jsonwebtoken bcrypt\nnpm install -D typescript @types/express @types/cors @types/jsonwebtoken @types/bcrypt ts-node nodemon\nnpx tsc --init\nConfigure tsconfig.json for working with ES modules and strict types. Add secret keys to the .env file:
ACCESS_TOKEN_SECRET=your-super-secret-access-key\nREFRESH_TOKEN_SECRET=your-super-secret-refresh-key\nACCESS_TOKEN_EXPIRES_IN=15m\nREFRESH_TOKEN_EXPIRES_IN=7d\n\n2. Implementing JWT Token Generation and Verification
\nLet's create a utility src/utils/jwt.ts for working with tokens. We will use two types of tokens: Access Token (short-lived) and Refresh Token (long-lived).
import jwt from 'jsonwebtoken';\nimport dotenv from 'dotenv';\n\ndotenv.config();\n\ninterface TokenPayload {\n userId: string;\n email: string;\n role: 'user' | 'admin';\n}\n\nexport const generateAccessToken = (payload: TokenPayload): string => {\n return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET!, {\n expiresIn: process.env.ACCESS_TOKEN_EXPIRES_IN || '15m',\n });\n};\n\nexport const generateRefreshToken = (payload: TokenPayload): string => {\n return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {\n expiresIn: process.env.REFRESH_TOKEN_EXPIRES_IN || '7d',\n });\n};\n\nexport const verifyAccessToken = (token: string): TokenPayload => {\n return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!) as TokenPayload;\n};\n\nexport const verifyRefreshToken = (token: string): TokenPayload => {\n return jwt.verify(token, process.env.REFRESH_TOKEN_SECRET!) as TokenPayload;\n};\nNote: we use strict typing for the payload. This guarantees that the token will always contain the necessary fields.
\n\n3. Creating Middleware for Route Protection
\nNow let's implement middleware that will check for the presence and validity of the Access Token in the Authorization header. Create src/middleware/auth.ts:
import { Request, Response, NextFunction } from 'express';\nimport { verifyAccessToken } from '../utils/jwt';\n\nexport interface AuthRequest extends Request {\n user?: {\n userId: string;\n email: string;\n role: 'user' | 'admin';\n };\n}\n\nexport const authenticate = (req: AuthRequest, res: Response, next: NextFunction): void => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN\n\n if (!token) {\n res.status(401).json({ error: 'Access token is required' });\n return;\n }\n\n try {\n const decoded = verifyAccessToken(token);\n req.user = decoded;\n next();\n } catch (error) {\n res.status(403).json({ error: 'Invalid or expired access token' });\n }\n};\n\nexport const authorizeRole = (...roles: string[]) => {\n return (req: AuthRequest, res: Response, next: NextFunction): void => {\n if (!req.user || !roles