{"version":3,"file":"authorize.mjs","names":[],"sources":["../../../../../src/astro/routes/api/oauth/authorize.ts"],"sourcesContent":["/**\n * GET/POST /_emdash/oauth/authorize\n *\n * OAuth 2.1 Authorization Endpoint. Handles both the consent page (GET)\n * and consent submission (POST).\n *\n * GET: Renders an HTML consent page showing which client is requesting\n *      access and which scopes are being requested.\n * POST: Processes the user's decision (approve/deny) and redirects\n *       to the client's redirect_uri with an authorization code or error.\n *\n * Requires an authenticated session (not token auth). If unauthenticated,\n * redirects to login with a return URL.\n */\n\nimport type { APIRoute } from \"astro\";\n\nimport { resolveUserAuthz } from \"#auth/authz.js\";\nimport { AuthzRepository } from \"#db/repositories/authz.js\";\n\nimport { escapeHtml } from \"#api/escape.js\";\nimport {\n\tbuildDeniedRedirect,\n\thandleAuthorizationApproval,\n\tvalidateRedirectUri,\n} from \"#api/handlers/oauth-authorization.js\";\nimport { lookupOAuthClient, validateClientRedirectUri } from \"#api/handlers/oauth-clients.js\";\nimport { getPublicOrigin } from \"#api/public-url.js\";\n\nexport const prerender = false;\n\n// ---------------------------------------------------------------------------\n// CSRF (SEC-18): Double-submit cookie pattern\n// ---------------------------------------------------------------------------\n\nconst CSRF_COOKIE_NAME = \"emdash_oauth_csrf\";\n\n/** Generate a 32-byte random token as hex. */\nfunction generateCsrfToken(): string {\n\tconst bytes = new Uint8Array(32);\n\tcrypto.getRandomValues(bytes);\n\treturn Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** Build the Set-Cookie header value for the CSRF token. */\nfunction csrfCookieHeader(token: string, request: Request, siteUrl?: string): string {\n\t// SameSite=Strict prevents cross-site form submission.\n\t// HttpOnly: the token value is embedded in the form hidden field server-side,\n\t// so JS never needs to read the cookie. HttpOnly adds defense-in-depth.\n\t// Secure is set when:\n\t//   - siteUrl is configured and uses https (proxy case — request may be http internally), OR\n\t//   - the actual request is over https (non-proxy case, preserve existing behaviour)\n\tconst isSecure = siteUrl\n\t\t? siteUrl.startsWith(\"https:\")\n\t\t: new URL(request.url).protocol === \"https:\";\n\tconst secure = isSecure ? \"; Secure\" : \"\";\n\treturn `${CSRF_COOKIE_NAME}=${token}; Path=/_emdash/oauth/authorize; HttpOnly; SameSite=Strict${secure}`;\n}\n\n/** Extract the CSRF token from the request's cookies. */\nfunction getCsrfCookie(request: Request): string | null {\n\tconst cookieHeader = request.headers.get(\"Cookie\");\n\tif (!cookieHeader) return null;\n\tconst match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${CSRF_COOKIE_NAME}=([^;]+)`));\n\treturn match?.[1] ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// OAuth scopes are policy slugs. The consent page shows each requested\n// policy by name; an empty request means \"act as me\" (every policy the\n// user's role holds).\n// ---------------------------------------------------------------------------\n\nconst POLICY_SLUG = /^[a-z0-9][a-z0-9_-]*$/;\n\nfunction parseScopeParam(value: string | null): string[] {\n\treturn [...new Set((value ?? \"\").split(\" \").filter((s) => POLICY_SLUG.test(s)))];\n}\n\n// ---------------------------------------------------------------------------\n// GET: Render consent page\n// ---------------------------------------------------------------------------\n\nexport const GET: APIRoute = async ({ url, request, locals }) => {\n\tconst { emdash, user } = locals;\n\n\t// Validate required OAuth params before rendering\n\tconst clientId = url.searchParams.get(\"client_id\");\n\tconst redirectUri = url.searchParams.get(\"redirect_uri\");\n\tconst responseType = url.searchParams.get(\"response_type\");\n\tconst codeChallenge = url.searchParams.get(\"code_challenge\");\n\tconst codeChallengeMethod = url.searchParams.get(\"code_challenge_method\");\n\tconst scope = url.searchParams.get(\"scope\");\n\tconst state = url.searchParams.get(\"state\");\n\n\t// Basic validation — detailed validation happens on POST\n\tif (!clientId || !redirectUri || responseType !== \"code\" || !codeChallenge) {\n\t\treturn new Response(\n\t\t\trenderErrorPage(\"Invalid authorization request. Missing required parameters.\"),\n\t\t\t{\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t},\n\t\t);\n\t}\n\n\tif (codeChallengeMethod && codeChallengeMethod !== \"S256\") {\n\t\treturn new Response(renderErrorPage(\"Only S256 code challenge method is supported.\"), {\n\t\t\tstatus: 400,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\n\t// Validate client_id is registered and redirect_uri is in the allowlist.\n\t// This check happens BEFORE authentication so we never redirect to an\n\t// unregistered URI (even for the login redirect, we only redirect to our\n\t// own login page, not to the client's redirect_uri).\n\tif (emdash?.db) {\n\t\tconst client = await lookupOAuthClient(emdash.db, clientId);\n\t\tif (!client) {\n\t\t\treturn new Response(renderErrorPage(\"Unknown client application.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\n\t\tconst clientUriError = validateClientRedirectUri(redirectUri, client.redirectUris);\n\t\tif (clientUriError) {\n\t\t\treturn new Response(renderErrorPage(\"The redirect URI is not registered for this client.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\t}\n\n\t// If not authenticated, redirect to login with return URL\n\tif (!user) {\n\t\tconst loginUrl = new URL(\"/_emdash/admin/login\", getPublicOrigin(url, emdash?.config));\n\t\tloginUrl.searchParams.set(\"redirect\", url.pathname + url.search);\n\t\treturn Response.redirect(loginUrl.toString(), 302);\n\t}\n\n\t// Requested policies, or everything the user's role holds when the\n\t// client asked for nothing specific.\n\tconst held = await resolveUserAuthz(emdash.db, user);\n\tconst requestedScopes = (() => {\n\t\tconst asked = parseScopeParam(scope);\n\t\tif (asked.length === 0) return [...held.rolePolicies];\n\t\tconst mine = new Set(held.rolePolicies);\n\t\treturn asked.filter((s) => mine.has(s));\n\t})();\n\n\tif (requestedScopes.length === 0) {\n\t\treturn new Response(renderErrorPage(\"None of the requested policies are held by your role.\"), {\n\t\t\tstatus: 400,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\tconst policyNames = new Map<string, string>();\n\tfor (const p of await new AuthzRepository(emdash.db).listPolicies()) policyNames.set(p.slug, p.name);\n\n\t// SEC-18: Generate CSRF token for the consent form (double-submit cookie pattern)\n\tconst csrfToken = generateCsrfToken();\n\n\t// Render the consent page\n\tconst html = renderConsentPage({\n\t\tclientId,\n\t\tscopes: requestedScopes.map((slug) => ({ slug, label: policyNames.get(slug) ?? slug })),\n\t\tredirectUri,\n\t\tresponseType,\n\t\tcodeChallenge,\n\t\tcodeChallengeMethod: codeChallengeMethod ?? \"S256\",\n\t\tstate: state ?? \"\",\n\t\tresource: url.searchParams.get(\"resource\") ?? \"\",\n\t\tuserName: user.name ?? user.email,\n\t\tcsrfToken,\n\t});\n\n\treturn new Response(html, {\n\t\theaders: {\n\t\t\t\"Content-Type\": \"text/html; charset=utf-8\",\n\t\t\t\"Set-Cookie\": csrfCookieHeader(csrfToken, request, getPublicOrigin(url, emdash?.config)),\n\t\t},\n\t});\n};\n\n// ---------------------------------------------------------------------------\n// POST: Process consent\n// ---------------------------------------------------------------------------\n\nexport const POST: APIRoute = async ({ request, locals }) => {\n\tconst { emdash, user } = locals;\n\n\tif (!emdash?.db) {\n\t\treturn new Response(renderErrorPage(\"EmDash is not initialized.\"), {\n\t\t\tstatus: 500,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\n\tif (!user) {\n\t\treturn new Response(renderErrorPage(\"Authentication required.\"), {\n\t\t\tstatus: 401,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\n\tconst formData = await request.formData();\n\tconst field = (name: string, fallback = \"\"): string => {\n\t\tconst v = formData.get(name);\n\t\treturn typeof v === \"string\" ? v : fallback;\n\t};\n\tconst asked = parseScopeParam(new URL(request.url).searchParams.get(\"scope\"));\n\tconst requestedScopes = new Set(\n\t\tasked.length ? asked : user ? (await resolveUserAuthz(emdash.db, user)).rolePolicies : [],\n\t);\n\tconst selectedScopes = [\n\t\t...new Set(\n\t\t\tformData\n\t\t\t\t.getAll(\"scope\")\n\t\t\t\t.filter((value): value is string => typeof value === \"string\")\n\t\t\t\t.filter((s) => POLICY_SLUG.test(s))\n\t\t\t\t.filter((scope) => requestedScopes.has(scope)),\n\t\t),\n\t];\n\n\t// SEC-18: Validate CSRF token (double-submit cookie pattern).\n\t// The form includes a hidden csrf_token field; the cookie has the same value.\n\t// An attacker cannot read the cookie to forge the form field (HttpOnly + SameSite=Strict).\n\tconst formCsrf = field(\"csrf_token\");\n\tconst cookieCsrf = getCsrfCookie(request);\n\tconst csrfError = new Response(\n\t\trenderErrorPage(\"Invalid or missing CSRF token. Please try again.\"),\n\t\t{ status: 403, headers: { \"Content-Type\": \"text/html; charset=utf-8\" } },\n\t);\n\tif (!formCsrf || !cookieCsrf) return csrfError;\n\n\t// Constant-time comparison: hash both values to fixed-length 32-byte digests,\n\t// then XOR every byte pair. This avoids crypto.subtle.timingSafeEqual which is\n\t// a Cloudflare Workers extension and doesn't exist in Node.js.\n\t// The SHA-256 pre-hash ensures fixed length, eliminating length-leaking.\n\tconst csrfEncoder = new TextEncoder();\n\tconst [csrfHashA, csrfHashB] = await Promise.all([\n\t\tcrypto.subtle.digest(\"SHA-256\", csrfEncoder.encode(formCsrf)),\n\t\tcrypto.subtle.digest(\"SHA-256\", csrfEncoder.encode(cookieCsrf)),\n\t]);\n\tconst a = new Uint8Array(csrfHashA);\n\tconst b = new Uint8Array(csrfHashB);\n\tlet diff = 0;\n\t// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- tsgo needs these\n\tfor (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!;\n\tif (diff !== 0) return csrfError;\n\n\tconst action = field(\"action\");\n\tconst redirectUri = field(\"redirect_uri\");\n\tconst state = field(\"state\") || undefined;\n\n\tif (!redirectUri) {\n\t\treturn new Response(renderErrorPage(\"Missing redirect_uri.\"), {\n\t\t\tstatus: 400,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\n\t// Validate redirect_uri scheme/host before using it for any redirect\n\tconst uriError = validateRedirectUri(redirectUri);\n\tif (uriError) {\n\t\treturn new Response(renderErrorPage(escapeHtml(uriError)), {\n\t\t\tstatus: 400,\n\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t});\n\t}\n\n\t// User denied — SEC-44: validate redirect_uri against client's registered URIs\n\t// before redirecting, to prevent open redirect on the deny path.\n\tif (action === \"deny\") {\n\t\tconst clientId = field(\"client_id\");\n\t\tif (!clientId) {\n\t\t\treturn new Response(renderErrorPage(\"Missing client_id.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\n\t\tconst client = await lookupOAuthClient(emdash.db, clientId);\n\t\tif (!client) {\n\t\t\treturn new Response(renderErrorPage(\"Unknown client application.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\n\t\tconst clientUriError = validateClientRedirectUri(redirectUri, client.redirectUris);\n\t\tif (clientUriError) {\n\t\t\treturn new Response(renderErrorPage(\"The redirect URI is not registered for this client.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\n\t\tconst denyUrl = buildDeniedRedirect(redirectUri, state);\n\t\treturn Response.redirect(denyUrl, 302);\n\t}\n\n\t// Clearing every checkbox is a refusal, not \"act as me\": nothing is granted.\n\tif (selectedScopes.length === 0) {\n\t\ttry {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\t\t\terrorUrl.searchParams.set(\"error\", \"invalid_scope\");\n\t\t\terrorUrl.searchParams.set(\"error_description\", \"No selected permission can be granted\");\n\t\t\tif (state) errorUrl.searchParams.set(\"state\", state);\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t} catch {\n\t\t\treturn new Response(renderErrorPage(\"No permission selected.\"), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\t}\n\n\t// User approved — process the authorization\n\tconst result = await handleAuthorizationApproval(emdash.db, user.id, { role: user.role, roleId: user.roleId ?? null }, {\n\t\tresponse_type: field(\"response_type\", \"code\"),\n\t\tclient_id: field(\"client_id\"),\n\t\tredirect_uri: redirectUri,\n\t\tscope: selectedScopes.join(\" \"),\n\t\tstate,\n\t\tcode_challenge: field(\"code_challenge\"),\n\t\tcode_challenge_method: field(\"code_challenge_method\", \"S256\"),\n\t\tresource: field(\"resource\") || undefined,\n\t});\n\n\tif (!result.success) {\n\t\tconst errMsg = result.error?.message ?? \"Authorization failed\";\n\t\tconst invalidScope = result.error?.code === \"INVALID_SCOPE\";\n\t\t// On error, redirect back with error params — use generic description to avoid\n\t\t// leaking internal error details to the (already-validated) redirect target\n\t\ttry {\n\t\t\tconst errorUrl = new URL(redirectUri);\n\t\t\terrorUrl.searchParams.set(\"error\", invalidScope ? \"invalid_scope\" : \"server_error\");\n\t\t\terrorUrl.searchParams.set(\n\t\t\t\t\"error_description\",\n\t\t\t\tinvalidScope ? \"No selected permission can be granted\" : \"Authorization failed\",\n\t\t\t);\n\t\t\tif (state) errorUrl.searchParams.set(\"state\", state);\n\t\t\treturn Response.redirect(errorUrl.toString(), 302);\n\t\t} catch {\n\t\t\treturn new Response(renderErrorPage(escapeHtml(errMsg)), {\n\t\t\t\tstatus: 400,\n\t\t\t\theaders: { \"Content-Type\": \"text/html; charset=utf-8\" },\n\t\t\t});\n\t\t}\n\t}\n\n\treturn Response.redirect(result.data.redirect_url, 302);\n};\n\n// ---------------------------------------------------------------------------\n// HTML rendering\n// ---------------------------------------------------------------------------\n\nfunction renderConsentPage(params: {\n\tclientId: string;\n\tscopes: Array<{ slug: string; label: string }>;\n\tredirectUri: string;\n\tresponseType: string;\n\tcodeChallenge: string;\n\tcodeChallengeMethod: string;\n\tstate: string;\n\tresource: string;\n\tuserName: string;\n\tcsrfToken: string;\n}): string {\n\tconst scopeList = params.scopes\n\t\t.map(\n\t\t\t(s) =>\n\t\t\t\t`<li><label><input type=\"checkbox\" name=\"scope\" value=\"${escapeHtml(s.slug)}\" checked><span>${escapeHtml(s.label)}</span></label></li>`,\n\t\t)\n\t\t.join(\"\\n\");\n\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Authorize Application — EmDash</title>\n<style>\n  * { margin: 0; padding: 0; box-sizing: border-box; }\n  body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem; }\n  .card { background: #171717; border: 1px solid #262626; border-radius: 12px; max-width: 420px; width: 100%; padding: 2rem; }\n  h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }\n  .client-id { color: #a3a3a3; font-size: 0.875rem; word-break: break-all; margin-bottom: 1.5rem; }\n  .user { color: #a3a3a3; font-size: 0.875rem; margin-bottom: 1rem; }\n  h2 { font-size: 0.875rem; font-weight: 500; color: #a3a3a3; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.75rem; }\n  ul { list-style: none; margin-bottom: 1.5rem; }\n  li { padding: 0.5rem 0; border-bottom: 1px solid #262626; font-size: 0.875rem; }\n  li:last-child { border-bottom: none; }\n  label { display: flex; align-items: flex-start; gap: 0.625rem; cursor: pointer; }\n  input[type=\"checkbox\"] { margin-top: 0.125rem; accent-color: #2563eb; }\n  .actions { display: flex; gap: 0.75rem; }\n  button { flex: 1; padding: 0.625rem 1rem; border-radius: 8px; border: none; font-size: 0.875rem; font-weight: 500; cursor: pointer; }\n  .approve { background: #2563eb; color: white; }\n  .approve:hover { background: #1d4ed8; }\n  .deny { background: #262626; color: #e5e5e5; }\n  .deny:hover { background: #333; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n  <h1>Authorize Application</h1>\n  <p class=\"client-id\">${escapeHtml(params.clientId)}</p>\n  <p class=\"user\">Signed in as <strong>${escapeHtml(params.userName)}</strong></p>\n  <form method=\"POST\">\n    <h2>Permissions requested</h2>\n    <ul>${scopeList}</ul>\n    <input type=\"hidden\" name=\"csrf_token\" value=\"${escapeHtml(params.csrfToken)}\">\n    <input type=\"hidden\" name=\"response_type\" value=\"${escapeHtml(params.responseType)}\">\n    <input type=\"hidden\" name=\"client_id\" value=\"${escapeHtml(params.clientId)}\">\n    <input type=\"hidden\" name=\"redirect_uri\" value=\"${escapeHtml(params.redirectUri)}\">\n    <input type=\"hidden\" name=\"state\" value=\"${escapeHtml(params.state)}\">\n    <input type=\"hidden\" name=\"code_challenge\" value=\"${escapeHtml(params.codeChallenge)}\">\n    <input type=\"hidden\" name=\"code_challenge_method\" value=\"${escapeHtml(params.codeChallengeMethod)}\">\n    <input type=\"hidden\" name=\"resource\" value=\"${escapeHtml(params.resource)}\">\n    <div class=\"actions\">\n      <button type=\"submit\" name=\"action\" value=\"deny\" class=\"deny\">Deny</button>\n      <button type=\"submit\" name=\"action\" value=\"approve\" class=\"approve\">Approve</button>\n    </div>\n  </form>\n</div>\n</body>\n</html>`;\n}\n\n\nfunction renderErrorPage(message: string): string {\n\treturn `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Authorization Error — EmDash</title>\n<style>\n  * { margin: 0; padding: 0; box-sizing: border-box; }\n  body { font-family: system-ui, -apple-system, sans-serif; background: #0a0a0a; color: #e5e5e5; display: flex; justify-content: center; align-items: center; min-height: 100vh; padding: 1rem; }\n  .card { background: #171717; border: 1px solid #262626; border-radius: 12px; max-width: 420px; width: 100%; padding: 2rem; }\n  h1 { font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: #ef4444; }\n  p { font-size: 0.875rem; color: #a3a3a3; }\n</style>\n</head>\n<body>\n<div class=\"card\">\n  <h1>Authorization Error</h1>\n  <p>${escapeHtml(message)}</p>\n</div>\n</body>\n</html>`;\n}\n"],"mappings":"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"}