UNPKG

wcz-layout

Version:

1 lines 9.19 kB
{"version":3,"file":"auth.mjs","names":["jose","buildUser","sanitizeReturnTo","ProviderId","TokenPayload","buildAuthUrl","createPkce","createState","exchangeCode","getProvider","providers","getAppSession","redirect","location","Response","status","headers","Location","resolveRedirectUri","origin","resolveProviderId","value","provider","find","candidate","id","handleLogin","Promise","getRequestUrl","url","returnTo","searchParams","get","requestedProvider","length","encodeURIComponent","providerId","verifier","challenge","state","session","update","pkceVerifier","redirectUri","handleCallback","expectedState","data","error","clear","code","result","codeVerifier","refresh_token","Error","id_token","claims","decodeJwt","user","employeeId","getEmployeeByEmployeeId","employee","name","nameTitleCase","department","departmentId","toUpperCase","companyName","companyCode","refreshToken","undefined","message","console","handleLogout","logoutUrl"],"sources":["../src/lib/auth/authHandlers.ts"],"sourcesContent":["import * as jose from \"jose\";\nimport { buildUser, sanitizeReturnTo } from \"~/lib/utils\";\nimport type { ProviderId } from \"~/models/User\";\nimport type { TokenPayload } from \"~/models/TokenPayload\";\nimport { buildAuthUrl, createPkce, createState, exchangeCode } from \"./oidc\";\nimport { getProvider, providers } from \"./providers\";\nimport { getAppSession } from \"./session\";\n\n// `@tanstack/react-start/server` pulls in the SSR renderer (`react-dom/server`),\n// so it is imported dynamically to keep it out of the client bundle.\n\n/**\n * Server-side handlers for the OAuth2 Authorization Code (PKCE) flow. They are\n * mounted by thin route files (`/auth/login`, `/auth/callback`, `/auth/logout`)\n * and drive the full server-side login — the browser never loads an auth library.\n * The chosen provider travels in the session alongside the PKCE verifier and\n * `state`, so one callback route serves every provider.\n */\n\nconst redirect = (location: string): Response =>\n new Response(null, { status: 302, headers: { Location: location } });\n\n/** The OAuth redirect URI, derived from the request origin. */\nconst resolveRedirectUri = (origin: string): string => `${origin}/auth/callback`;\n\nconst resolveProviderId = (value: string | null): ProviderId => {\n const provider = providers.find((candidate) => candidate.id === value);\n return provider?.id ?? \"entra\";\n};\n\n/**\n * GET /auth/login — start the flow: stash PKCE + state + provider, redirect to the IdP.\n * Without `?provider=` and with more than one provider registered, sends the user to\n * the chooser at `/login` instead; a single provider (whichever it is) starts right away.\n */\nexport async function handleLogin(): Promise<Response> {\n const { getRequestUrl } = await import(\"@tanstack/react-start/server\");\n const url = getRequestUrl();\n const returnTo = sanitizeReturnTo(url.searchParams.get(\"returnTo\"));\n const requestedProvider = url.searchParams.get(\"provider\");\n if (!requestedProvider && providers.length > 1)\n return redirect(`/login?returnTo=${encodeURIComponent(returnTo)}`);\n const providerId = requestedProvider ? resolveProviderId(requestedProvider) : providers[0].id;\n\n const { verifier, challenge } = createPkce();\n const state = createState();\n\n const session = await getAppSession();\n await session.update({ pkceVerifier: verifier, state, returnTo, provider: providerId });\n\n return redirect(\n buildAuthUrl(getProvider(providerId), {\n redirectUri: resolveRedirectUri(url.origin),\n state,\n challenge,\n }),\n );\n}\n\n/** GET /auth/callback — finish the flow: validate state, exchange code, set session. */\nexport async function handleCallback(): Promise<Response> {\n const { getRequestUrl } = await import(\"@tanstack/react-start/server\");\n const url = getRequestUrl();\n const session = await getAppSession();\n const {\n pkceVerifier,\n state: expectedState,\n returnTo = \"/\",\n provider: providerId = \"entra\",\n } = session.data;\n\n const error = url.searchParams.get(\"error\");\n if (error) {\n await session.clear();\n return redirect(`/?auth_error=${encodeURIComponent(error)}`);\n }\n\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (!code || !state || !pkceVerifier || state !== expectedState) {\n await session.clear();\n return redirect(\"/?auth_error=invalid_state\");\n }\n\n try {\n const result = await exchangeCode(getProvider(providerId), {\n code,\n redirectUri: resolveRedirectUri(url.origin),\n codeVerifier: pkceVerifier,\n });\n\n if (!result.refresh_token)\n throw new Error(\"No refresh token returned (is the offline_access scope granted?)\");\n if (!result.id_token) throw new Error(\"No id token returned (is the openid scope granted?)\");\n\n // Not signature-verified on purpose: this id token came straight from the\n // provider's token endpoint over TLS, in response to our own client-authenticated\n // request (OIDC Core 3.1.3.7 permits skipping verification on this leg).\n const claims = jose.decodeJwt<TokenPayload>(result.id_token);\n const user = buildUser(claims, providerId);\n\n if (providerId === \"aws\" && user.employeeId) {\n const { getEmployeeByEmployeeId } = await import(\"~/data/server/peoplesoft\");\n const employee = await getEmployeeByEmployeeId({ data: { employeeId: user.employeeId } });\n user.name = employee.nameTitleCase;\n user.department = employee.departmentId.toUpperCase();\n user.companyName = employee.companyCode;\n }\n\n // Persist the refresh token + trimmed user; drop the spent transient values.\n await session.update({\n refreshToken: result.refresh_token,\n user,\n pkceVerifier: undefined,\n state: undefined,\n returnTo: undefined,\n });\n\n return redirect(returnTo);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"token_exchange_failed\";\n console.error(\"[auth] code exchange failed:\", message);\n await session.clear();\n return redirect(`/?auth_error=${encodeURIComponent(message)}`);\n }\n}\n\n/** GET /auth/logout — clear the local session and sign out of the provider's session. */\nexport async function handleLogout(): Promise<Response> {\n const { getRequestUrl } = await import(\"@tanstack/react-start/server\");\n const url = getRequestUrl();\n const session = await getAppSession();\n const providerId = session.data.provider ?? \"entra\";\n await session.clear();\n return redirect(getProvider(providerId).logoutUrl(`${url.origin}/`));\n}\n"],"mappings":"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"}