UNPKG

wcz-layout

Version:

1 lines 5.92 kB
{"version":3,"file":"auth.mjs","names":[],"sources":["../src/lib/auth/authHandlers.ts"],"sourcesContent":["import { buildUser } from \"~/lib/utils\";\nimport { getAppSession } from \"./session\";\n\n// These handlers run only as server route handlers, so their server-only deps are\n// imported dynamically to keep them out of the client bundle: `entra` pulls in\n// `@azure/msal-node`, and `@tanstack/react-start/server` pulls in the SSR renderer\n// (`react-dom/server`).\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 * in the consuming app and drive the full server-side Entra login — the browser\n * never loads an auth library.\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\n/**\n * Only same-origin paths are allowed, so `returnTo` cannot become an open\n * redirect. Backslash counts as a separator too: browsers normalize `/\\evil`\n * to the protocol-relative `//evil`.\n */\nconst sanitizeReturnTo = (value: string | null): string =>\n value && /^\\/(?![/\\\\])/.test(value) ? value : \"/\";\n\n/** GET /auth/login — start the flow: stash PKCE + state, redirect to Entra. */\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\n const { cryptoProvider, buildAuthCodeUrl } = await import(\"./entra\");\n const { verifier, challenge } = await cryptoProvider.generatePkceCodes();\n const state = cryptoProvider.createNewGuid();\n\n const session = await getAppSession();\n await session.update({ pkceVerifier: verifier, state, returnTo });\n\n const authUrl = await buildAuthCodeUrl({\n redirectUri: resolveRedirectUri(url.origin),\n state,\n codeChallenge: challenge,\n });\n return redirect(authUrl);\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 { pkceVerifier, state: expectedState, returnTo = \"/\" } = 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 { exchangeCodeForSession } = await import(\"./entra\");\n const { refreshToken, claims } = await exchangeCodeForSession({\n code,\n redirectUri: resolveRedirectUri(url.origin),\n codeVerifier: pkceVerifier,\n });\n\n // Persist the refresh token + trimmed user; drop the spent transient values.\n await session.update({\n refreshToken,\n user: buildUser(claims),\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 Entra 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 await session.clear();\n const { buildLogoutUrl } = await import(\"./entra\");\n return redirect(buildLogoutUrl(`${url.origin}/`));\n}\n"],"mappings":";;;;;;;;;AAeA,MAAM,YAAY,aAChB,IAAI,SAAS,MAAM;CAAE,QAAQ;CAAK,SAAS,EAAE,UAAU,SAAS;AAAE,CAAC;;AAGrE,MAAM,sBAAsB,WAA2B,GAAG,OAAM;;;;;;AAOhE,MAAM,oBAAoB,UACxB,SAAS,eAAe,KAAK,KAAK,IAAI,QAAQ;;AAGhD,eAAsB,cAAiC;CACrD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,MAAM,cAAc;CAC1B,MAAM,WAAW,iBAAiB,IAAI,aAAa,IAAI,UAAU,CAAC;CAElE,MAAM,EAAE,gBAAgB,qBAAqB,MAAM,OAAO;CAC1D,MAAM,EAAE,UAAU,cAAc,MAAM,eAAe,kBAAkB;CACvE,MAAM,QAAQ,eAAe,cAAc;CAG3C,OAAM,MADgB,cAAc,EAAA,CACtB,OAAO;EAAE,cAAc;EAAU;EAAO;CAAS,CAAC;CAEhE,MAAM,UAAU,MAAM,iBAAiB;EACrC,aAAa,mBAAmB,IAAI,MAAM;EAC1C;EACA,eAAe;CACjB,CAAC;CACD,OAAO,SAAS,OAAO;AACzB;;AAGA,eAAsB,iBAAoC;CACxD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,MAAM,cAAc;CAC1B,MAAM,UAAU,MAAM,cAAc;CACpC,MAAM,EAAE,cAAc,OAAO,eAAe,WAAW,QAAQ,QAAQ;CAEvE,MAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;CAC1C,IAAI,OAAO;EACT,MAAM,QAAQ,MAAM;EACpB,OAAO,SAAS,gBAAgB,mBAAmB,KAAK,GAAG;CAC7D;CAEA,MAAM,OAAO,IAAI,aAAa,IAAI,MAAM;CACxC,MAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;CAC1C,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,gBAAgB,UAAU,eAAe;EAC/D,MAAM,QAAQ,MAAM;EACpB,OAAO,SAAS,4BAA4B;CAC9C;CAEA,IAAI;EACF,MAAM,EAAE,2BAA2B,MAAM,OAAO;EAChD,MAAM,EAAE,cAAc,WAAW,MAAM,uBAAuB;GAC5D;GACA,aAAa,mBAAmB,IAAI,MAAM;GAC1C,cAAc;EAChB,CAAC;EAGD,MAAM,QAAQ,OAAO;GACnB;GACA,MAAM,UAAU,MAAM;GACtB,cAAc,KAAA;GACd,OAAO,KAAA;GACP,UAAU,KAAA;EACZ,CAAC;EAED,OAAO,SAAS,QAAQ;CAC1B,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;EACzD,QAAQ,MAAM,gCAAgC,OAAO;EACrD,MAAM,QAAQ,MAAM;EACpB,OAAO,SAAS,gBAAgB,mBAAmB,OAAO,GAAG;CAC/D;AACF;;AAGA,eAAsB,eAAkC;CACtD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CACvC,MAAM,MAAM,cAAc;CAE1B,OAAM,MADgB,cAAc,EAAA,CACtB,MAAM;CACpB,MAAM,EAAE,mBAAmB,MAAM,OAAO;CACxC,OAAO,SAAS,eAAe,GAAG,IAAI,OAAM,EAAG,CAAC;AAClD"}