fix: extend OAuth state cookie lifetime to match the 10-minute state TTL (#544)
This commit is contained in:
parent
b32c0bd841
commit
61b32ec647
@ -15,6 +15,16 @@ export function createBaseAuthConfig() {
|
|||||||
// /api/auth endpoint. Header lookup is case-insensitive.
|
// /api/auth endpoint. Header lookup is case-insensitive.
|
||||||
ipAddressHeaders: ["cf-connecting-ip"],
|
ipAddressHeaders: ["cf-connecting-ip"],
|
||||||
},
|
},
|
||||||
|
// Better Auth writes the OAuth state verification row with a 10-minute
|
||||||
|
// expiry but sets the matching signed cookie with maxAge 300, and
|
||||||
|
// parseGenericState checks the cookie before the row's expiresAt — so the
|
||||||
|
// intended 10-minute window is unreachable. The GSC and GA4 providers
|
||||||
|
// below force `select_account consent`, a two-screen Google flow, so a
|
||||||
|
// user who takes more than 5 minutes returns with a live verification row
|
||||||
|
// and a dead cookie and fails with "State mismatch: State not persisted
|
||||||
|
// correctly". The row's expiresAt still enforces the real 10-minute
|
||||||
|
// window and the state stays single-use, so this is not a weakening.
|
||||||
|
cookies: { state: { attributes: { maxAge: 600 } } },
|
||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
|
// Encrypt OAuth access/refresh tokens at rest in D1. Also covers the
|
||||||
|
|||||||
@ -68,14 +68,27 @@ function createAuth() {
|
|||||||
const auth = betterAuth({
|
const auth = betterAuth({
|
||||||
baseURL: baseUrl,
|
baseURL: baseUrl,
|
||||||
secret: getHostedSecret(),
|
secret: getHostedSecret(),
|
||||||
// The api-key plugin logs every verification failure at error level — a
|
|
||||||
// stale key or a throttled caller included. The /mcp handler already logs
|
|
||||||
// the response it returns at the right level (debug for 401, warn for 429),
|
|
||||||
// so drop the duplicate.
|
|
||||||
logger: {
|
logger: {
|
||||||
log: (level, message, ...args) => {
|
log: (level, message, ...args: unknown[]) => {
|
||||||
|
// The api-key plugin logs every verification failure at error level — a
|
||||||
|
// stale key or a throttled caller included. The /mcp handler already logs
|
||||||
|
// the response it returns at the right level (debug for 401, warn for 429),
|
||||||
|
// so drop the duplicate.
|
||||||
if (message.startsWith("Failed to validate API key")) return;
|
if (message.startsWith("Failed to validate API key")) return;
|
||||||
console[level](`[better-auth] ${message}`, ...args);
|
// "Failed to parse state" is user/browser behavior: a replayed OAuth
|
||||||
|
// callback URL (back button, restored tab), or a consent screen left
|
||||||
|
// open past the state's lifetime. The request already redirects the
|
||||||
|
// user to an error page; nothing here is on-call actionable.
|
||||||
|
const effectiveLevel =
|
||||||
|
level === "error" && message === "Failed to parse state"
|
||||||
|
? "warn"
|
||||||
|
: level;
|
||||||
|
// Also drops Better Auth's ISO-timestamp prefix, which makes every log
|
||||||
|
// line fingerprint as its own error group in observability.
|
||||||
|
console[effectiveLevel === "debug" ? "log" : effectiveLevel](
|
||||||
|
`[better-auth] ${message}`,
|
||||||
|
...args,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...baseAuthConfig,
|
...baseAuthConfig,
|
||||||
|
|||||||
@ -103,6 +103,7 @@ export async function handleMcpApiKeyRequest(
|
|||||||
// clients (see lib/auth-api-key.ts). Cloudflare's counter is per-colo
|
// clients (see lib/auth-api-key.ts). Cloudflare's counter is per-colo
|
||||||
// best-effort, which is all this needs to be: credits bound spend, this
|
// best-effort, which is all this needs to be: credits bound spend, this
|
||||||
// bounds runaway request volume.
|
// bounds runaway request volume.
|
||||||
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the binding is declared in alchemy.run.ts; env stays unknown through the MCP handler chain
|
||||||
const rateLimit = (env as { MCP_RATE_LIMIT?: RateLimit }).MCP_RATE_LIMIT;
|
const rateLimit = (env as { MCP_RATE_LIMIT?: RateLimit }).MCP_RATE_LIMIT;
|
||||||
if (rateLimit) {
|
if (rateLimit) {
|
||||||
const { success } = await rateLimit.limit({ key: userId });
|
const { success } = await rateLimit.limit({ key: userId });
|
||||||
|
|||||||
@ -106,7 +106,7 @@ async function checkBatchLive(
|
|||||||
results.push(outcome.value);
|
results.push(outcome.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reason = outcome.reason;
|
const reason: unknown = outcome.reason;
|
||||||
const code = reason instanceof AppError ? reason.code : "UNKNOWN";
|
const code = reason instanceof AppError ? reason.code : "UNKNOWN";
|
||||||
const message = reason instanceof Error ? reason.message : String(reason);
|
const message = reason instanceof Error ? reason.message : String(reason);
|
||||||
// DataForSEO erring on its own side is a provider flake, not our bug: the
|
// DataForSEO erring on its own side is a provider flake, not our bug: the
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user