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(),
|
getGalleryTokenFromRequest: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../utils/tokenRevocation', () => ({
|
||||||
|
isTokenRevoked: jest.fn().mockResolvedValue(false),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('../utils/dbCompat', () => ({
|
jest.mock('../utils/dbCompat', () => ({
|
||||||
formatBoolean: (v) => (v ? 1 : 0),
|
formatBoolean: (v) => (v ? 1 : 0),
|
||||||
}));
|
}));
|
||||||
@@ -45,6 +49,7 @@ jest.mock('../utils/dbCompat', () => ({
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
const { db } = require('../database/db');
|
const { db } = require('../database/db');
|
||||||
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
||||||
|
const { isTokenRevoked } = require('../utils/tokenRevocation');
|
||||||
const { verifyGalleryAccess } = require('../middleware/gallery');
|
const { verifyGalleryAccess } = require('../middleware/gallery');
|
||||||
|
|
||||||
function makeRes() {
|
function makeRes() {
|
||||||
@@ -93,6 +98,55 @@ beforeEach(() => {
|
|||||||
db.mockReset();
|
db.mockReset();
|
||||||
jwt.verify.mockReset();
|
jwt.verify.mockReset();
|
||||||
getGalleryTokenFromRequest.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 ----------------------------
|
// ---- customer-minted JWT, assignment intact ----------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const jwt = require('jsonwebtoken');
|
|||||||
const { db, withRetry } = require('../database/db');
|
const { db, withRetry } = require('../database/db');
|
||||||
const { formatBoolean } = require('../utils/dbCompat');
|
const { formatBoolean } = require('../utils/dbCompat');
|
||||||
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
const { getGalleryTokenFromRequest } = require('../utils/tokenUtils');
|
||||||
|
const { isTokenRevoked } = require('../utils/tokenRevocation');
|
||||||
const logger = require('../utils/logger');
|
const logger = require('../utils/logger');
|
||||||
|
|
||||||
// Check if the request carries a valid admin preview token (Feature 3)
|
// 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' });
|
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 we have a slug in the URL params or from pre-middleware, verify it matches
|
||||||
if (requestedSlug) {
|
if (requestedSlug) {
|
||||||
// Verify by slug and ensure it matches the token's event
|
// Verify by slug and ensure it matches the token's event
|
||||||
|
|||||||
Reference in New Issue
Block a user