fix: fall back to session cookies for stale tokens
This commit is contained in:
parent
6b3d267abb
commit
e793a59570
@ -8,27 +8,30 @@ const { logError } = require("../utils/logger");
|
|||||||
async function auth(req, res, next) {
|
async function auth(req, res, next) {
|
||||||
const header = req.headers.authorization || "";
|
const header = req.headers.authorization || "";
|
||||||
const token = header.startsWith("Bearer ") ? header.slice(7).trim() : null;
|
const token = header.startsWith("Bearer ") ? header.slice(7).trim() : null;
|
||||||
|
const cookies = parseCookieHeader(req.headers.cookie);
|
||||||
|
const sid = cookies[cookieName()];
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const jwtSecret = process.env.JWT_SECRET;
|
const jwtSecret = process.env.JWT_SECRET;
|
||||||
if (!jwtSecret) {
|
if (!jwtSecret && !sid) {
|
||||||
logError(req, "middleware.auth.jwtSecretMissing", new Error("JWT_SECRET is not configured"));
|
logError(req, "middleware.auth.jwtSecretMissing", new Error("JWT_SECRET is not configured"));
|
||||||
return sendError(res, 500, "Authentication is unavailable");
|
return sendError(res, 500, "Authentication is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (jwtSecret) {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, jwtSecret);
|
const decoded = jwt.verify(token, jwtSecret);
|
||||||
req.user = decoded; // id + role
|
req.user = decoded; // id + role
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (!sid) {
|
||||||
return sendError(res, 401, "Invalid or expired token");
|
return sendError(res, 401, "Invalid or expired token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cookies = parseCookieHeader(req.headers.cookie);
|
|
||||||
const sid = cookies[cookieName()];
|
|
||||||
|
|
||||||
if (!sid) {
|
if (!sid) {
|
||||||
return sendError(res, 401, "Missing authentication");
|
return sendError(res, 401, "Missing authentication");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,16 +10,14 @@ async function optionalAuth(req, res, next) {
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const jwtSecret = process.env.JWT_SECRET;
|
const jwtSecret = process.env.JWT_SECRET;
|
||||||
if (!jwtSecret) {
|
if (jwtSecret) {
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, jwtSecret);
|
const decoded = jwt.verify(token, jwtSecret);
|
||||||
req.user = decoded;
|
req.user = decoded;
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return next();
|
// Continue to the session cookie fallback below.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
118
backend/tests/auth.middleware.test.js
Normal file
118
backend/tests/auth.middleware.test.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
jest.mock("jsonwebtoken", () => ({
|
||||||
|
verify: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../models/session.model", () => ({
|
||||||
|
getActiveSessionWithUser: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock("../utils/logger", () => ({
|
||||||
|
logError: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const jwt = require("jsonwebtoken");
|
||||||
|
const Session = require("../models/session.model");
|
||||||
|
const auth = require("../middleware/auth");
|
||||||
|
|
||||||
|
function createResponse() {
|
||||||
|
const res = {};
|
||||||
|
res.status = jest.fn().mockReturnValue(res);
|
||||||
|
res.json = jest.fn().mockReturnValue(res);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("auth middleware", () => {
|
||||||
|
const originalJwtSecret = process.env.JWT_SECRET;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env.JWT_SECRET = "test-secret";
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalJwtSecret === undefined) {
|
||||||
|
delete process.env.JWT_SECRET;
|
||||||
|
} else {
|
||||||
|
process.env.JWT_SECRET = originalJwtSecret;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses a valid bearer token without reading the session cookie", async () => {
|
||||||
|
jwt.verify.mockReturnValue({ id: 5, role: "admin" });
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer valid-token",
|
||||||
|
cookie: "sid=session-id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth(req, res, next);
|
||||||
|
|
||||||
|
expect(jwt.verify).toHaveBeenCalledWith("valid-token", "test-secret");
|
||||||
|
expect(Session.getActiveSessionWithUser).not.toHaveBeenCalled();
|
||||||
|
expect(req.user).toEqual({ id: 5, role: "admin" });
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to a valid session cookie when the bearer token is stale", async () => {
|
||||||
|
jwt.verify.mockImplementation(() => {
|
||||||
|
throw new Error("stale token");
|
||||||
|
});
|
||||||
|
Session.getActiveSessionWithUser.mockResolvedValue({
|
||||||
|
id: "session-id",
|
||||||
|
user_id: 7,
|
||||||
|
role: "user",
|
||||||
|
username: "shopper",
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer stale-token",
|
||||||
|
cookie: "sid=session-id",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth(req, res, next);
|
||||||
|
|
||||||
|
expect(Session.getActiveSessionWithUser).toHaveBeenCalledWith("session-id");
|
||||||
|
expect(req.user).toEqual({
|
||||||
|
id: 7,
|
||||||
|
role: "user",
|
||||||
|
username: "shopper",
|
||||||
|
});
|
||||||
|
expect(req.session_id).toBe("session-id");
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(res.status).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects a stale bearer token when no session cookie is present", async () => {
|
||||||
|
jwt.verify.mockImplementation(() => {
|
||||||
|
throw new Error("stale token");
|
||||||
|
});
|
||||||
|
|
||||||
|
const req = {
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer stale-token",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const res = createResponse();
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
await auth(req, res, next);
|
||||||
|
|
||||||
|
expect(Session.getActiveSessionWithUser).not.toHaveBeenCalled();
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(401);
|
||||||
|
expect(res.json).toHaveBeenCalledWith({
|
||||||
|
error: {
|
||||||
|
code: "unauthorized",
|
||||||
|
message: "Invalid or expired token",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user