-
Notifications
You must be signed in to change notification settings - Fork 2
feat: guild admin authentication & authorization #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
86ad3a4
38feff0
8114c01
0657948
e037302
2ea7ea4
fe7a652
6f31174
d287621
5c1dafe
51fc321
d9dd49e
0b41aaa
8ba3f53
81f238f
22ddbc4
bf9a30e
32a8084
9058e80
82b4a8a
8ae3cfc
dc6da3e
4927aa4
c1b9ae1
0687145
733b6e6
7892460
74cebb2
61c1e81
efe7303
e8e3b10
b375d6b
5c54a4a
c51105b
4db3706
15ca9f7
66f9f97
ef62f98
33591fd
92ff4fb
29a9af9
958487f
a610284
68f688a
a22697d
519a05c
8fd3516
1df4565
160d4f6
038e0e1
caa09b5
a77f0c6
e9737f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| /** | ||
| * OAuth2 JWT Middleware | ||
| * Verifies JWT tokens from Discord OAuth2 sessions | ||
| */ | ||
|
|
||
| import { handleOAuthJwt } from './oauthJwt.js'; | ||
|
|
||
| /** | ||
| * Creates middleware that verifies a JWT Bearer token from the Authorization header. | ||
| * Attaches the decoded user payload to req.user on success. | ||
| * | ||
| * @returns {import('express').RequestHandler} Express middleware function | ||
| */ | ||
| export function requireOAuth() { | ||
| return (req, res, next) => { | ||
| return handleOAuthJwt(req, res, next, { missingTokenError: 'No token provided' }); | ||
| }; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * Shared OAuth JWT middleware helpers | ||
| */ | ||
|
|
||
| import { error } from '../../logger.js'; | ||
| import { verifyJwtToken } from './verifyJwt.js'; | ||
|
|
||
| /** | ||
| * Extract Bearer token from Authorization header. | ||
| * | ||
| * @param {string|undefined} authHeader - Raw Authorization header value | ||
| * @returns {string|null} JWT token if present, otherwise null | ||
| */ | ||
| export function getBearerToken(authHeader) { | ||
| if (!authHeader?.startsWith('Bearer ')) { | ||
| return null; | ||
| } | ||
| return authHeader.slice(7); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exported function never imported outside its moduleLow Severity
|
||
|
|
||
| /** | ||
| * Authenticate request using OAuth JWT Bearer token. | ||
| * | ||
| * @param {import('express').Request} req - Express request | ||
| * @param {import('express').Response} res - Express response | ||
| * @param {import('express').NextFunction} next - Express next callback | ||
| * @param {{ missingTokenError?: string }} [options] - Behavior options | ||
| * @returns {boolean} True if middleware chain has been handled, false if no Bearer token was provided and no missing-token error was requested | ||
| */ | ||
| export function handleOAuthJwt(req, res, next, options = {}) { | ||
| const token = getBearerToken(req.headers.authorization); | ||
| if (!token) { | ||
| if (options.missingTokenError) { | ||
| res.status(401).json({ error: options.missingTokenError }); | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| const result = verifyJwtToken(token); | ||
| if (result.error) { | ||
| if (result.status === 500) { | ||
| error('SESSION_SECRET not configured — cannot verify OAuth token', { | ||
| ip: req.ip, | ||
| path: req.path, | ||
| }); | ||
| } | ||
| res.status(result.status).json({ error: result.error }); | ||
| return true; | ||
| } | ||
|
|
||
| req.authMethod = 'oauth'; | ||
| req.user = result.user; | ||
| next(); | ||
| return true; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| /** | ||
| * JWT Verification Helper | ||
| * Shared JWT verification logic used by both requireAuth and requireOAuth middleware | ||
| */ | ||
|
|
||
| import jwt from 'jsonwebtoken'; | ||
| import { getSessionToken } from '../utils/sessionStore.js'; | ||
|
|
||
| /** | ||
| * Lazily cached SESSION_SECRET — read from env on first call, then reused. | ||
| * Avoids per-request env lookup while remaining compatible with test stubs | ||
| * (vi.stubEnv sets process.env before the first call within each test). | ||
| * Call `_resetSecretCache()` in test teardown if needed. | ||
| */ | ||
| let _cachedSecret; | ||
|
|
||
| /** @internal Reset the cached secret (for test teardown). */ | ||
| export function _resetSecretCache() { | ||
| _cachedSecret = undefined; | ||
| } | ||
|
|
||
| function getSecret() { | ||
| if (_cachedSecret === undefined) { | ||
| _cachedSecret = process.env.SESSION_SECRET || ''; | ||
| } | ||
| return _cachedSecret; | ||
| } | ||
|
|
||
| /** | ||
| * Verify a JWT token and validate the associated server-side session. | ||
| * | ||
| * @param {string} token - The JWT Bearer token to verify | ||
| * @returns {{ user: Object } | { error: string, status: number }} | ||
| * On success: `{ user }` with the decoded JWT payload. | ||
| * On failure: `{ error, status }` with an error message and HTTP status code. | ||
| */ | ||
| export function verifyJwtToken(token) { | ||
BillChirico marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| const secret = getSecret(); | ||
| if (!secret) return { error: 'Session not configured', status: 500 }; | ||
|
|
||
| try { | ||
| const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); | ||
| if (!getSessionToken(decoded.userId)) { | ||
| return { error: 'Session expired or revoked', status: 401 }; | ||
| } | ||
| return { user: decoded }; | ||
| } catch { | ||
| return { error: 'Invalid or expired token', status: 401 }; | ||
| } | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.