Skip to content

Commit 8fd3516

Browse files
committed
refactor: share oauth jwt middleware verification flow
1 parent 519a05c commit 8fd3516

5 files changed

Lines changed: 85 additions & 40 deletions

File tree

src/api/middleware/auth.js

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
*/
55

66
import crypto from 'node:crypto';
7-
import { error, warn } from '../../logger.js';
8-
import { verifyJwtToken } from './verifyJwt.js';
7+
import { warn } from '../../logger.js';
8+
import { handleOAuthJwt } from './oauthJwt.js';
99

1010
/**
1111
* Performs a constant-time comparison of the given secret against BOT_API_SECRET.
@@ -59,22 +59,8 @@ export function requireAuth() {
5959
}
6060

6161
// Try OAuth2 JWT
62-
const authHeader = req.headers.authorization;
63-
if (authHeader?.startsWith('Bearer ')) {
64-
const token = authHeader.slice(7);
65-
const result = verifyJwtToken(token);
66-
if (result.error) {
67-
if (result.status === 500) {
68-
error('SESSION_SECRET not configured — cannot verify OAuth token', {
69-
ip: req.ip,
70-
path: req.path,
71-
});
72-
}
73-
return res.status(result.status).json({ error: result.error });
74-
}
75-
req.authMethod = 'oauth';
76-
req.user = result.user;
77-
return next();
62+
if (handleOAuthJwt(req, res, next)) {
63+
return;
7864
}
7965

8066
// Neither auth method provided or valid

src/api/middleware/oauth.js

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
* Verifies JWT tokens from Discord OAuth2 sessions
44
*/
55

6-
import { error } from '../../logger.js';
7-
import { verifyJwtToken } from './verifyJwt.js';
6+
import { handleOAuthJwt } from './oauthJwt.js';
87

98
/**
109
* Creates middleware that verifies a JWT Bearer token from the Authorization header.
@@ -14,25 +13,6 @@ import { verifyJwtToken } from './verifyJwt.js';
1413
*/
1514
export function requireOAuth() {
1615
return (req, res, next) => {
17-
const authHeader = req.headers.authorization;
18-
19-
if (!authHeader?.startsWith('Bearer ')) {
20-
return res.status(401).json({ error: 'No token provided' });
21-
}
22-
23-
const token = authHeader.slice(7);
24-
const result = verifyJwtToken(token);
25-
if (result.error) {
26-
if (result.status === 500) {
27-
error('SESSION_SECRET not configured — cannot verify OAuth token', {
28-
ip: req.ip,
29-
path: req.path,
30-
});
31-
}
32-
return res.status(result.status).json({ error: result.error });
33-
}
34-
req.authMethod = 'oauth';
35-
req.user = result.user;
36-
return next();
16+
return handleOAuthJwt(req, res, next, { missingTokenError: 'No token provided' });
3717
};
3818
}

src/api/middleware/oauthJwt.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Shared OAuth JWT middleware helpers
3+
*/
4+
5+
import { error } from '../../logger.js';
6+
import { verifyJwtToken } from './verifyJwt.js';
7+
8+
/**
9+
* Extract Bearer token from Authorization header.
10+
*
11+
* @param {string|undefined} authHeader - Raw Authorization header value
12+
* @returns {string|null} JWT token if present, otherwise null
13+
*/
14+
export function getBearerToken(authHeader) {
15+
if (!authHeader?.startsWith('Bearer ')) {
16+
return null;
17+
}
18+
return authHeader.slice(7);
19+
}
20+
21+
/**
22+
* Authenticate request using OAuth JWT Bearer token.
23+
*
24+
* @param {import('express').Request} req - Express request
25+
* @param {import('express').Response} res - Express response
26+
* @param {import('express').NextFunction} next - Express next callback
27+
* @param {{ missingTokenError?: string }} [options] - Behavior options
28+
* @returns {boolean} True if middleware chain has been handled, false if no Bearer token was provided and no missing-token error was requested
29+
*/
30+
export function handleOAuthJwt(req, res, next, options = {}) {
31+
const token = getBearerToken(req.headers.authorization);
32+
if (!token) {
33+
if (options.missingTokenError) {
34+
res.status(401).json({ error: options.missingTokenError });
35+
return true;
36+
}
37+
return false;
38+
}
39+
40+
const result = verifyJwtToken(token);
41+
if (result.error) {
42+
if (result.status === 500) {
43+
error('SESSION_SECRET not configured — cannot verify OAuth token', {
44+
ip: req.ip,
45+
path: req.path,
46+
});
47+
}
48+
res.status(result.status).json({ error: result.error });
49+
return true;
50+
}
51+
52+
req.authMethod = 'oauth';
53+
req.user = result.user;
54+
next();
55+
return true;
56+
}

tests/api/middleware/auth.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,18 @@ describe('auth middleware', () => {
100100
expect(next).not.toHaveBeenCalled();
101101
});
102102

103+
it('should return Unauthorized when Authorization header is not Bearer and no API secret succeeds', () => {
104+
vi.stubEnv('BOT_API_SECRET', '');
105+
req.headers.authorization = 'Basic abc123';
106+
const middleware = requireAuth();
107+
108+
middleware(req, res, next);
109+
110+
expect(res.status).toHaveBeenCalledWith(401);
111+
expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized' });
112+
expect(next).not.toHaveBeenCalled();
113+
});
114+
103115
it('should return 401 with specific error when x-api-secret does not match', () => {
104116
vi.stubEnv('BOT_API_SECRET', 'test-secret');
105117
req.headers['x-api-secret'] = 'wrong-secret';

tests/api/middleware/oauth.test.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ describe('requireOAuth middleware', () => {
5252
expect(res.json).toHaveBeenCalledWith({ error: 'No token provided' });
5353
});
5454

55+
it('should still return No token provided even if x-api-secret header is present', () => {
56+
req.headers['x-api-secret'] = 'test-secret';
57+
const middleware = requireOAuth();
58+
59+
middleware(req, res, next);
60+
61+
expect(res.status).toHaveBeenCalledWith(401);
62+
expect(res.json).toHaveBeenCalledWith({ error: 'No token provided' });
63+
expect(next).not.toHaveBeenCalled();
64+
});
65+
5566
it('should return 500 when SESSION_SECRET is not set', () => {
5667
vi.stubEnv('SESSION_SECRET', '');
5768
req.headers.authorization = 'Bearer some-token';

0 commit comments

Comments
 (0)