fix(backend): check the revocation store on gallery access
Gallery logout wrote to revoked_tokens correctly, but verifyGalleryAccess never read it back — a logged-out gallery JWT kept working until natural expiry. Admin auth already calls isTokenRevoked(); gallery was the outlier. Note: this gap was independently closed on main via a broader gallery- access refactor (PR #1357), so no main-branch fix is needed there — this is a stable-only backport of the same protection.
This commit is contained in:
@@ -38,6 +38,10 @@ jest.mock('../utils/tokenUtils', () => ({
|
||||
getGalleryTokenFromRequest: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/tokenRevocation', () => ({
|
||||
isTokenRevoked: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/dbCompat', () => ({
|
||||
formatBoolean: (v) => (v ? 1 : 0),
|
||||
}));
|
||||
@@ -45,6 +49,7 @@ jest.mock('../utils/dbCompat', () => ({
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { db } = require('../database/db');
|
||||
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
||||
const { isTokenRevoked } = require('../utils/tokenRevocation');
|
||||
const { verifyGalleryAccess } = require('../middleware/gallery');
|
||||
|
||||
function makeRes() {
|
||||
@@ -93,6 +98,55 @@ beforeEach(() => {
|
||||
db.mockReset();
|
||||
jwt.verify.mockReset();
|
||||
getGalleryTokenFromRequest.mockReset();
|
||||
isTokenRevoked.mockReset();
|
||||
isTokenRevoked.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
// ---- revoked gallery token (GHSA-q7f7-gjx8-mf6h) -----------------------
|
||||
|
||||
describe('verifyGalleryAccess — revoked token', () => {
|
||||
it('returns 401 TOKEN_REVOKED and never reaches the events query when revoked', async () => {
|
||||
getGalleryTokenFromRequest.mockReturnValue('tkn');
|
||||
jwt.verify.mockReturnValue({ type: 'gallery', eventId: 42 });
|
||||
isTokenRevoked.mockResolvedValue(true);
|
||||
|
||||
const req = makeReq();
|
||||
const res = makeRes();
|
||||
const next = jest.fn();
|
||||
await verifyGalleryAccess(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ code: 'TOKEN_REVOKED' }),
|
||||
);
|
||||
expect(db).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('proceeds normally when the token is not revoked', async () => {
|
||||
getGalleryTokenFromRequest.mockReturnValue('tkn');
|
||||
jwt.verify.mockReturnValue({ type: 'gallery', eventId: 42 });
|
||||
isTokenRevoked.mockResolvedValue(false);
|
||||
|
||||
const eventsChain = {};
|
||||
eventsChain.where = jest.fn().mockReturnValue(eventsChain);
|
||||
eventsChain.select = jest.fn().mockReturnValue(eventsChain);
|
||||
eventsChain.first = jest.fn().mockResolvedValue({
|
||||
id: 42, slug: 'test-event', is_active: true, is_archived: false,
|
||||
});
|
||||
db.mockImplementationOnce(() => eventsChain);
|
||||
|
||||
const req = makeReq();
|
||||
const res = makeRes();
|
||||
const next = jest.fn();
|
||||
await verifyGalleryAccess(req, res, next);
|
||||
|
||||
expect(isTokenRevoked).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'gallery', eventId: 42 }),
|
||||
);
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---- customer-minted JWT, assignment intact ----------------------------
|
||||
|
||||
@@ -2,6 +2,7 @@ const jwt = require('jsonwebtoken');
|
||||
const { db, withRetry } = require('../database/db');
|
||||
const { formatBoolean } = require('../utils/dbCompat');
|
||||
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
||||
const { isTokenRevoked } = require('../utils/tokenRevocation');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Check if the request carries a valid admin preview token (Feature 3)
|
||||
@@ -89,6 +90,14 @@ async function verifyGalleryAccess(req, res, next) {
|
||||
return res.status(403).json({ error: 'Invalid token type for gallery access' });
|
||||
}
|
||||
|
||||
// Gallery logout writes to the revocation store (see routes/auth.js),
|
||||
// but nothing on this path ever read it back (GHSA-q7f7-gjx8-mf6h) — a
|
||||
// logged-out gallery JWT kept working until natural expiry.
|
||||
if (await isTokenRevoked(decoded)) {
|
||||
logger.warn('[verifyGalleryAccess] Revoked token used', { eventId: decoded.eventId });
|
||||
return res.status(401).json({ error: 'Token has been revoked', code: 'TOKEN_REVOKED' });
|
||||
}
|
||||
|
||||
// If we have a slug in the URL params or from pre-middleware, verify it matches
|
||||
if (requestedSlug) {
|
||||
// Verify by slug and ensure it matches the token's event
|
||||
|
||||
Reference in New Issue
Block a user