feat(auth): harden OAuth state generation and callback validation (closes #1562)

generateOAuthState() now throws when crypto.getRandomValues is unavailable
instead of silently degrading to a non-cryptographic Math.random() fallback —
OAuth state is a CSRF token and must be unpredictable.

The web /oauth/callback handler now rejects a returned `state` param that does
not parse to the expected 64-char-hex authId shape (a forgery indicator),
surfacing a clear error toast instead of proceeding with an undefined session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 18:23:38 -04:00
commit 2b84281346

View file

@ -50,20 +50,17 @@ export const parseOAuthCallbackParams = (location: string): CallbackParams => {
* @returns A random state for the OAuth 2.0 flow.
*/
export const generateOAuthState = (): string => {
// Generate a random state
const array = new Uint8Array(32);
// Use crypto.getRandomValues (available in both browser and Node.js)
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(array);
} else {
// Fallback for environments without crypto.getRandomValues
// This should not happen in modern environments
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
// OAuth state is a CSRF token — it MUST be unpredictable. crypto.getRandomValues
// is available in every supported runtime (browsers, Node ≥15); if it's somehow
// missing, fail loudly rather than silently degrading to Math.random (whose
// output is predictable from a small amount of observed state).
if (typeof crypto === "undefined" || !crypto.getRandomValues) {
throw new Error(
"crypto.getRandomValues is not available; refusing to generate an OAuth state with a non-cryptographic RNG.",
);
}
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);