Skip to content

Commit db8395d

Browse files
authored
Merge pull request #51 from LONECODER1/feature/jwtmiddleware
feat:Add JWT-based authentication middleware to protect APIs
2 parents f8a43ce + 222cbac commit db8395d

File tree

11 files changed

+444
-22
lines changed

11 files changed

+444
-22
lines changed

backend/.env.example

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
# --- JWT Configuration ---
3+
# Secret key for signing access tokens. Should be a long, random string.
4+
JWT_SECRET=your_jwt_secret_key_here
5+
6+
# Secret key for signing refresh tokens. Should be a long, random string, different from JWT_SECRET.
7+
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here
8+
9+
# Access token expiration time (e.g., 10m, 1h, 1d)
10+
ACCESS_TOKEN_EXPIRY=10m
11+
12+
# Refresh token expiration time (e.g., 7d, 30d)
13+
REFRESH_TOKEN_EXPIRY=7d
14+
15+
# --- Server Configuration ---
16+
# Port for the server to run on
17+
PORT=4000

backend/package-lock.json

Lines changed: 61 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,27 @@
1111
"author": "",
1212
"license": "MIT",
1313
"dependencies": {
14+
"bcryptjs": "^3.0.2",
1415
"body-parser": "^1.20.2",
16+
"cookie-parser": "^1.4.7",
1517
"cors": "^2.8.5",
16-
"dotenv": "^16.3.1",
17-
"express": "^4.18.2",
18+
"dotenv": "^16.6.1",
19+
"express": "^4.21.2",
1820
"jsonwebtoken": "^9.0.2",
19-
"mongoose": "^8.19.1",
21+
"mongoose": "^8.19.2",
2022
"morgan": "^1.10.0",
2123
"nodemon": "^3.1.10"
2224
},
2325
"devDependencies": {
26+
"@types/bcryptjs": "^2.4.6",
2427
"@types/body-parser": "^1.19.2",
28+
"@types/cookie-parser": "^1.4.9",
2529
"@types/cors": "^2.8.13",
26-
"@types/express": "^4.17.21",
30+
"@types/express": "^4.17.23",
2731
"@types/jsonwebtoken": "^9.0.10",
2832
"@types/morgan": "^1.9.7",
33+
"ts-node": "^10.9.2",
2934
"ts-node-dev": "^2.0.0",
30-
"typescript": "^5.2.2"
35+
"typescript": "^5.9.3"
3136
}
32-
}
37+
}

backend/src/app.ts

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,132 @@
1-
import express, { Application } from 'express';
1+
import express, { Application, Request, Response } from 'express';
22
import cors from 'cors';
33
import morgan from 'morgan';
44
import bodyParser from 'body-parser';
55
import healthRoutes from './routes/healthRoutes';
6+
7+
// Consolidated Imports
68
import productRoutes from "./routes/product.routes";
79
import mongoose from "mongoose";
10+
import bcrypt from "bcryptjs";
11+
import cookieParser from "cookie-parser";
12+
import { signAccessToken, signRefreshToken, verifyRefreshToken } from "./controllers/auth";
13+
import { authenticate, AuthRequest } from "./middleware/authMiddleware";
14+
815
const app: Application = express();
916
import dotenv from "dotenv";
1017
dotenv.config();
18+
1119
// Middleware setup
1220
app.use(cors());
1321
app.use(morgan('dev'));
1422
app.use(bodyParser.json());
1523
app.use(bodyParser.urlencoded({ extended: true }));
24+
// Middleware needed for Auth
25+
app.use(cookieParser());
26+
27+
28+
// 3. User Interface and Mock Data
29+
interface User {
30+
id: string;
31+
username: string;
32+
passwordHash: string;
33+
role: string;
34+
}
35+
36+
const users: User[] = [
37+
{ id: "1", username: "alice", passwordHash: bcrypt.hashSync("password", 8), role: "admin" },
38+
];
39+
40+
const refreshTokens = new Map<string, string>()
41+
42+
43+
// 4. Authentication API Routes
44+
app.post("/login", async (req: Request, res: Response) => {
45+
const { username, password } = req.body;
46+
const user = users.find((u) => u.username === username);
47+
if (!user) return res.status(401).json({ error: "Invalid credentials" });
48+
49+
const match = await bcrypt.compare(password, user.passwordHash);
50+
if (!match) return res.status(401).json({ error: "Invalid credentials" });
51+
52+
const payload = { sub: user.id, username: user.username, role: user.role };
53+
const accessToken = signAccessToken(payload);
54+
const refreshToken = signRefreshToken(payload);
55+
56+
refreshTokens.set(user.id, refreshToken);
1657

58+
res.cookie("refreshToken", refreshToken, {
59+
httpOnly: true,
60+
sameSite: "strict",
61+
secure: process.env.NODE_ENV === "production",
62+
maxAge: 7 * 24 * 60 * 60 * 1000,
63+
});
64+
65+
res.json({ accessToken });
66+
});
67+
68+
app.post("/refresh", (req: Request, res: Response) => {
69+
const token = req.cookies?.refreshToken || req.body.refreshToken;
70+
71+
if (!token) {
72+
return res.status(401).json({ error: "Refresh token is missing" });
73+
}
74+
75+
try {
76+
const payload = verifyRefreshToken(token);
77+
const storedToken = refreshTokens.get(payload.sub);
78+
79+
if (!storedToken) {
80+
return res.status(401).json({ error: "Session not found or already logged out" });
81+
}
82+
83+
if (storedToken !== token) {
84+
return res.status(401).json({ error: "Token used is not the latest valid token" });
85+
}
86+
87+
const cleanPayload = {
88+
sub: payload.sub,
89+
username: payload.username,
90+
role: payload.role
91+
};
92+
93+
const newAccess = signAccessToken(cleanPayload);
94+
const newRefresh = signRefreshToken(cleanPayload);
95+
96+
refreshTokens.set(cleanPayload.sub, newRefresh);
97+
98+
res.cookie("refreshToken", newRefresh, {
99+
httpOnly: true,
100+
sameSite: "strict",
101+
secure: process.env.NODE_ENV === "production",
102+
maxAge: 7 * 24 * 60 * 60 * 1000,
103+
});
104+
105+
res.json({ accessToken: newAccess });
106+
107+
} catch (error) {
108+
console.error("Refresh token verification failed:", error);
109+
res.status(401).json({ error: "Refresh token is expired or invalid" });
110+
}
111+
});
112+
113+
app.post("/logout", authenticate, (req: AuthRequest, res: Response) => {
114+
// req.user is guaranteed to exist by the 'authenticate' middleware
115+
refreshTokens.delete(req.user!.sub);
116+
res.clearCookie("refreshToken");
117+
res.status(204).send();
118+
});
119+
120+
app.get("/protected", authenticate, (req: AuthRequest, res: Response) => {
121+
res.json({ message: "Protected route accessed", user: req.user });
122+
});
17123

18-
// API Routes
124+
// Existing API Routes
19125
app.use('/api/health', healthRoutes);
20126
app.use("/api/products", productRoutes);
21127
// Default
22128
app.get("/", (_req, res) => {
23-
res.send("API is running 🚀");
129+
res.send("API is running ");
24130
});
25131

26132
// MongoDB connect (optional)

backend/src/controllers/auth.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import jwt, { SignOptions, JwtPayload } from "jsonwebtoken";
2+
import dotenv from "dotenv";
3+
dotenv.config();
4+
5+
if (!process.env.JWT_SECRET || !process.env.JWT_REFRESH_SECRET) {
6+
throw new Error("JWT secrets missing in environment variables");
7+
}
8+
9+
const accessSecret = process.env.JWT_SECRET as string;
10+
const refreshSecret = process.env.JWT_REFRESH_SECRET as string;
11+
12+
// Define your payload type
13+
export interface TokenPayload extends JwtPayload {
14+
sub: string;
15+
username?: string;
16+
role?: string;
17+
//role?:"user"|"admin"; // Example roles
18+
}
19+
20+
export function signAccessToken(payload: TokenPayload) {
21+
const options: SignOptions = { expiresIn: process.env.ACCESS_TOKEN_EXPIRY as any };
22+
23+
// --- CRITICAL FIX: Create a clean payload copy ---
24+
const cleanPayload = { ...payload };
25+
delete cleanPayload.iat; // Remove "Issued At" claim
26+
delete cleanPayload.exp; // Remove "Expiration" claim to allow options.expiresIn to work
27+
// --- END FIX ---
28+
29+
return jwt.sign(cleanPayload, accessSecret, options);
30+
}
31+
32+
export function signRefreshToken(payload: TokenPayload) {
33+
const options: SignOptions = { expiresIn: process.env.REFRESH_TOKEN_EXPIRY as any };
34+
35+
// --- CRITICAL FIX: Create a clean payload copy ---
36+
const cleanPayload = { ...payload };
37+
delete cleanPayload.iat; // Remove "Issued At" claim
38+
delete cleanPayload.exp; // Remove "Expiration" claim to allow options.expiresIn to work
39+
// --- END FIX ---
40+
41+
return jwt.sign(cleanPayload, refreshSecret, options);
42+
}
43+
44+
export function verifyAccessToken(token: string): TokenPayload {
45+
return jwt.verify(token, accessSecret) as TokenPayload;
46+
}
47+
48+
export function verifyRefreshToken(token: string): TokenPayload {
49+
return jwt.verify(token, refreshSecret) as TokenPayload;
50+
}

0 commit comments

Comments
 (0)