- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler - Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto - Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example Made-with: Cursor
45 lines
1.5 KiB
JavaScript
45 lines
1.5 KiB
JavaScript
/**
|
|
* Signed OAuth state (userId) to prevent tampering when OAUTH_STATE_SECRET is set.
|
|
*/
|
|
|
|
import crypto from 'crypto'
|
|
import { config } from '../config/index.mjs'
|
|
|
|
export function buildOAuthState(userId) {
|
|
const secret = config.oauthStateSecret
|
|
if (!secret) {
|
|
return JSON.stringify({ userId })
|
|
}
|
|
const body = JSON.stringify({ userId, exp: Date.now() + 15 * 60 * 1000 })
|
|
const sig = crypto.createHmac('sha256', secret).update(body).digest('hex')
|
|
return Buffer.from(JSON.stringify({ b: body, s: sig })).toString('base64url')
|
|
}
|
|
|
|
export function parseOAuthState(state) {
|
|
if (!state || typeof state !== 'string') {
|
|
throw new Error('invalid_state')
|
|
}
|
|
const trimmed = state.trim()
|
|
const secret = config.oauthStateSecret
|
|
|
|
if (trimmed.startsWith('{')) {
|
|
const legacy = JSON.parse(trimmed)
|
|
if (!legacy.userId) throw new Error('invalid_state')
|
|
if (secret) {
|
|
throw new Error('unsigned_state_rejected')
|
|
}
|
|
return { userId: legacy.userId }
|
|
}
|
|
|
|
const raw = Buffer.from(trimmed, 'base64url').toString('utf8')
|
|
const outer = JSON.parse(raw)
|
|
if (!outer.b || !outer.s) throw new Error('invalid_state')
|
|
if (!secret) throw new Error('signed_state_requires_secret')
|
|
const expected = crypto.createHmac('sha256', secret).update(outer.b).digest('hex')
|
|
if (outer.s !== expected) throw new Error('invalid_state_signature')
|
|
const payload = JSON.parse(outer.b)
|
|
if (payload.exp != null && payload.exp < Date.now()) throw new Error('state_expired')
|
|
if (!payload.userId) throw new Error('invalid_state')
|
|
return { userId: payload.userId }
|
|
}
|