@thunderid/nextjs 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ThunderIDNextClient.js +1 -1
- package/dist/ThunderIDNextClient.js.map +1 -1
- package/dist/cjs/ThunderIDNextClient.cjs +1 -1
- package/dist/cjs/ThunderIDNextClient.cjs.map +1 -1
- package/dist/cjs/client/contexts/ThunderID/ThunderIDProvider.cjs +1 -2
- package/dist/cjs/client/contexts/ThunderID/ThunderIDProvider.cjs.map +1 -1
- package/dist/cjs/index.cjs +2 -0
- package/dist/cjs/server/actions/handleOAuthCallbackAction.cjs +2 -4
- package/dist/cjs/server/actions/handleOAuthCallbackAction.cjs.map +1 -1
- package/dist/cjs/server/actions/signInAction.cjs +1 -1
- package/dist/cjs/server/actions/signInAction.cjs.map +1 -1
- package/dist/cjs/server/backchannelLogout.cjs +92 -0
- package/dist/cjs/server/backchannelLogout.cjs.map +1 -0
- package/dist/cjs/server/index.cjs +2 -0
- package/dist/cjs/server/proxy/thunderIDProxy.cjs +4 -1
- package/dist/cjs/server/proxy/thunderIDProxy.cjs.map +1 -1
- package/dist/cjs/utils/SessionManager.cjs +17 -1
- package/dist/cjs/utils/SessionManager.cjs.map +1 -1
- package/dist/cjs/utils/handleRefreshToken.cjs +5 -2
- package/dist/cjs/utils/handleRefreshToken.cjs.map +1 -1
- package/dist/cjs/utils/loggedOutSessions.cjs +32 -0
- package/dist/cjs/utils/loggedOutSessions.cjs.map +1 -0
- package/dist/client/contexts/ThunderID/ThunderIDProvider.d.ts +1 -1
- package/dist/client/contexts/ThunderID/ThunderIDProvider.d.ts.map +1 -1
- package/dist/client/contexts/ThunderID/ThunderIDProvider.js +1 -2
- package/dist/client/contexts/ThunderID/ThunderIDProvider.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/server/actions/handleOAuthCallbackAction.d.ts +1 -2
- package/dist/server/actions/handleOAuthCallbackAction.d.ts.map +1 -1
- package/dist/server/actions/handleOAuthCallbackAction.js +2 -4
- package/dist/server/actions/handleOAuthCallbackAction.js.map +1 -1
- package/dist/server/actions/signInAction.d.ts.map +1 -1
- package/dist/server/actions/signInAction.js +1 -1
- package/dist/server/actions/signInAction.js.map +1 -1
- package/dist/server/backchannelLogout.d.ts +40 -0
- package/dist/server/backchannelLogout.d.ts.map +1 -0
- package/dist/server/backchannelLogout.js +90 -0
- package/dist/server/backchannelLogout.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -1
- package/dist/server/proxy/thunderIDProxy.d.ts +12 -1
- package/dist/server/proxy/thunderIDProxy.d.ts.map +1 -1
- package/dist/server/proxy/thunderIDProxy.js +4 -1
- package/dist/server/proxy/thunderIDProxy.js.map +1 -1
- package/dist/utils/SessionManager.d.ts +13 -1
- package/dist/utils/SessionManager.d.ts.map +1 -1
- package/dist/utils/SessionManager.js +17 -1
- package/dist/utils/SessionManager.js.map +1 -1
- package/dist/utils/handleRefreshToken.d.ts.map +1 -1
- package/dist/utils/handleRefreshToken.js +5 -2
- package/dist/utils/handleRefreshToken.js.map +1 -1
- package/dist/utils/loggedOutSessions.d.ts +12 -0
- package/dist/utils/loggedOutSessions.d.ts.map +1 -0
- package/dist/utils/loggedOutSessions.js +29 -0
- package/dist/utils/loggedOutSessions.js.map +1 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thunderIDProxy.cjs","names":["eqIdx: number","entries: {name: string; part: string}[]","CookieChunking","remaining: string[]","newParts: string[]","resolvedConfig: ThunderIDNextConfig","decorateConfigWithNextEnv","url: URL","hasCallbackParams: boolean","tempSessionToken: string | undefined","SessionManager","verifiedSession: SessionTokenPayload | undefined","getSessionFromRequest","expiredSession: SessionTokenPayload | undefined","rawToken: string | undefined","getChunkedCookie","decoded: SessionTokenPayload","now: number","candidateSession: SessionTokenPayload | undefined","needsRefresh: boolean","REFRESH_BUFFER_SECONDS","activeSession: SessionTokenPayload | undefined","refreshCookieUpdate: {expiry: number; token: string} | undefined","handleRefreshToken","rawSessionCookie: string | undefined","sessionId: string | undefined","getSessionIdFromRequest","handlerResponse: NextResponse | void","referer: string | null","refererUrl: URL","requestUrl: URL","redirectUrl: string","NextResponse","cookieName: string","cookieName","requestHeaders: Headers","cleanResponse: NextResponse","requestHeaders","cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions>","updatedCookieHeader: string","response: NextResponse"],"sources":["../../../../src/server/proxy/thunderIDProxy.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {CookieChunking} from '@thunderid/node';\nimport {NextRequest, NextResponse} from 'next/server';\nimport {REFRESH_BUFFER_SECONDS} from '../../constants/sessionConstants';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {\n ChunkedCookieOptions,\n ChunkedCookieWriter,\n deleteChunkedCookie,\n getChunkedCookie,\n setChunkedCookie,\n} from '../../utils/chunkedCookie';\nimport decorateConfigWithNextEnv from '../../utils/decorateConfigWithNextEnv';\nimport handleRefreshToken from '../../utils/handleRefreshToken';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport {getSessionFromRequest, getSessionIdFromRequest} from '../../utils/sessionUtils';\n\nexport type ThunderIDProxyOptions = Partial<ThunderIDNextConfig>;\n\nexport interface ThunderIDProxyContext {\n /** Get the session payload from JWT session if available */\n getSession: () => Promise<SessionTokenPayload | undefined>;\n /** Get the session ID from the current request */\n getSessionId: () => string | undefined;\n /** Check if the current request has a valid ThunderID session */\n isSignedIn: () => boolean;\n /**\n * Protect a route by redirecting unauthenticated users.\n * Redirect URL fallback order:\n * 1. options.redirect\n * 2. resolvedOptions.signInUrl\n * 3. resolvedOptions.defaultRedirect\n * 4. referer (if from same origin)\n * If none are available, falls back to '/'.\n */\n protectRoute: (routeOptions?: {redirect?: string}) => Promise<NextResponse | void>;\n}\n\ntype ThunderIDProxyHandler = (\n thunderid: ThunderIDProxyContext,\n req: NextRequest,\n) => Promise<NextResponse | void> | NextResponse | void;\n\n/**\n * Splits a raw Cookie header string into its `{name, part}` entries, where\n * `part` is the original `name=value` text (trimmed, never empty).\n */\nconst parseCookieHeaderParts = (cookieHeader: string): {name: string; part: string}[] =>\n cookieHeader\n .split(';')\n .map((p: string) => p.trim())\n .filter(Boolean)\n .map((part: string) => {\n const eqIdx: number = part.indexOf('=');\n const name: string = eqIdx === -1 ? part : part.slice(0, eqIdx).trim();\n return {name, part};\n });\n\n/**\n * Removes a cookie from a raw Cookie header string — the unchunked `name`\n * entry and/or any numbered `${name}.0`, `${name}.1`, ... chunk entries.\n */\nexport const removeChunkedCookieFromHeader = (cookieHeader: string, name: string): string => {\n const entries: {name: string; part: string}[] = parseCookieHeaderParts(cookieHeader);\n const toRemove = new Set<string>(\n CookieChunking.filterChunkNames(\n name,\n entries.map((e) => e.name),\n ),\n );\n\n return entries\n .filter((e) => !toRemove.has(e.name))\n .map((e) => e.part)\n .join('; ');\n};\n\n/**\n * Replaces a cookie's value inside a raw Cookie header string, splitting it\n * across numbered `${name}.0`, `${name}.1`, ... chunk entries once it would\n * exceed the ~4KB per-cookie limit browsers enforce. Any entry the cookie\n * doesn't appear in yet is appended; stale chunk entries a shrunk value no\n * longer needs are dropped.\n */\nexport const replaceChunkedCookieInHeader = (cookieHeader: string, name: string, value: string): string => {\n const remaining: string[] = parseCookieHeaderParts(removeChunkedCookieFromHeader(cookieHeader, name)).map(\n (e) => e.part,\n );\n const newParts: string[] = Object.entries(CookieChunking.split(name, value)).map(\n ([chunkName, chunkValue]: [string, string]) => `${chunkName}=${chunkValue}`,\n );\n\n return [...remaining, ...newParts].join('; ');\n};\n\n/**\n * Adapts a request/response pair into a {@link ChunkedCookieWriter}: existing\n * chunk names are discovered from the incoming request's cookies (what the\n * browser currently holds), while writes/deletes apply to the response.\n */\nconst toChunkedCookieWriter = (\n request: NextRequest,\n responseCookies: NextResponse['cookies'],\n): ChunkedCookieWriter => ({\n delete: (name: string): void => {\n responseCookies.delete(name);\n },\n getAll: (): {name: string}[] => request.cookies.getAll(),\n set: (name: string, value: string, options: ChunkedCookieOptions): void => {\n responseCookies.set(name, value, options);\n },\n});\n\n/**\n * ThunderID proxy that integrates authentication into your Next.js application.\n * Similar to Clerk's clerkMiddleware pattern.\n *\n * Proactively refreshes the access token when it is within REFRESH_BUFFER_SECONDS of\n * expiry so that Server Components always receive a fresh session. The refresh also\n * recovers expired tokens as long as a refresh token is present.\n *\n * The updated session cookie is written to:\n * - The response → browser stores the new cookie for subsequent requests.\n * - The forwarded request headers → the same-request Server Component render sees\n * the fresh token immediately without waiting for the next navigation.\n *\n * Token refresh requires baseUrl, clientId, and clientSecret. These are resolved from\n * the options argument first, then from the standard ThunderID environment variables\n * (NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID,\n * THUNDERID_CLIENT_SECRET). If none are available the refresh step is skipped silently.\n *\n * @param handler - Optional handler function to customize proxy behavior\n * @param options - Configuration options for the proxy\n * @returns Next.js middleware function\n *\n * @example\n * ```typescript\n * // middleware.ts - Basic usage (config read from env vars automatically)\n * import { thunderIDProxy } from '@thunderid/nextjs/server';\n *\n * export default thunderIDProxy();\n *\n * export const config = {\n * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],\n * };\n * ```\n *\n * @example\n * ```typescript\n * // With route protection\n * import { thunderIDProxy, createRouteMatcher } from '@thunderid/nextjs/server';\n *\n * const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);\n *\n * export default thunderIDProxy(async (thunderid, req) => {\n * if (isProtectedRoute(req)) {\n * await thunderid.protectRoute();\n * }\n * });\n * ```\n */\nconst thunderIDProxy =\n (\n handler?: ThunderIDProxyHandler,\n options?: ThunderIDProxyOptions | ((req: NextRequest) => ThunderIDProxyOptions),\n ): ((request: NextRequest) => Promise<NextResponse>) =>\n async (request: NextRequest): Promise<NextResponse> => {\n const resolvedOptions: ThunderIDProxyOptions = typeof options === 'function' ? options(request) : options || {};\n\n // Resolve full config from passed options + environment variable fallbacks.\n const resolvedConfig: ThunderIDNextConfig = decorateConfigWithNextEnv(resolvedOptions as ThunderIDNextConfig);\n\n // ── OAuth callback detection ──────────────────────────────────────────────\n const url: URL = new URL(request.url);\n const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state');\n\n let isValidOAuthCallback = false;\n if (hasCallbackParams && !url.searchParams.has('error')) {\n const tempSessionToken: string | undefined = request.cookies.get(\n SessionManager.getTempSessionCookieName(),\n )?.value;\n if (tempSessionToken) {\n try {\n await SessionManager.verifyTempSession(tempSessionToken);\n isValidOAuthCallback = true;\n } catch {\n isValidOAuthCallback = false;\n }\n }\n }\n\n // ── Session resolution ────────────────────────────────────────────────────\n // Step 1: Attempt to get a fully verified (signature + expiry) session.\n const verifiedSession: SessionTokenPayload | undefined = await getSessionFromRequest(request);\n\n // Step 2: If no verified session exists, verify the raw cookie's signature\n // without enforcing expiry. This allows the proxy to recover from an\n // expired access token as long as the JWT is authentic and a refresh token\n // is present. Skipping the signature check here would let a tampered cookie\n // drive identity-confusion attacks since handleRefreshToken reuses `sub`,\n // `sessionId`, and `organizationId` from the input payload when minting the\n // new session JWT.\n let expiredSession: SessionTokenPayload | undefined;\n if (!verifiedSession) {\n const rawToken: string | undefined = getChunkedCookie(request.cookies, SessionManager.getSessionCookieName());\n if (rawToken) {\n try {\n const decoded: SessionTokenPayload = await SessionManager.verifySessionTokenForRefresh(rawToken);\n if (decoded.refreshToken) {\n expiredSession = decoded;\n }\n } catch {\n // Forged, tampered, wrong type, or malformed — ignore.\n }\n }\n }\n\n // ── Token refresh ─────────────────────────────────────────────────────────\n const now: number = Math.floor(Date.now() / 1000);\n const candidateSession: SessionTokenPayload | undefined = verifiedSession ?? expiredSession;\n\n // Config is required to call the token endpoint.\n const hasRefreshConfig = !!(resolvedConfig.baseUrl && resolvedConfig.clientId && resolvedConfig.clientSecret);\n\n // Refresh when:\n // a) Token is verified but within the proactive buffer window, OR\n // b) Token has already expired but a refresh token is available.\n const needsRefresh: boolean =\n !isValidOAuthCallback &&\n hasRefreshConfig &&\n !!candidateSession?.refreshToken &&\n ((!!verifiedSession && verifiedSession.exp <= now + REFRESH_BUFFER_SECONDS) || !!expiredSession);\n\n let activeSession: SessionTokenPayload | undefined = verifiedSession;\n let refreshCookieUpdate: {expiry: number; token: string} | undefined;\n\n if (needsRefresh && candidateSession) {\n try {\n const {newSessionToken, sessionCookieExpiryTime} = await handleRefreshToken(candidateSession, {\n baseUrl: resolvedConfig.baseUrl!,\n clientId: resolvedConfig.clientId!,\n clientSecret: resolvedConfig.clientSecret!,\n sessionCookie: resolvedConfig.sessionCookie,\n });\n // Verify the newly minted token so activeSession reflects fresh claims.\n activeSession = await SessionManager.verifySessionToken(newSessionToken);\n refreshCookieUpdate = {expiry: sessionCookieExpiryTime, token: newSessionToken};\n } catch {\n // Refresh failed — clear the irrecoverable session.\n activeSession = undefined;\n }\n }\n\n // ── Session cleanup detection ─────────────────────────────────────────────\n // Mark stale cookies for deletion when the session is irrecoverable. Skipped\n // during OAuth callbacks where a session cookie may not exist yet.\n const rawSessionCookie: string | undefined = getChunkedCookie(\n request.cookies,\n SessionManager.getSessionCookieName(),\n );\n\n let shouldClearCookie = false;\n\n if (!isValidOAuthCallback && rawSessionCookie && !activeSession && !refreshCookieUpdate) {\n // A cookie was present but all resolution paths (verify, decode, refresh)\n // failed — the session is dead and cannot be recovered.\n shouldClearCookie = true;\n }\n\n const sessionId: string | undefined = activeSession?.sessionId ?? (await getSessionIdFromRequest(request));\n const isAuthenticated = !!activeSession;\n\n // ── Proxy context ─────────────────────────────────────────────────────────\n const thunderid: ThunderIDProxyContext = {\n getSession: async (): Promise<SessionTokenPayload | undefined> => activeSession,\n getSessionId: (): string | undefined => sessionId,\n isSignedIn: (): boolean => isAuthenticated,\n protectRoute: async (routeOptions?: {redirect?: string}): Promise<NextResponse | void> => {\n // Skip during a valid OAuth callback to avoid redirecting before the\n // callback action has had a chance to complete.\n if (isValidOAuthCallback) {\n return undefined;\n }\n\n if (!isAuthenticated) {\n const referer: string | null = request.headers.get('referer');\n let fallbackRedirect = '/';\n\n if (referer) {\n try {\n const refererUrl: URL = new URL(referer);\n const requestUrl: URL = new URL(request.url);\n if (refererUrl.origin === requestUrl.origin) {\n fallbackRedirect = refererUrl.pathname + refererUrl.search;\n }\n } catch {\n // Invalid referer — ignore.\n }\n }\n\n const redirectUrl: string = routeOptions?.redirect ?? resolvedConfig.signInUrl! ?? fallbackRedirect;\n\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n\n return undefined;\n },\n };\n\n // ── Handler ───────────────────────────────────────────────────────────────\n const handlerResponse: NextResponse | void = handler ? await handler(thunderid, request) : undefined;\n\n // ── Build final response ──────────────────────────────────────────────────\n if (shouldClearCookie) {\n const cookieName: string = SessionManager.getSessionCookieName();\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the deletion so the browser discards the stale cookie (and\n // every chunk of it).\n deleteChunkedCookie(toChunkedCookieWriter(request, handlerResponse.cookies), cookieName);\n return handlerResponse;\n }\n\n // Pass-through: strip the dead cookie from the forwarded request headers\n // so the same-request Server Component render sees no session at all.\n const requestHeaders: Headers = new Headers(request.headers);\n requestHeaders.set('cookie', removeChunkedCookieFromHeader(request.headers.get('cookie') ?? '', cookieName));\n const cleanResponse: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n deleteChunkedCookie(toChunkedCookieWriter(request, cleanResponse.cookies), cookieName);\n return cleanResponse;\n }\n\n if (!refreshCookieUpdate) {\n return handlerResponse ?? NextResponse.next();\n }\n\n // A token refresh occurred — the new session cookie must be applied to:\n // 1. The HTTP response so the browser stores the updated cookie.\n // 2. The forwarded request headers so the same-request Server Component\n // render reads the fresh session token instead of the expired one.\n const cookieName: string = SessionManager.getSessionCookieName();\n const cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions> =\n SessionManager.getSessionCookieOptions(refreshCookieUpdate.expiry);\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the refresh cookie so the browser receives it even on redirects.\n setChunkedCookie(\n toChunkedCookieWriter(request, handlerResponse.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return handlerResponse;\n }\n\n // Default pass-through: update both the response cookie and the request\n // Cookie header so the downstream Server Component render is not stale.\n const requestHeaders: Headers = new Headers(request.headers);\n const updatedCookieHeader: string = replaceChunkedCookieInHeader(\n request.headers.get('cookie') ?? '',\n cookieName,\n refreshCookieUpdate.token,\n );\n requestHeaders.set('cookie', updatedCookieHeader);\n\n const response: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n setChunkedCookie(\n toChunkedCookieWriter(request, response.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return response;\n };\n\nexport default thunderIDProxy;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"thunderIDProxy.cjs","names":["eqIdx: number","entries: {name: string; part: string}[]","CookieChunking","remaining: string[]","newParts: string[]","resolvedOptions: ThunderIDProxyOptions","resolvedConfig: ThunderIDNextConfig","decorateConfigWithNextEnv","url: URL","hasCallbackParams: boolean","tempSessionToken: string | undefined","SessionManager","verifiedSession: SessionTokenPayload | undefined","getSessionFromRequest","expiredSession: SessionTokenPayload | undefined","rawToken: string | undefined","getChunkedCookie","decoded: SessionTokenPayload","now: number","candidateSession: SessionTokenPayload | undefined","needsRefresh: boolean","REFRESH_BUFFER_SECONDS","activeSession: SessionTokenPayload | undefined","refreshCookieUpdate: {expiry: number; token: string} | undefined","handleRefreshToken","rawSessionCookie: string | undefined","sessionId: string | undefined","getSessionIdFromRequest","handlerResponse: NextResponse | void","referer: string | null","refererUrl: URL","requestUrl: URL","redirectUrl: string","NextResponse","cookieName: string","cookieName","requestHeaders: Headers","cleanResponse: NextResponse","requestHeaders","cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions>","updatedCookieHeader: string","response: NextResponse"],"sources":["../../../../src/server/proxy/thunderIDProxy.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {CookieChunking, LogoutRecordStore} from '@thunderid/node';\nimport {NextRequest, NextResponse} from 'next/server';\nimport {REFRESH_BUFFER_SECONDS} from '../../constants/sessionConstants';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {\n ChunkedCookieOptions,\n ChunkedCookieWriter,\n deleteChunkedCookie,\n getChunkedCookie,\n setChunkedCookie,\n} from '../../utils/chunkedCookie';\nimport decorateConfigWithNextEnv from '../../utils/decorateConfigWithNextEnv';\nimport handleRefreshToken from '../../utils/handleRefreshToken';\nimport {setLogoutRecordStore} from '../../utils/loggedOutSessions';\nimport SessionManager, {SessionTokenPayload} from '../../utils/SessionManager';\nimport {getSessionFromRequest, getSessionIdFromRequest} from '../../utils/sessionUtils';\n\nexport type ThunderIDProxyOptions = Partial<ThunderIDNextConfig> & {\n /**\n * Where sessions ended by back-channel logout are recorded. Give the same store to\n * `backchannelLogout()`. Needed when more than one instance of the application runs, or when\n * this proxy runs on the Edge runtime, which shares no memory with the route handler. Defaults\n * to the memory of this process.\n */\n backchannelLogout?: {store?: LogoutRecordStore};\n};\n\nexport interface ThunderIDProxyContext {\n /** Get the session payload from JWT session if available */\n getSession: () => Promise<SessionTokenPayload | undefined>;\n /** Get the session ID from the current request */\n getSessionId: () => string | undefined;\n /** Check if the current request has a valid ThunderID session */\n isSignedIn: () => boolean;\n /**\n * Protect a route by redirecting unauthenticated users.\n * Redirect URL fallback order:\n * 1. options.redirect\n * 2. resolvedOptions.signInUrl\n * 3. resolvedOptions.defaultRedirect\n * 4. referer (if from same origin)\n * If none are available, falls back to '/'.\n */\n protectRoute: (routeOptions?: {redirect?: string}) => Promise<NextResponse | void>;\n}\n\ntype ThunderIDProxyHandler = (\n thunderid: ThunderIDProxyContext,\n req: NextRequest,\n) => Promise<NextResponse | void> | NextResponse | void;\n\n/**\n * Splits a raw Cookie header string into its `{name, part}` entries, where\n * `part` is the original `name=value` text (trimmed, never empty).\n */\nconst parseCookieHeaderParts = (cookieHeader: string): {name: string; part: string}[] =>\n cookieHeader\n .split(';')\n .map((p: string) => p.trim())\n .filter(Boolean)\n .map((part: string) => {\n const eqIdx: number = part.indexOf('=');\n const name: string = eqIdx === -1 ? part : part.slice(0, eqIdx).trim();\n return {name, part};\n });\n\n/**\n * Removes a cookie from a raw Cookie header string — the unchunked `name`\n * entry and/or any numbered `${name}.0`, `${name}.1`, ... chunk entries.\n */\nexport const removeChunkedCookieFromHeader = (cookieHeader: string, name: string): string => {\n const entries: {name: string; part: string}[] = parseCookieHeaderParts(cookieHeader);\n const toRemove = new Set<string>(\n CookieChunking.filterChunkNames(\n name,\n entries.map((e) => e.name),\n ),\n );\n\n return entries\n .filter((e) => !toRemove.has(e.name))\n .map((e) => e.part)\n .join('; ');\n};\n\n/**\n * Replaces a cookie's value inside a raw Cookie header string, splitting it\n * across numbered `${name}.0`, `${name}.1`, ... chunk entries once it would\n * exceed the ~4KB per-cookie limit browsers enforce. Any entry the cookie\n * doesn't appear in yet is appended; stale chunk entries a shrunk value no\n * longer needs are dropped.\n */\nexport const replaceChunkedCookieInHeader = (cookieHeader: string, name: string, value: string): string => {\n const remaining: string[] = parseCookieHeaderParts(removeChunkedCookieFromHeader(cookieHeader, name)).map(\n (e) => e.part,\n );\n const newParts: string[] = Object.entries(CookieChunking.split(name, value)).map(\n ([chunkName, chunkValue]: [string, string]) => `${chunkName}=${chunkValue}`,\n );\n\n return [...remaining, ...newParts].join('; ');\n};\n\n/**\n * Adapts a request/response pair into a {@link ChunkedCookieWriter}: existing\n * chunk names are discovered from the incoming request's cookies (what the\n * browser currently holds), while writes/deletes apply to the response.\n */\nconst toChunkedCookieWriter = (\n request: NextRequest,\n responseCookies: NextResponse['cookies'],\n): ChunkedCookieWriter => ({\n delete: (name: string): void => {\n responseCookies.delete(name);\n },\n getAll: (): {name: string}[] => request.cookies.getAll(),\n set: (name: string, value: string, options: ChunkedCookieOptions): void => {\n responseCookies.set(name, value, options);\n },\n});\n\n/**\n * ThunderID proxy that integrates authentication into your Next.js application.\n * Similar to Clerk's clerkMiddleware pattern.\n *\n * Proactively refreshes the access token when it is within REFRESH_BUFFER_SECONDS of\n * expiry so that Server Components always receive a fresh session. The refresh also\n * recovers expired tokens as long as a refresh token is present.\n *\n * The updated session cookie is written to:\n * - The response → browser stores the new cookie for subsequent requests.\n * - The forwarded request headers → the same-request Server Component render sees\n * the fresh token immediately without waiting for the next navigation.\n *\n * Token refresh requires baseUrl, clientId, and clientSecret. These are resolved from\n * the options argument first, then from the standard ThunderID environment variables\n * (NEXT_PUBLIC_THUNDERID_BASE_URL, NEXT_PUBLIC_THUNDERID_CLIENT_ID,\n * THUNDERID_CLIENT_SECRET). If none are available the refresh step is skipped silently.\n *\n * @param handler - Optional handler function to customize proxy behavior\n * @param options - Configuration options for the proxy\n * @returns Next.js middleware function\n *\n * @example\n * ```typescript\n * // middleware.ts - Basic usage (config read from env vars automatically)\n * import { thunderIDProxy } from '@thunderid/nextjs/server';\n *\n * export default thunderIDProxy();\n *\n * export const config = {\n * matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],\n * };\n * ```\n *\n * @example\n * ```typescript\n * // With route protection\n * import { thunderIDProxy, createRouteMatcher } from '@thunderid/nextjs/server';\n *\n * const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);\n *\n * export default thunderIDProxy(async (thunderid, req) => {\n * if (isProtectedRoute(req)) {\n * await thunderid.protectRoute();\n * }\n * });\n * ```\n */\nconst thunderIDProxy =\n (\n handler?: ThunderIDProxyHandler,\n options?: ThunderIDProxyOptions | ((req: NextRequest) => ThunderIDProxyOptions),\n ): ((request: NextRequest) => Promise<NextResponse>) =>\n async (request: NextRequest): Promise<NextResponse> => {\n const resolvedOptions: ThunderIDProxyOptions = typeof options === 'function' ? options(request) : options || {};\n\n setLogoutRecordStore(resolvedOptions.backchannelLogout?.store);\n\n // Resolve full config from passed options + environment variable fallbacks.\n const resolvedConfig: ThunderIDNextConfig = decorateConfigWithNextEnv(resolvedOptions as ThunderIDNextConfig);\n\n // ── OAuth callback detection ──────────────────────────────────────────────\n const url: URL = new URL(request.url);\n const hasCallbackParams: boolean = url.searchParams.has('code') && url.searchParams.has('state');\n\n let isValidOAuthCallback = false;\n if (hasCallbackParams && !url.searchParams.has('error')) {\n const tempSessionToken: string | undefined = request.cookies.get(\n SessionManager.getTempSessionCookieName(),\n )?.value;\n if (tempSessionToken) {\n try {\n await SessionManager.verifyTempSession(tempSessionToken);\n isValidOAuthCallback = true;\n } catch {\n isValidOAuthCallback = false;\n }\n }\n }\n\n // ── Session resolution ────────────────────────────────────────────────────\n // Step 1: Attempt to get a fully verified (signature + expiry) session.\n const verifiedSession: SessionTokenPayload | undefined = await getSessionFromRequest(request);\n\n // Step 2: If no verified session exists, verify the raw cookie's signature\n // without enforcing expiry. This allows the proxy to recover from an\n // expired access token as long as the JWT is authentic and a refresh token\n // is present. Skipping the signature check here would let a tampered cookie\n // drive identity-confusion attacks since handleRefreshToken reuses `sub`,\n // `sessionId`, and `organizationId` from the input payload when minting the\n // new session JWT.\n let expiredSession: SessionTokenPayload | undefined;\n if (!verifiedSession) {\n const rawToken: string | undefined = getChunkedCookie(request.cookies, SessionManager.getSessionCookieName());\n if (rawToken) {\n try {\n const decoded: SessionTokenPayload = await SessionManager.verifySessionTokenForRefresh(rawToken);\n if (decoded.refreshToken) {\n expiredSession = decoded;\n }\n } catch {\n // Forged, tampered, wrong type, or malformed — ignore.\n }\n }\n }\n\n // ── Token refresh ─────────────────────────────────────────────────────────\n const now: number = Math.floor(Date.now() / 1000);\n const candidateSession: SessionTokenPayload | undefined = verifiedSession ?? expiredSession;\n\n // Config is required to call the token endpoint.\n const hasRefreshConfig = !!(resolvedConfig.baseUrl && resolvedConfig.clientId && resolvedConfig.clientSecret);\n\n // Refresh when:\n // a) Token is verified but within the proactive buffer window, OR\n // b) Token has already expired but a refresh token is available.\n const needsRefresh: boolean =\n !isValidOAuthCallback &&\n hasRefreshConfig &&\n !!candidateSession?.refreshToken &&\n ((!!verifiedSession && verifiedSession.exp <= now + REFRESH_BUFFER_SECONDS) || !!expiredSession);\n\n let activeSession: SessionTokenPayload | undefined = verifiedSession;\n let refreshCookieUpdate: {expiry: number; token: string} | undefined;\n\n if (needsRefresh && candidateSession) {\n try {\n const {newSessionToken, sessionCookieExpiryTime} = await handleRefreshToken(candidateSession, {\n baseUrl: resolvedConfig.baseUrl!,\n clientId: resolvedConfig.clientId!,\n clientSecret: resolvedConfig.clientSecret!,\n sessionCookie: resolvedConfig.sessionCookie,\n });\n // Verify the newly minted token so activeSession reflects fresh claims.\n activeSession = await SessionManager.verifySessionToken(newSessionToken);\n refreshCookieUpdate = {expiry: sessionCookieExpiryTime, token: newSessionToken};\n } catch {\n // Refresh failed — clear the irrecoverable session.\n activeSession = undefined;\n }\n }\n\n // ── Session cleanup detection ─────────────────────────────────────────────\n // Mark stale cookies for deletion when the session is irrecoverable. Skipped\n // during OAuth callbacks where a session cookie may not exist yet.\n const rawSessionCookie: string | undefined = getChunkedCookie(\n request.cookies,\n SessionManager.getSessionCookieName(),\n );\n\n let shouldClearCookie = false;\n\n if (!isValidOAuthCallback && rawSessionCookie && !activeSession && !refreshCookieUpdate) {\n // A cookie was present but all resolution paths (verify, decode, refresh)\n // failed — the session is dead and cannot be recovered.\n shouldClearCookie = true;\n }\n\n const sessionId: string | undefined = activeSession?.sessionId ?? (await getSessionIdFromRequest(request));\n const isAuthenticated = !!activeSession;\n\n // ── Proxy context ─────────────────────────────────────────────────────────\n const thunderid: ThunderIDProxyContext = {\n getSession: async (): Promise<SessionTokenPayload | undefined> => activeSession,\n getSessionId: (): string | undefined => sessionId,\n isSignedIn: (): boolean => isAuthenticated,\n protectRoute: async (routeOptions?: {redirect?: string}): Promise<NextResponse | void> => {\n // Skip during a valid OAuth callback to avoid redirecting before the\n // callback action has had a chance to complete.\n if (isValidOAuthCallback) {\n return undefined;\n }\n\n if (!isAuthenticated) {\n const referer: string | null = request.headers.get('referer');\n let fallbackRedirect = '/';\n\n if (referer) {\n try {\n const refererUrl: URL = new URL(referer);\n const requestUrl: URL = new URL(request.url);\n if (refererUrl.origin === requestUrl.origin) {\n fallbackRedirect = refererUrl.pathname + refererUrl.search;\n }\n } catch {\n // Invalid referer — ignore.\n }\n }\n\n const redirectUrl: string = routeOptions?.redirect ?? resolvedConfig.signInUrl! ?? fallbackRedirect;\n\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n\n return undefined;\n },\n };\n\n // ── Handler ───────────────────────────────────────────────────────────────\n const handlerResponse: NextResponse | void = handler ? await handler(thunderid, request) : undefined;\n\n // ── Build final response ──────────────────────────────────────────────────\n if (shouldClearCookie) {\n const cookieName: string = SessionManager.getSessionCookieName();\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the deletion so the browser discards the stale cookie (and\n // every chunk of it).\n deleteChunkedCookie(toChunkedCookieWriter(request, handlerResponse.cookies), cookieName);\n return handlerResponse;\n }\n\n // Pass-through: strip the dead cookie from the forwarded request headers\n // so the same-request Server Component render sees no session at all.\n const requestHeaders: Headers = new Headers(request.headers);\n requestHeaders.set('cookie', removeChunkedCookieFromHeader(request.headers.get('cookie') ?? '', cookieName));\n const cleanResponse: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n deleteChunkedCookie(toChunkedCookieWriter(request, cleanResponse.cookies), cookieName);\n return cleanResponse;\n }\n\n if (!refreshCookieUpdate) {\n return handlerResponse ?? NextResponse.next();\n }\n\n // A token refresh occurred — the new session cookie must be applied to:\n // 1. The HTTP response so the browser stores the updated cookie.\n // 2. The forwarded request headers so the same-request Server Component\n // render reads the fresh session token instead of the expired one.\n const cookieName: string = SessionManager.getSessionCookieName();\n const cookieOptions: ReturnType<typeof SessionManager.getSessionCookieOptions> =\n SessionManager.getSessionCookieOptions(refreshCookieUpdate.expiry);\n\n if (handlerResponse) {\n // Handler returned a response (e.g. a redirect from protectRoute).\n // Attach the refresh cookie so the browser receives it even on redirects.\n setChunkedCookie(\n toChunkedCookieWriter(request, handlerResponse.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return handlerResponse;\n }\n\n // Default pass-through: update both the response cookie and the request\n // Cookie header so the downstream Server Component render is not stale.\n const requestHeaders: Headers = new Headers(request.headers);\n const updatedCookieHeader: string = replaceChunkedCookieInHeader(\n request.headers.get('cookie') ?? '',\n cookieName,\n refreshCookieUpdate.token,\n );\n requestHeaders.set('cookie', updatedCookieHeader);\n\n const response: NextResponse = NextResponse.next({request: {headers: requestHeaders}});\n setChunkedCookie(\n toChunkedCookieWriter(request, response.cookies),\n cookieName,\n refreshCookieUpdate.token,\n cookieOptions,\n );\n return response;\n };\n\nexport default thunderIDProxy;\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_loggedOutSessions = require('./loggedOutSessions.cjs');
|
|
2
3
|
const require_sessionConstants = require('../constants/sessionConstants.cjs');
|
|
3
4
|
let __thunderid_node = require("@thunderid/node");
|
|
4
5
|
__thunderid_node = require_rolldown_runtime.__toESM(__thunderid_node);
|
|
@@ -50,7 +51,7 @@ var SessionManager = class {
|
|
|
50
51
|
}
|
|
51
52
|
return require_sessionConstants.DEFAULT_SESSION_COOKIE_EXPIRY_TIME;
|
|
52
53
|
}
|
|
53
|
-
static async createSessionToken(accessToken, userId, sessionId, scopes, accessTokenTtlSeconds, refreshToken, organizationId) {
|
|
54
|
+
static async createSessionToken(accessToken, userId, sessionId, scopes, accessTokenTtlSeconds, refreshToken, organizationId, serverSession) {
|
|
54
55
|
const secret = this.getSecret();
|
|
55
56
|
return await new jose.SignJWT({
|
|
56
57
|
accessToken,
|
|
@@ -58,6 +59,8 @@ var SessionManager = class {
|
|
|
58
59
|
refreshToken,
|
|
59
60
|
scopes,
|
|
60
61
|
sessionId,
|
|
62
|
+
sid: serverSession?.sid,
|
|
63
|
+
signedInAt: serverSession?.signedInAt ?? Math.floor(Date.now() / 1e3),
|
|
61
64
|
type: "session"
|
|
62
65
|
}).setProtectedHeader({ alg: "HS256" }).setSubject(userId).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + accessTokenTtlSeconds).sign(secret);
|
|
63
66
|
}
|
|
@@ -68,6 +71,7 @@ var SessionManager = class {
|
|
|
68
71
|
try {
|
|
69
72
|
const { payload } = await (0, jose.jwtVerify)(token, this.getSecret());
|
|
70
73
|
if (payload["type"] !== "session") throw new Error("Invalid token type");
|
|
74
|
+
await this.refuseLoggedOutSession(payload);
|
|
71
75
|
return payload;
|
|
72
76
|
} catch (error) {
|
|
73
77
|
throw new __thunderid_node.ThunderIDRuntimeError(`Invalid session token: ${error instanceof Error ? error.message : "Unknown error"}`, "invalid-session-token", "nextjs", "Session token verification failed");
|
|
@@ -89,6 +93,7 @@ var SessionManager = class {
|
|
|
89
93
|
const { payload: rawPayload } = await (0, jose.compactVerify)(token, this.getSecret());
|
|
90
94
|
const payload = JSON.parse(new TextDecoder().decode(rawPayload));
|
|
91
95
|
if (payload.type !== "session") throw new Error("Invalid token type");
|
|
96
|
+
await this.refuseLoggedOutSession(payload);
|
|
92
97
|
return payload;
|
|
93
98
|
} catch (error) {
|
|
94
99
|
throw new __thunderid_node.ThunderIDRuntimeError(`Invalid session token: ${error instanceof Error ? error.message : "Unknown error"}`, "invalid-session-token-for-refresh", "nextjs", "Session token signature or type check failed during refresh");
|
|
@@ -109,6 +114,17 @@ var SessionManager = class {
|
|
|
109
114
|
/**
|
|
110
115
|
* Get session cookie options
|
|
111
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* Throws when a back-channel logout has ended the session this cookie carries. The cookie is
|
|
119
|
+
* still correctly signed, so this is the only thing that stops it from being used.
|
|
120
|
+
*/
|
|
121
|
+
static async refuseLoggedOutSession(payload) {
|
|
122
|
+
if (await require_loggedOutSessions.getLoggedOutSessions().isLoggedOut({
|
|
123
|
+
sid: payload.sid,
|
|
124
|
+
startedAt: payload.signedInAt ?? payload.iat,
|
|
125
|
+
sub: payload.sub
|
|
126
|
+
})) throw new Error("The session was ended by a back-channel logout");
|
|
127
|
+
}
|
|
112
128
|
static getSessionCookieOptions(maxAge) {
|
|
113
129
|
return {
|
|
114
130
|
httpOnly: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionManager.cjs","names":["secret: string | undefined","ThunderIDRuntimeError","secret: Uint8Array","SignJWT","envValue: string | undefined","parsed: number","DEFAULT_SESSION_COOKIE_EXPIRY_TIME","payload: SessionTokenPayload","CookieConfig"],"sources":["../../../src/utils/SessionManager.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {ThunderIDRuntimeError, CookieConfig} from '@thunderid/node';\nimport {SignJWT, jwtVerify, compactVerify, JWTPayload} from 'jose';\nimport {DEFAULT_SESSION_COOKIE_EXPIRY_TIME} from '../constants/sessionConstants';\n\n/**\n * Session token payload interface\n */\nexport interface SessionTokenPayload extends JWTPayload {\n /** Expiration timestamp — doubles as the access token expiry (JWT exp == access token exp) */\n exp: number;\n /** Issued at timestamp */\n iat: number;\n /** Organization ID if applicable */\n organizationId?: string;\n /** The refresh token; empty string if not provided by the auth server */\n refreshToken: string;\n /** OAuth scopes */\n scopes: string[];\n /** Session ID */\n sessionId: string;\n /** User ID */\n sub: string;\n /** Token type discriminant — must be 'session' for access-session JWTs */\n type: 'session';\n}\n\n/**\n * Session management utility class for JWT-based session cookies\n */\nclass SessionManager {\n /**\n * Get the signing secret from environment variable\n * Throws error in production if not set\n */\n private static getSecret(): Uint8Array {\n const secret: string | undefined = process.env['THUNDERID_SECRET'];\n\n if (!secret) {\n if (process.env['NODE_ENV'] === 'production') {\n throw new ThunderIDRuntimeError(\n 'THUNDERID_SECRET environment variable is required in production',\n 'session-secret-required',\n 'nextjs',\n 'Set the THUNDERID_SECRET environment variable with a secure random string',\n );\n }\n // Use a default secret for development (not secure)\n // eslint-disable-next-line no-console\n console.warn('Using default secret for development. Set THUNDERID_SECRET for production!');\n return new TextEncoder().encode('development-secret-not-for-production');\n }\n\n return new TextEncoder().encode(secret);\n }\n\n /**\n * Create a temporary session cookie for login initiation\n */\n static async createTempSession(sessionId: string): Promise<string> {\n const secret: Uint8Array = this.getSecret();\n\n const jwt: string = await new SignJWT({\n sessionId,\n type: 'temp',\n })\n .setProtectedHeader({alg: 'HS256'})\n .setIssuedAt()\n .setExpirationTime('15m')\n .sign(secret);\n\n return jwt;\n }\n\n /**\n * Resolve the session cookie expiry time in seconds.\n *\n * Resolution order (first defined value wins):\n * 1. `configuredExpiry` — value from `ThunderIDNodeConfig.sessionCookie?.expiryTime`\n * 2. `THUNDERID_SESSION_COOKIE_EXPIRY_TIME` environment variable\n * 3. `DEFAULT_SESSION_COOKIE_EXPIRY_TIME` (24 hours)\n */\n static resolveSessionCookieExpiry(configuredExpiry?: number): number {\n if (configuredExpiry != null && configuredExpiry > 0) {\n return configuredExpiry;\n }\n\n const envValue: string | undefined = process.env['THUNDERID_SESSION_COOKIE_EXPIRY_TIME'];\n\n if (envValue) {\n const parsed: number = parseInt(envValue, 10);\n\n if (!Number.isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n\n return DEFAULT_SESSION_COOKIE_EXPIRY_TIME;\n }\n\n static async createSessionToken(\n accessToken: string,\n userId: string,\n sessionId: string,\n scopes: string,\n accessTokenTtlSeconds: number,\n refreshToken: string,\n organizationId?: string,\n ): Promise<string> {\n const secret: Uint8Array = this.getSecret();\n\n const jwt: string = await new SignJWT({\n accessToken,\n organizationId,\n refreshToken,\n scopes,\n sessionId,\n type: 'session',\n } as Omit<SessionTokenPayload, 'sub' | 'iat' | 'exp'>)\n .setProtectedHeader({alg: 'HS256'})\n .setSubject(userId)\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + accessTokenTtlSeconds)\n .sign(secret);\n\n return jwt;\n }\n\n /**\n * Verify and decode a session token\n */\n static async verifySessionToken(token: string): Promise<SessionTokenPayload> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload} = await jwtVerify(token, secret);\n\n if (payload['type'] !== 'session') {\n throw new Error('Invalid token type');\n }\n\n return payload as SessionTokenPayload;\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-session-token',\n 'nextjs',\n 'Session token verification failed',\n );\n }\n }\n\n /**\n * Verify a session token for refresh. Validates the HMAC signature and the\n * `type === 'session'` discriminant but intentionally skips the `exp` check\n * so an expired access token can still be exchanged for a new one.\n *\n * Session lifetime is still bounded — the cookie's `maxAge` is set from\n * `sessionCookieExpiryTime`, so the browser drops an over-age session regardless\n * of the access-token exp embedded in the JWT.\n *\n * Never use the returned payload for authorization.\n */\n static async verifySessionTokenForRefresh(token: string): Promise<SessionTokenPayload> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload: rawPayload} = await compactVerify(token, secret);\n const payload: SessionTokenPayload = JSON.parse(new TextDecoder().decode(rawPayload)) as SessionTokenPayload;\n\n if (payload.type !== 'session') {\n throw new Error('Invalid token type');\n }\n\n return payload;\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-session-token-for-refresh',\n 'nextjs',\n 'Session token signature or type check failed during refresh',\n );\n }\n }\n\n /**\n * Verify and decode a temporary session token\n */\n static async verifyTempSession(token: string): Promise<{sessionId: string}> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload} = await jwtVerify(token, secret);\n\n if (payload['type'] !== 'temp') {\n throw new Error('Invalid token type');\n }\n\n return {sessionId: payload['sessionId'] as string};\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid temporary session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-temp-session-token',\n 'nextjs',\n 'Temporary session token verification failed',\n );\n }\n }\n\n /**\n * Get session cookie options\n */\n static getSessionCookieOptions(maxAge: number): {\n httpOnly: boolean;\n maxAge: number;\n path: string;\n sameSite: 'lax';\n secure: boolean;\n } {\n return {\n httpOnly: true,\n maxAge,\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env['NODE_ENV'] === 'production',\n };\n }\n\n /**\n * Get temporary session cookie options\n */\n static getTempSessionCookieOptions(): {\n httpOnly: boolean;\n maxAge: number;\n path: string;\n sameSite: 'lax';\n secure: boolean;\n } {\n return {\n httpOnly: true,\n maxAge: 15 * 60,\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env['NODE_ENV'] === 'production',\n };\n }\n\n /**\n * Get session cookie name\n */\n static getSessionCookieName(): string {\n return CookieConfig.getSessionCookieName();\n }\n\n /**\n * Get temporary session cookie name\n */\n static getTempSessionCookieName(): string {\n return CookieConfig.getTempSessionCookieName();\n }\n}\n\nexport default SessionManager;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SessionManager.cjs","names":["secret: string | undefined","ThunderIDRuntimeError","secret: Uint8Array","SignJWT","envValue: string | undefined","parsed: number","DEFAULT_SESSION_COOKIE_EXPIRY_TIME","payload: SessionTokenPayload","getLoggedOutSessions","CookieConfig"],"sources":["../../../src/utils/SessionManager.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {ThunderIDRuntimeError, CookieConfig} from '@thunderid/node';\nimport {SignJWT, jwtVerify, compactVerify, JWTPayload} from 'jose';\nimport {getLoggedOutSessions} from './loggedOutSessions';\nimport {DEFAULT_SESSION_COOKIE_EXPIRY_TIME} from '../constants/sessionConstants';\n\n/**\n * Session token payload interface\n */\nexport interface SessionTokenPayload extends JWTPayload {\n /** Expiration timestamp — doubles as the access token expiry (JWT exp == access token exp) */\n exp: number;\n /** Issued at timestamp */\n iat: number;\n /** Organization ID if applicable */\n organizationId?: string;\n /** The refresh token; empty string if not provided by the auth server */\n refreshToken: string;\n /** OAuth scopes */\n scopes: string[];\n /** Session ID */\n sessionId: string;\n /** The server session this session joined, from the `sid` of the ID token. */\n sid?: string;\n /** When the user signed in, in seconds. Kept across token refresh, unlike `iat`. */\n signedInAt?: number;\n /** User ID */\n sub: string;\n /** Token type discriminant — must be 'session' for access-session JWTs */\n type: 'session';\n}\n\n/**\n * Session management utility class for JWT-based session cookies\n */\nclass SessionManager {\n /**\n * Get the signing secret from environment variable\n * Throws error in production if not set\n */\n private static getSecret(): Uint8Array {\n const secret: string | undefined = process.env['THUNDERID_SECRET'];\n\n if (!secret) {\n if (process.env['NODE_ENV'] === 'production') {\n throw new ThunderIDRuntimeError(\n 'THUNDERID_SECRET environment variable is required in production',\n 'session-secret-required',\n 'nextjs',\n 'Set the THUNDERID_SECRET environment variable with a secure random string',\n );\n }\n // Use a default secret for development (not secure)\n // eslint-disable-next-line no-console\n console.warn('Using default secret for development. Set THUNDERID_SECRET for production!');\n return new TextEncoder().encode('development-secret-not-for-production');\n }\n\n return new TextEncoder().encode(secret);\n }\n\n /**\n * Create a temporary session cookie for login initiation\n */\n static async createTempSession(sessionId: string): Promise<string> {\n const secret: Uint8Array = this.getSecret();\n\n const jwt: string = await new SignJWT({\n sessionId,\n type: 'temp',\n })\n .setProtectedHeader({alg: 'HS256'})\n .setIssuedAt()\n .setExpirationTime('15m')\n .sign(secret);\n\n return jwt;\n }\n\n /**\n * Resolve the session cookie expiry time in seconds.\n *\n * Resolution order (first defined value wins):\n * 1. `configuredExpiry` — value from `ThunderIDNodeConfig.sessionCookie?.expiryTime`\n * 2. `THUNDERID_SESSION_COOKIE_EXPIRY_TIME` environment variable\n * 3. `DEFAULT_SESSION_COOKIE_EXPIRY_TIME` (24 hours)\n */\n static resolveSessionCookieExpiry(configuredExpiry?: number): number {\n if (configuredExpiry != null && configuredExpiry > 0) {\n return configuredExpiry;\n }\n\n const envValue: string | undefined = process.env['THUNDERID_SESSION_COOKIE_EXPIRY_TIME'];\n\n if (envValue) {\n const parsed: number = parseInt(envValue, 10);\n\n if (!Number.isNaN(parsed) && parsed > 0) {\n return parsed;\n }\n }\n\n return DEFAULT_SESSION_COOKIE_EXPIRY_TIME;\n }\n\n static async createSessionToken(\n accessToken: string,\n userId: string,\n sessionId: string,\n scopes: string,\n accessTokenTtlSeconds: number,\n refreshToken: string,\n organizationId?: string,\n serverSession?: {sid?: string; signedInAt?: number},\n ): Promise<string> {\n const secret: Uint8Array = this.getSecret();\n\n const jwt: string = await new SignJWT({\n accessToken,\n organizationId,\n refreshToken,\n scopes,\n sessionId,\n // What a back-channel logout matches this session by.\n sid: serverSession?.sid,\n signedInAt: serverSession?.signedInAt ?? Math.floor(Date.now() / 1000),\n type: 'session',\n } as Omit<SessionTokenPayload, 'sub' | 'iat' | 'exp'>)\n .setProtectedHeader({alg: 'HS256'})\n .setSubject(userId)\n .setIssuedAt()\n .setExpirationTime(Math.floor(Date.now() / 1000) + accessTokenTtlSeconds)\n .sign(secret);\n\n return jwt;\n }\n\n /**\n * Verify and decode a session token\n */\n static async verifySessionToken(token: string): Promise<SessionTokenPayload> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload} = await jwtVerify(token, secret);\n\n if (payload['type'] !== 'session') {\n throw new Error('Invalid token type');\n }\n\n await this.refuseLoggedOutSession(payload as SessionTokenPayload);\n\n return payload as SessionTokenPayload;\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-session-token',\n 'nextjs',\n 'Session token verification failed',\n );\n }\n }\n\n /**\n * Verify a session token for refresh. Validates the HMAC signature and the\n * `type === 'session'` discriminant but intentionally skips the `exp` check\n * so an expired access token can still be exchanged for a new one.\n *\n * Session lifetime is still bounded — the cookie's `maxAge` is set from\n * `sessionCookieExpiryTime`, so the browser drops an over-age session regardless\n * of the access-token exp embedded in the JWT.\n *\n * Never use the returned payload for authorization.\n */\n static async verifySessionTokenForRefresh(token: string): Promise<SessionTokenPayload> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload: rawPayload} = await compactVerify(token, secret);\n const payload: SessionTokenPayload = JSON.parse(new TextDecoder().decode(rawPayload)) as SessionTokenPayload;\n\n if (payload.type !== 'session') {\n throw new Error('Invalid token type');\n }\n\n await this.refuseLoggedOutSession(payload);\n\n return payload;\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-session-token-for-refresh',\n 'nextjs',\n 'Session token signature or type check failed during refresh',\n );\n }\n }\n\n /**\n * Verify and decode a temporary session token\n */\n static async verifyTempSession(token: string): Promise<{sessionId: string}> {\n try {\n const secret: Uint8Array = this.getSecret();\n const {payload} = await jwtVerify(token, secret);\n\n if (payload['type'] !== 'temp') {\n throw new Error('Invalid token type');\n }\n\n return {sessionId: payload['sessionId'] as string};\n } catch (error) {\n throw new ThunderIDRuntimeError(\n `Invalid temporary session token: ${error instanceof Error ? error.message : 'Unknown error'}`,\n 'invalid-temp-session-token',\n 'nextjs',\n 'Temporary session token verification failed',\n );\n }\n }\n\n /**\n * Get session cookie options\n */\n /**\n * Throws when a back-channel logout has ended the session this cookie carries. The cookie is\n * still correctly signed, so this is the only thing that stops it from being used.\n */\n private static async refuseLoggedOutSession(payload: SessionTokenPayload): Promise<void> {\n const loggedOut: boolean = await getLoggedOutSessions().isLoggedOut({\n sid: payload.sid,\n startedAt: payload.signedInAt ?? payload.iat,\n sub: payload.sub,\n });\n\n if (loggedOut) {\n throw new Error('The session was ended by a back-channel logout');\n }\n }\n\n static getSessionCookieOptions(maxAge: number): {\n httpOnly: boolean;\n maxAge: number;\n path: string;\n sameSite: 'lax';\n secure: boolean;\n } {\n return {\n httpOnly: true,\n maxAge,\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env['NODE_ENV'] === 'production',\n };\n }\n\n /**\n * Get temporary session cookie options\n */\n static getTempSessionCookieOptions(): {\n httpOnly: boolean;\n maxAge: number;\n path: string;\n sameSite: 'lax';\n secure: boolean;\n } {\n return {\n httpOnly: true,\n maxAge: 15 * 60,\n path: '/',\n sameSite: 'lax' as const,\n secure: process.env['NODE_ENV'] === 'production',\n };\n }\n\n /**\n * Get session cookie name\n */\n static getSessionCookieName(): string {\n return CookieConfig.getSessionCookieName();\n }\n\n /**\n * Get temporary session cookie name\n */\n static getTempSessionCookieName(): string {\n return CookieConfig.getTempSessionCookieName();\n }\n}\n\nexport default SessionManager;\n"],"mappings":"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"}
|
|
@@ -10,7 +10,7 @@ const require_SessionManager = require('./SessionManager.cjs');
|
|
|
10
10
|
*/
|
|
11
11
|
const handleRefreshToken = async (sessionPayload, config) => {
|
|
12
12
|
const { baseUrl, clientId, clientSecret, sessionCookie } = config;
|
|
13
|
-
const { refreshToken: storedRefreshToken, sessionId, sub, scopes, organizationId } = sessionPayload;
|
|
13
|
+
const { refreshToken: storedRefreshToken, sessionId, sub, scopes, organizationId, sid, signedInAt, iat } = sessionPayload;
|
|
14
14
|
if (!storedRefreshToken) throw new Error("No refresh token found in session payload.");
|
|
15
15
|
const tokenEndpoint = `${baseUrl}/oauth2/token`;
|
|
16
16
|
const body = new URLSearchParams({
|
|
@@ -42,7 +42,10 @@ const handleRefreshToken = async (sessionPayload, config) => {
|
|
|
42
42
|
const newScopes = tokenData["scope"] ?? (Array.isArray(scopes) ? scopes.join(" ") : scopes ?? "");
|
|
43
43
|
const resolvedSessionCookieExpiry = require_SessionManager.default.resolveSessionCookieExpiry(sessionCookie?.expiryTime);
|
|
44
44
|
return {
|
|
45
|
-
newSessionToken: await require_SessionManager.default.createSessionToken(newAccessToken, sub, sessionId, newScopes, expiresIn, newRefreshToken, organizationId
|
|
45
|
+
newSessionToken: await require_SessionManager.default.createSessionToken(newAccessToken, sub, sessionId, newScopes, expiresIn, newRefreshToken, organizationId, {
|
|
46
|
+
sid,
|
|
47
|
+
signedInAt: signedInAt ?? iat
|
|
48
|
+
}),
|
|
46
49
|
sessionCookieExpiryTime: resolvedSessionCookieExpiry,
|
|
47
50
|
tokenResponse: {
|
|
48
51
|
accessToken: newAccessToken,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handleRefreshToken.cjs","names":["body: URLSearchParams","response: Response","tokenData: Record<string, unknown>","newAccessToken: string","expiresIn: number","newRefreshToken: string","newScopes: string","resolvedSessionCookieExpiry: number","SessionManager"],"sources":["../../../src/utils/handleRefreshToken.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {TokenResponse, SessionCookieConfig} from '@thunderid/node';\nimport SessionManager, {SessionTokenPayload} from './SessionManager';\n\n/**\n * Config required to call the token endpoint.\n */\nexport interface HandleRefreshTokenConfig {\n baseUrl: string;\n clientId: string;\n clientSecret: string;\n sessionCookie?: SessionCookieConfig;\n}\n\n/**\n * Result returned by handleRefreshToken.\n * Callers are responsible for persisting newSessionToken in the appropriate cookie context.\n */\nexport interface HandleRefreshTokenResult {\n newSessionToken: string;\n sessionCookieExpiryTime: number;\n tokenResponse: TokenResponse;\n}\n\n/**\n * Handles the OAuth refresh_token grant and builds a new session JWT string.\n *\n * Intentionally decoupled from cookie APIs so it can be called from both the Edge\n * Runtime (Next.js middleware) and the Node.js Runtime (server actions).\n * Cookie persistence is the caller's responsibility.\n */\nconst handleRefreshToken = async (\n sessionPayload: SessionTokenPayload,\n config: HandleRefreshTokenConfig,\n): Promise<HandleRefreshTokenResult> => {\n const {baseUrl, clientId, clientSecret, sessionCookie} = config;\n const {refreshToken: storedRefreshToken
|
|
1
|
+
{"version":3,"file":"handleRefreshToken.cjs","names":["body: URLSearchParams","response: Response","tokenData: Record<string, unknown>","newAccessToken: string","expiresIn: number","newRefreshToken: string","newScopes: string","resolvedSessionCookieExpiry: number","SessionManager"],"sources":["../../../src/utils/handleRefreshToken.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport type {TokenResponse, SessionCookieConfig} from '@thunderid/node';\nimport SessionManager, {SessionTokenPayload} from './SessionManager';\n\n/**\n * Config required to call the token endpoint.\n */\nexport interface HandleRefreshTokenConfig {\n baseUrl: string;\n clientId: string;\n clientSecret: string;\n sessionCookie?: SessionCookieConfig;\n}\n\n/**\n * Result returned by handleRefreshToken.\n * Callers are responsible for persisting newSessionToken in the appropriate cookie context.\n */\nexport interface HandleRefreshTokenResult {\n newSessionToken: string;\n sessionCookieExpiryTime: number;\n tokenResponse: TokenResponse;\n}\n\n/**\n * Handles the OAuth refresh_token grant and builds a new session JWT string.\n *\n * Intentionally decoupled from cookie APIs so it can be called from both the Edge\n * Runtime (Next.js middleware) and the Node.js Runtime (server actions).\n * Cookie persistence is the caller's responsibility.\n */\nconst handleRefreshToken = async (\n sessionPayload: SessionTokenPayload,\n config: HandleRefreshTokenConfig,\n): Promise<HandleRefreshTokenResult> => {\n const {baseUrl, clientId, clientSecret, sessionCookie} = config;\n const {\n refreshToken: storedRefreshToken,\n sessionId,\n sub,\n scopes,\n organizationId,\n sid,\n signedInAt,\n iat,\n } = sessionPayload;\n\n if (!storedRefreshToken) {\n throw new Error('No refresh token found in session payload.');\n }\n\n const tokenEndpoint = `${baseUrl}/oauth2/token`;\n const body: URLSearchParams = new URLSearchParams({\n client_id: clientId ?? '',\n client_secret: clientSecret ?? '',\n grant_type: 'refresh_token',\n refresh_token: storedRefreshToken,\n });\n\n let response: Response;\n\n try {\n response = await fetch(tokenEndpoint, {\n body: body.toString(),\n headers: {'Content-Type': 'application/x-www-form-urlencoded'},\n method: 'POST',\n });\n } catch (fetchError) {\n throw new Error(\n `Token refresh network error: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`,\n );\n }\n\n if (!response.ok) {\n throw new Error(`Token endpoint rejected refresh (HTTP ${response.status}).`);\n }\n\n let tokenData: Record<string, unknown>;\n\n try {\n tokenData = (await response.json()) as Record<string, unknown>;\n } catch {\n throw new Error('Failed to parse token endpoint response as JSON.');\n }\n\n const newAccessToken: string = tokenData['access_token'] as string;\n const expiresIn: number = tokenData['expires_in'] as number;\n // Use the rotated refresh token if the server provided one; otherwise keep the existing one.\n const newRefreshToken: string = (tokenData['refresh_token'] as string | undefined) ?? storedRefreshToken;\n const newScopes: string =\n (tokenData['scope'] as string | undefined) ??\n (Array.isArray(scopes) ? scopes.join(' ') : ((scopes as string) ?? ''));\n\n const resolvedSessionCookieExpiry: number = SessionManager.resolveSessionCookieExpiry(sessionCookie?.expiryTime);\n\n const newSessionToken: string = await SessionManager.createSessionToken(\n newAccessToken,\n sub,\n sessionId,\n newScopes,\n expiresIn,\n newRefreshToken,\n organizationId,\n // A refreshed session is still the same sign-in, so it keeps what a logout matches it by.\n {sid, signedInAt: signedInAt ?? iat},\n );\n\n return {\n newSessionToken,\n sessionCookieExpiryTime: resolvedSessionCookieExpiry,\n tokenResponse: {\n accessToken: newAccessToken,\n createdAt: Math.floor(Date.now() / 1000),\n expiresIn: String(expiresIn),\n idToken: (tokenData['id_token'] as string | undefined) ?? '',\n refreshToken: newRefreshToken,\n scope: newScopes,\n tokenType: (tokenData['token_type'] as string | undefined) ?? 'Bearer',\n },\n };\n};\n\nexport default handleRefreshToken;\n"],"mappings":";;;;;;;;;;AAiCA,MAAM,qBAAqB,OACzB,gBACA,WACsC;CACtC,MAAM,EAAC,SAAS,UAAU,cAAc,kBAAiB;CACzD,MAAM,EACJ,cAAc,oBACd,WACA,KACA,QACA,gBACA,KACA,YACA,QACE;AAEJ,KAAI,CAAC,mBACH,OAAM,IAAI,MAAM,6CAA6C;CAG/D,MAAM,gBAAgB,GAAG,QAAQ;CACjC,MAAMA,OAAwB,IAAI,gBAAgB;EAChD,WAAW,YAAY;EACvB,eAAe,gBAAgB;EAC/B,YAAY;EACZ,eAAe;EAChB,CAAC;CAEF,IAAIC;AAEJ,KAAI;AACF,aAAW,MAAM,MAAM,eAAe;GACpC,MAAM,KAAK,UAAU;GACrB,SAAS,EAAC,gBAAgB,qCAAoC;GAC9D,QAAQ;GACT,CAAC;UACK,YAAY;AACnB,QAAM,IAAI,MACR,gCAAgC,sBAAsB,QAAQ,WAAW,UAAU,OAAO,WAAW,GACtG;;AAGH,KAAI,CAAC,SAAS,GACZ,OAAM,IAAI,MAAM,yCAAyC,SAAS,OAAO,IAAI;CAG/E,IAAIC;AAEJ,KAAI;AACF,cAAa,MAAM,SAAS,MAAM;SAC5B;AACN,QAAM,IAAI,MAAM,mDAAmD;;CAGrE,MAAMC,iBAAyB,UAAU;CACzC,MAAMC,YAAoB,UAAU;CAEpC,MAAMC,kBAA2B,UAAU,oBAA2C;CACtF,MAAMC,YACH,UAAU,aACV,MAAM,QAAQ,OAAO,GAAG,OAAO,KAAK,IAAI,GAAK,UAAqB;CAErE,MAAMC,8BAAsCC,+BAAe,2BAA2B,eAAe,WAAW;AAchH,QAAO;EACL,iBAb8B,MAAMA,+BAAe,mBACnD,gBACA,KACA,WACA,WACA,WACA,iBACA,gBAEA;GAAC;GAAK,YAAY,cAAc;GAAI,CACrC;EAIC,yBAAyB;EACzB,eAAe;GACb,aAAa;GACb,WAAW,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK;GACxC,WAAW,OAAO,UAAU;GAC5B,SAAU,UAAU,eAAsC;GAC1D,cAAc;GACd,OAAO;GACP,WAAY,UAAU,iBAAwC;GAC/D;EACF;;AAGH,iCAAe"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let __thunderid_node = require("@thunderid/node");
|
|
3
|
+
__thunderid_node = require_rolldown_runtime.__toESM(__thunderid_node);
|
|
4
|
+
|
|
5
|
+
//#region src/utils/loggedOutSessions.ts
|
|
6
|
+
const SHARED_KEY = Symbol.for("thunderid.nextjs.loggedOutSessions");
|
|
7
|
+
const shared = () => {
|
|
8
|
+
const scope = globalThis;
|
|
9
|
+
scope[SHARED_KEY] ??= { sessions: new __thunderid_node.LoggedOutSessions() };
|
|
10
|
+
return scope[SHARED_KEY];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Returns the list of sessions that back-channel logout has ended, shared by everything that runs
|
|
14
|
+
* in this process.
|
|
15
|
+
*/
|
|
16
|
+
const getLoggedOutSessions = () => shared().sessions;
|
|
17
|
+
/**
|
|
18
|
+
* Makes the list use the given store, so several instances of an application can share it.
|
|
19
|
+
* Without one, records stay in the memory of this process.
|
|
20
|
+
*/
|
|
21
|
+
const setLogoutRecordStore = (store) => {
|
|
22
|
+
const current = shared();
|
|
23
|
+
if (store && current.store !== store) {
|
|
24
|
+
current.store = store;
|
|
25
|
+
current.sessions = new __thunderid_node.LoggedOutSessions(store);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
exports.getLoggedOutSessions = getLoggedOutSessions;
|
|
31
|
+
exports.setLogoutRecordStore = setLogoutRecordStore;
|
|
32
|
+
//# sourceMappingURL=loggedOutSessions.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loggedOutSessions.cjs","names":["SHARED_KEY: unique symbol","scope: GlobalWithShared","LoggedOutSessions","current: Shared"],"sources":["../../../src/utils/loggedOutSessions.ts"],"sourcesContent":["// Copyright 2026 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\nimport {LoggedOutSessions, LogoutRecordStore} from '@thunderid/node';\n\n// Next.js bundles the proxy, route handlers and server components separately, so a module-level\n// variable would give each its own list. The global object is the one thing they share in a process.\nconst SHARED_KEY: unique symbol = Symbol.for('thunderid.nextjs.loggedOutSessions');\n\ninterface Shared {\n sessions: LoggedOutSessions;\n store?: LogoutRecordStore;\n}\n\ntype GlobalWithShared = typeof globalThis & {[SHARED_KEY]?: Shared};\n\nconst shared = (): Shared => {\n const scope: GlobalWithShared = globalThis as GlobalWithShared;\n\n scope[SHARED_KEY] ??= {sessions: new LoggedOutSessions()};\n\n return scope[SHARED_KEY];\n};\n\n/**\n * Returns the list of sessions that back-channel logout has ended, shared by everything that runs\n * in this process.\n */\nexport const getLoggedOutSessions = (): LoggedOutSessions => shared().sessions;\n\n/**\n * Makes the list use the given store, so several instances of an application can share it.\n * Without one, records stay in the memory of this process.\n */\nexport const setLogoutRecordStore = (store?: LogoutRecordStore): void => {\n const current: Shared = shared();\n\n if (store && current.store !== store) {\n current.store = store;\n current.sessions = new LoggedOutSessions(store);\n }\n};\n"],"mappings":";;;;;AAOA,MAAMA,aAA4B,OAAO,IAAI,qCAAqC;AASlF,MAAM,eAAuB;CAC3B,MAAMC,QAA0B;AAEhC,OAAM,gBAAgB,EAAC,UAAU,IAAIC,oCAAmB,EAAC;AAEzD,QAAO,MAAM;;;;;;AAOf,MAAa,6BAAgD,QAAQ,CAAC;;;;;AAMtE,MAAa,wBAAwB,UAAoC;CACvE,MAAMC,UAAkB,QAAQ;AAEhC,KAAI,SAAS,QAAQ,UAAU,OAAO;AACpC,UAAQ,QAAQ;AAChB,UAAQ,WAAW,IAAID,mCAAkB,MAAM"}
|
|
@@ -20,7 +20,7 @@ export type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps,
|
|
|
20
20
|
applicationId?: string;
|
|
21
21
|
language?: string;
|
|
22
22
|
}) => Promise<FlowMetadataResponse>;
|
|
23
|
-
handleOAuthCallback: (code: string, state: string
|
|
23
|
+
handleOAuthCallback: (code: string, state: string) => Promise<{
|
|
24
24
|
error?: string;
|
|
25
25
|
redirectUrl?: string;
|
|
26
26
|
success: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThunderIDProvider.d.ts","sourceRoot":"","sources":["../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"names":[],"mappings":"AAKA,OAAO,EACL,eAAe,EAEf,oBAAoB,EAEpB,qBAAqB,EACrB,IAAI,EACJ,WAAW,EAGZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAQL,sBAAsB,EACvB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAAC,EAAE,EAAE,iBAAiB,EAAkD,MAAM,OAAO,CAAC;AAC7F,OAAyB,EAAC,qBAAqB,EAAC,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAC,aAAa,EAAC,MAAM,sCAAsC,CAAC;AAGnE;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC,GACtG,IAAI,CAAC,sBAAsB,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IACrD,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAC;IACtD,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnG,mBAAmB,EAAE,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"ThunderIDProvider.d.ts","sourceRoot":"","sources":["../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"names":[],"mappings":"AAKA,OAAO,EACL,eAAe,EAEf,oBAAoB,EAEpB,qBAAqB,EACrB,IAAI,EACJ,WAAW,EAGZ,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAQL,sBAAsB,EACvB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EAAC,EAAE,EAAE,iBAAiB,EAAkD,MAAM,OAAO,CAAC;AAC7F,OAAyB,EAAC,qBAAqB,EAAC,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAC,aAAa,EAAC,MAAM,sCAAsC,CAAC;AAGnE;;GAEG;AACH,MAAM,MAAM,4BAA4B,GAAG,OAAO,CAAC,IAAI,CAAC,sBAAsB,EAAE,SAAS,GAAG,UAAU,CAAC,CAAC,GACtG,IAAI,CAAC,sBAAsB,EAAE,SAAS,GAAG,UAAU,CAAC,GAAG;IACrD,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,CAAC;IACtD,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAC,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnG,mBAAmB,EAAE,CACnB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,KACV,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;IACvE;;;OAGG;IACH,WAAW,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,UAAU,EAAE,OAAO,CAAC;IACpB,kBAAkB,EAAE,qBAAqB,CAAC,oBAAoB,CAAC,CAAC;IAChE,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACxC,OAAO,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,EAAE,qBAAqB,CAAC,QAAQ,CAAC,CAAC;IACxC,aAAa,EAAE,CACb,aAAa,EAAE,qBAAqB,EACpC,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC;QAAC,IAAI,EAAE;YAAC,IAAI,EAAE,IAAI,CAAA;SAAC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;IACpE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,WAAW,EAAE,WAAW,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,IAAI,CAAC;CACrD,CAAC;AAEJ,QAAA,MAAM,uBAAuB,EAAE,EAAE,CAAC,iBAAiB,CAAC,4BAA4B,CAAC,CA2VhF,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -35,14 +35,13 @@ const ThunderIDClientProvider = ({ baseUrl, children, signIn, clearSession, refr
|
|
|
35
35
|
try {
|
|
36
36
|
const code = searchParams.get("code");
|
|
37
37
|
const state = searchParams.get("state");
|
|
38
|
-
const sessionState = searchParams.get("session_state");
|
|
39
38
|
if (searchParams.get("error")) {
|
|
40
39
|
logger_default.error("[ThunderIDClientProvider] An error was received for the initiated sign-in request.");
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
if (code && state) {
|
|
44
43
|
setIsLoading(true);
|
|
45
|
-
const result = await handleOAuthCallback(code, state
|
|
44
|
+
const result = await handleOAuthCallback(code, state);
|
|
46
45
|
if (result.success) if (result.redirectUrl) router.push(result.redirectUrl);
|
|
47
46
|
else window.location.reload();
|
|
48
47
|
else logger_default.error(`[ThunderIDClientProvider] An error occurred while signing in: ${result.error || "Authentication failed"}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThunderIDProvider.js","names":["ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>>","reRenderCheckRef: RefObject<boolean>","router: AppRouterInstance","searchParams: ReadonlyURLSearchParams","code: string | null","state: string | null","sessionState: string | null","result: {error?: string; redirectUrl?: string; success: boolean}","result: any","contextValue: ThunderIDContextProps","reactContextValue: ReactThunderIDContextProps","ThunderIDContext","ReactThunderIDContext"],"sources":["../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use client';\n\nimport {\n AttributeSchema,\n EmbeddedFlowExecuteRequestConfig,\n FlowMetadataResponse,\n generateFlattenedUserProfile,\n UpdateMeProfileConfig,\n User,\n UserProfile,\n ThunderIDRuntimeError,\n getVendorPrefix,\n} from '@thunderid/node';\nimport {\n I18nProvider,\n FlowMetaProvider,\n FlowProvider,\n UserProvider,\n ThemeProvider,\n ThunderIDContext as ReactThunderIDContext,\n ThunderIDContextProps as ReactThunderIDContextProps,\n ThunderIDProviderProps,\n} from '@thunderid/react';\nimport {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server';\nimport {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime';\nimport {useRouter, useSearchParams} from 'next/navigation';\nimport {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';\nimport ThunderIDContext, {ThunderIDContextProps} from './ThunderIDContext';\nimport {RefreshResult} from '../../../server/actions/refreshToken';\nimport logger from '../../../utils/logger';\n\n/**\n * Props interface of {@link ThunderIDClientProvider}\n */\nexport type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps, 'baseUrl' | 'clientId'>> &\n Pick<ThunderIDProviderProps, 'baseUrl' | 'clientId'> & {\n applicationId: ThunderIDContextProps['applicationId'];\n clearSession: () => Promise<void>;\n /**\n * Server Action that fetches `GET /flow/meta` server-side, passed down to `FlowMetaProvider` so\n * it never calls `baseUrl` directly from the browser — avoiding the CORS requirement that a\n * direct client-side fetch to the ThunderID server would otherwise impose. Used for both the\n * initial fetch (when `initialMeta` isn't seeded, e.g. it failed server-side) and subsequent\n * `switchLanguage()` calls.\n */\n fetchMeta?: (params: {applicationId?: string; language?: string}) => Promise<FlowMetadataResponse>;\n handleOAuthCallback: (\n code: string,\n state: string,\n sessionState?: string,\n ) => Promise<{error?: string; redirectUrl?: string; success: boolean}>;\n /**\n * Flow metadata fetched server-side ahead of time, seeding `FlowMetaProvider` so it can skip\n * its own initial client-side fetch (avoiding a flash of untranslated i18n keys).\n */\n initialMeta?: FlowMetadataResponse | null;\n isSignedIn: boolean;\n organizationHandle: ThunderIDContextProps['organizationHandle'];\n refreshToken: () => Promise<RefreshResult>;\n signIn: ThunderIDContextProps['signIn'];\n signOut: ThunderIDContextProps['signOut'];\n signUp: ThunderIDContextProps['signUp'];\n updateProfile: (\n requestConfig: UpdateMeProfileConfig,\n sessionId?: string,\n ) => Promise<{data: {user: User}; error: string; success: boolean}>;\n user: User | null;\n userProfile: UserProfile;\n userSchema?: Record<string, AttributeSchema> | null;\n };\n\nconst ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({\n baseUrl,\n children,\n signIn,\n clearSession,\n refreshToken,\n signOut,\n signUp,\n handleOAuthCallback,\n preferences,\n isSignedIn,\n signInUrl,\n signUpUrl,\n user: _user,\n userProfile: _userProfile,\n userSchema: _userSchema = null,\n updateProfile,\n applicationId,\n organizationHandle,\n scopes,\n vendor,\n initialMeta = null,\n fetchMeta,\n}: PropsWithChildren<ThunderIDClientProviderProps>) => {\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const router: AppRouterInstance = useRouter();\n const searchParams: ReadonlyURLSearchParams = useSearchParams();\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [user, setUser] = useState<User | null>(_user);\n const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);\n\n useEffect(() => {\n setUserProfile(_userProfile);\n }, [_userProfile]);\n\n useEffect(() => {\n setUserSchema(_userSchema);\n }, [_userSchema]);\n\n useEffect(() => {\n setUser(_user);\n }, [_user]);\n\n // Handle OAuth callback automatically\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n // Don't handle callback if already signed in\n if (isSignedIn) return;\n\n (async (): Promise<void> => {\n try {\n const code: string | null = searchParams.get('code');\n const state: string | null = searchParams.get('state');\n const sessionState: string | null = searchParams.get('session_state');\n const error: string | null = searchParams.get('error');\n\n // Check for OAuth errors first\n if (error) {\n logger.error('[ThunderIDClientProvider] An error was received for the initiated sign-in request.');\n\n return;\n }\n\n // Handle OAuth callback if code and state are present\n if (code && state) {\n setIsLoading(true);\n\n const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback(\n code,\n state,\n sessionState || undefined,\n );\n\n if (result.success) {\n // Redirect to the success URL\n if (result.redirectUrl) {\n router.push(result.redirectUrl);\n } else {\n // Refresh the page to update authentication state\n window.location.reload();\n }\n } else {\n logger.error(\n `[ThunderIDClientProvider] An error occurred while signing in: ${result.error || 'Authentication failed'}`,\n );\n }\n }\n } catch (error) {\n logger.error('[ThunderIDClientProvider] Failed to handle OAuth callback:', error);\n }\n })();\n }, []);\n\n useEffect(() => {\n // Set loading to false when server has resolved authentication state\n setIsLoading(false);\n }, [isSignedIn, user]);\n\n const handleSignIn = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signIn) {\n throw new ThunderIDRuntimeError(\n '`signIn` function is not available.',\n 'ThunderIDClientProvider-handleSignIn-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signIn(payload, request);\n\n // Redirect based flow URL is sent as `signInUrl` in the response.\n // Use window.location.href instead of router.push() — the OAuth authorization\n // endpoint is on an external server, and router.push() would send RSC fetch\n // headers that the identity provider doesn't understand, causing a CORS error.\n if (result?.data?.signInUrl) {\n window.location.href = result.data.signInUrl;\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response.\n if (result?.data?.afterSignInUrl) {\n router.push(result.data.afterSignInUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignUp = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signUp) {\n throw new ThunderIDRuntimeError(\n '`signUp` function is not available.',\n 'ThunderIDClientProvider-handleSignUp-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signUp(payload, request);\n\n // Redirect based flow URL is sent as `signUpUrl` in the response.\n if (result?.data?.signUpUrl) {\n router.push(result.data.signUpUrl);\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response.\n if (result?.data?.afterSignUpUrl) {\n router.push(result.data.afterSignUpUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignOut = async (): Promise<any> => {\n logger.debug('[ThunderIDClientProvider][handleSignOut] `handleSignOut` called.');\n\n try {\n const result: any = await signOut();\n\n logger.debug('[ThunderIDClientProvider][handleSignOut] Sign out result:', result);\n\n if (result?.data?.afterSignOutUrl) {\n router.push(result.data.afterSignOutUrl);\n\n return {location: result.data.afterSignOutUrl, redirected: true};\n }\n\n if (result?.error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error result was returned during signing the user out with a button click:',\n result.error,\n );\n }\n\n return result?.data ?? result;\n } catch (error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error occurred during signing the user out with a button click:',\n error,\n );\n\n return undefined;\n }\n };\n\n const contextValue: ThunderIDContextProps = useMemo(\n () => ({\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n preferences,\n refreshToken,\n scopes,\n signIn: handleSignIn,\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n }),\n [\n baseUrl,\n user,\n isSignedIn,\n isLoading,\n signInUrl,\n signUpUrl,\n applicationId,\n organizationHandle,\n scopes,\n preferences,\n ],\n );\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile) => ({\n ...prev,\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n // Bridge into @thunderid/react's own ThunderIDContext. Internal react components rendered by\n // BaseSignIn/BaseSignUp — most notably FlowMetaProvider, which fetches `/flow/meta` and injects\n // its i18n bundle — call react's `useThunderID()` directly. Without this bridge they'd only see\n // react's context default (applicationId/baseUrl undefined, isInitialized false), so the meta\n // fetch would never fire and flow labels would render as untranslated i18n keys. Fields with no\n // nextjs equivalent (token/http helpers meant for direct browser API calls) are stubbed out,\n // since nextjs routes those operations through server actions instead.\n const unsupported = (name: string): (() => Promise<never>) => {\n return () =>\n Promise.reject(\n new ThunderIDRuntimeError(\n `\\`${name}\\` is not supported in @thunderid/nextjs.`,\n `ThunderIDClientProvider-${name}-NotSupportedError-001`,\n 'nextjs',\n ),\n );\n };\n\n const reactContextValue: ReactThunderIDContextProps = useMemo(\n () => ({\n afterSignInUrl: undefined,\n applicationId,\n baseUrl,\n clearSession,\n clientId: undefined,\n discovery: {wellKnown: null},\n exchangeToken: unsupported('exchangeToken'),\n getAccessToken: unsupported('getAccessToken'),\n getDecodedIdToken: unsupported('getDecodedIdToken'),\n getIdToken: unsupported('getIdToken'),\n getStorageManager: () => Promise.resolve(null),\n http: {\n request: unsupported('http.request'),\n requestAll: unsupported('http.requestAll'),\n },\n instanceId: 0,\n isInitialized: !isLoading,\n isLoading,\n isMetaLoading: false,\n isSignedIn,\n meta: initialMeta ?? null,\n organizationHandle,\n reInitialize: unsupported('reInitialize'),\n recover: unsupported('recover'),\n resolveFlowTemplateLiterals: (text: string | undefined): string => text ?? '',\n scopes,\n signIn: handleSignIn,\n signInSilently: unsupported('signInSilently'),\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n userSchema,\n vendor: getVendorPrefix(vendor),\n }),\n [\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n scopes,\n signInUrl,\n signUpUrl,\n user,\n userSchema,\n initialMeta,\n vendor,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={contextValue}>\n <ReactThunderIDContext.Provider value={reactContextValue}>\n <I18nProvider preferences={preferences?.i18n}>\n <FlowMetaProvider\n enabled={preferences?.resolveFromMeta !== false}\n initialMeta={initialMeta}\n fetchMeta={fetchMeta}\n >\n <ThemeProvider theme={preferences?.theme?.overrides} mode={preferences?.theme?.mode}>\n <FlowProvider>\n <UserProvider\n profile={userProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n updateProfile={updateProfile}\n >\n {children}\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ReactThunderIDContext.Provider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDClientProvider;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ThunderIDProvider.js","names":["ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>>","reRenderCheckRef: RefObject<boolean>","router: AppRouterInstance","searchParams: ReadonlyURLSearchParams","code: string | null","state: string | null","result: {error?: string; redirectUrl?: string; success: boolean}","result: any","contextValue: ThunderIDContextProps","reactContextValue: ReactThunderIDContextProps","ThunderIDContext","ReactThunderIDContext"],"sources":["../../../../src/client/contexts/ThunderID/ThunderIDProvider.tsx"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use client';\n\nimport {\n AttributeSchema,\n EmbeddedFlowExecuteRequestConfig,\n FlowMetadataResponse,\n generateFlattenedUserProfile,\n UpdateMeProfileConfig,\n User,\n UserProfile,\n ThunderIDRuntimeError,\n getVendorPrefix,\n} from '@thunderid/node';\nimport {\n I18nProvider,\n FlowMetaProvider,\n FlowProvider,\n UserProvider,\n ThemeProvider,\n ThunderIDContext as ReactThunderIDContext,\n ThunderIDContextProps as ReactThunderIDContextProps,\n ThunderIDProviderProps,\n} from '@thunderid/react';\nimport {ReadonlyURLSearchParams} from 'next/dist/client/components/navigation.react-server';\nimport {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime';\nimport {useRouter, useSearchParams} from 'next/navigation';\nimport {FC, PropsWithChildren, RefObject, useEffect, useMemo, useRef, useState} from 'react';\nimport ThunderIDContext, {ThunderIDContextProps} from './ThunderIDContext';\nimport {RefreshResult} from '../../../server/actions/refreshToken';\nimport logger from '../../../utils/logger';\n\n/**\n * Props interface of {@link ThunderIDClientProvider}\n */\nexport type ThunderIDClientProviderProps = Partial<Omit<ThunderIDProviderProps, 'baseUrl' | 'clientId'>> &\n Pick<ThunderIDProviderProps, 'baseUrl' | 'clientId'> & {\n applicationId: ThunderIDContextProps['applicationId'];\n clearSession: () => Promise<void>;\n /**\n * Server Action that fetches `GET /flow/meta` server-side, passed down to `FlowMetaProvider` so\n * it never calls `baseUrl` directly from the browser — avoiding the CORS requirement that a\n * direct client-side fetch to the ThunderID server would otherwise impose. Used for both the\n * initial fetch (when `initialMeta` isn't seeded, e.g. it failed server-side) and subsequent\n * `switchLanguage()` calls.\n */\n fetchMeta?: (params: {applicationId?: string; language?: string}) => Promise<FlowMetadataResponse>;\n handleOAuthCallback: (\n code: string,\n state: string,\n ) => Promise<{error?: string; redirectUrl?: string; success: boolean}>;\n /**\n * Flow metadata fetched server-side ahead of time, seeding `FlowMetaProvider` so it can skip\n * its own initial client-side fetch (avoiding a flash of untranslated i18n keys).\n */\n initialMeta?: FlowMetadataResponse | null;\n isSignedIn: boolean;\n organizationHandle: ThunderIDContextProps['organizationHandle'];\n refreshToken: () => Promise<RefreshResult>;\n signIn: ThunderIDContextProps['signIn'];\n signOut: ThunderIDContextProps['signOut'];\n signUp: ThunderIDContextProps['signUp'];\n updateProfile: (\n requestConfig: UpdateMeProfileConfig,\n sessionId?: string,\n ) => Promise<{data: {user: User}; error: string; success: boolean}>;\n user: User | null;\n userProfile: UserProfile;\n userSchema?: Record<string, AttributeSchema> | null;\n };\n\nconst ThunderIDClientProvider: FC<PropsWithChildren<ThunderIDClientProviderProps>> = ({\n baseUrl,\n children,\n signIn,\n clearSession,\n refreshToken,\n signOut,\n signUp,\n handleOAuthCallback,\n preferences,\n isSignedIn,\n signInUrl,\n signUpUrl,\n user: _user,\n userProfile: _userProfile,\n userSchema: _userSchema = null,\n updateProfile,\n applicationId,\n organizationHandle,\n scopes,\n vendor,\n initialMeta = null,\n fetchMeta,\n}: PropsWithChildren<ThunderIDClientProviderProps>) => {\n const reRenderCheckRef: RefObject<boolean> = useRef(false);\n const router: AppRouterInstance = useRouter();\n const searchParams: ReadonlyURLSearchParams = useSearchParams();\n const [isLoading, setIsLoading] = useState<boolean>(true);\n const [user, setUser] = useState<User | null>(_user);\n const [userProfile, setUserProfile] = useState<UserProfile>(_userProfile);\n const [userSchema, setUserSchema] = useState<Record<string, AttributeSchema> | null>(_userSchema);\n\n useEffect(() => {\n setUserProfile(_userProfile);\n }, [_userProfile]);\n\n useEffect(() => {\n setUserSchema(_userSchema);\n }, [_userSchema]);\n\n useEffect(() => {\n setUser(_user);\n }, [_user]);\n\n // Handle OAuth callback automatically\n useEffect(() => {\n // React 18.x Strict.Mode has a new check for `Ensuring reusable state` to facilitate an upcoming react feature.\n // https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state\n // This will remount all the useEffects to ensure that there are no unexpected side effects.\n // When react remounts the signIn hook of the AuthProvider, it will cause a race condition. Hence, we have to\n // prevent the re-render of this hook as suggested in the following discussion.\n // https://github.com/reactwg/react-18/discussions/18#discussioncomment-795623\n if (reRenderCheckRef.current) {\n return;\n }\n\n reRenderCheckRef.current = true;\n\n // Don't handle callback if already signed in\n if (isSignedIn) return;\n\n (async (): Promise<void> => {\n try {\n const code: string | null = searchParams.get('code');\n const state: string | null = searchParams.get('state');\n const error: string | null = searchParams.get('error');\n\n // Check for OAuth errors first\n if (error) {\n logger.error('[ThunderIDClientProvider] An error was received for the initiated sign-in request.');\n\n return;\n }\n\n // Handle OAuth callback if code and state are present\n if (code && state) {\n setIsLoading(true);\n\n const result: {error?: string; redirectUrl?: string; success: boolean} = await handleOAuthCallback(\n code,\n state,\n );\n\n if (result.success) {\n // Redirect to the success URL\n if (result.redirectUrl) {\n router.push(result.redirectUrl);\n } else {\n // Refresh the page to update authentication state\n window.location.reload();\n }\n } else {\n logger.error(\n `[ThunderIDClientProvider] An error occurred while signing in: ${result.error || 'Authentication failed'}`,\n );\n }\n }\n } catch (error) {\n logger.error('[ThunderIDClientProvider] Failed to handle OAuth callback:', error);\n }\n })();\n }, []);\n\n useEffect(() => {\n // Set loading to false when server has resolved authentication state\n setIsLoading(false);\n }, [isSignedIn, user]);\n\n const handleSignIn = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signIn) {\n throw new ThunderIDRuntimeError(\n '`signIn` function is not available.',\n 'ThunderIDClientProvider-handleSignIn-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signIn(payload, request);\n\n // Redirect based flow URL is sent as `signInUrl` in the response.\n // Use window.location.href instead of router.push() — the OAuth authorization\n // endpoint is on an external server, and router.push() would send RSC fetch\n // headers that the identity provider doesn't understand, causing a CORS error.\n if (result?.data?.signInUrl) {\n window.location.href = result.data.signInUrl;\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response.\n if (result?.data?.afterSignInUrl) {\n router.push(result.data.afterSignInUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignUp = async (payload: any, request: EmbeddedFlowExecuteRequestConfig): Promise<any> => {\n if (!signUp) {\n throw new ThunderIDRuntimeError(\n '`signUp` function is not available.',\n 'ThunderIDClientProvider-handleSignUp-RuntimeError-001',\n 'nextjs',\n );\n }\n\n const result: any = await signUp(payload, request);\n\n // Redirect based flow URL is sent as `signUpUrl` in the response.\n if (result?.data?.signUpUrl) {\n router.push(result.data.signUpUrl);\n\n return undefined;\n }\n\n // After the Embedded flow is successful, the URL to navigate next is sent as `afterSignUpUrl` in the response.\n if (result?.data?.afterSignUpUrl) {\n router.push(result.data.afterSignUpUrl);\n\n return undefined;\n }\n\n if (result?.error) {\n throw new Error(result.error);\n }\n\n return result?.data ?? result;\n };\n\n const handleSignOut = async (): Promise<any> => {\n logger.debug('[ThunderIDClientProvider][handleSignOut] `handleSignOut` called.');\n\n try {\n const result: any = await signOut();\n\n logger.debug('[ThunderIDClientProvider][handleSignOut] Sign out result:', result);\n\n if (result?.data?.afterSignOutUrl) {\n router.push(result.data.afterSignOutUrl);\n\n return {location: result.data.afterSignOutUrl, redirected: true};\n }\n\n if (result?.error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error result was returned during signing the user out with a button click:',\n result.error,\n );\n }\n\n return result?.data ?? result;\n } catch (error) {\n logger.error(\n '[ThunderIDClientProvider][handleSignOut] Error occurred during signing the user out with a button click:',\n error,\n );\n\n return undefined;\n }\n };\n\n const contextValue: ThunderIDContextProps = useMemo(\n () => ({\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n preferences,\n refreshToken,\n scopes,\n signIn: handleSignIn,\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n }),\n [\n baseUrl,\n user,\n isSignedIn,\n isLoading,\n signInUrl,\n signUpUrl,\n applicationId,\n organizationHandle,\n scopes,\n preferences,\n ],\n );\n\n const handleProfileUpdate = (payload: User): void => {\n setUser(payload);\n setUserProfile((prev: UserProfile) => ({\n ...prev,\n flattenedProfile: generateFlattenedUserProfile(payload),\n profile: payload,\n }));\n };\n\n // Bridge into @thunderid/react's own ThunderIDContext. Internal react components rendered by\n // BaseSignIn/BaseSignUp — most notably FlowMetaProvider, which fetches `/flow/meta` and injects\n // its i18n bundle — call react's `useThunderID()` directly. Without this bridge they'd only see\n // react's context default (applicationId/baseUrl undefined, isInitialized false), so the meta\n // fetch would never fire and flow labels would render as untranslated i18n keys. Fields with no\n // nextjs equivalent (token/http helpers meant for direct browser API calls) are stubbed out,\n // since nextjs routes those operations through server actions instead.\n const unsupported = (name: string): (() => Promise<never>) => {\n return () =>\n Promise.reject(\n new ThunderIDRuntimeError(\n `\\`${name}\\` is not supported in @thunderid/nextjs.`,\n `ThunderIDClientProvider-${name}-NotSupportedError-001`,\n 'nextjs',\n ),\n );\n };\n\n const reactContextValue: ReactThunderIDContextProps = useMemo(\n () => ({\n afterSignInUrl: undefined,\n applicationId,\n baseUrl,\n clearSession,\n clientId: undefined,\n discovery: {wellKnown: null},\n exchangeToken: unsupported('exchangeToken'),\n getAccessToken: unsupported('getAccessToken'),\n getDecodedIdToken: unsupported('getDecodedIdToken'),\n getIdToken: unsupported('getIdToken'),\n getStorageManager: () => Promise.resolve(null),\n http: {\n request: unsupported('http.request'),\n requestAll: unsupported('http.requestAll'),\n },\n instanceId: 0,\n isInitialized: !isLoading,\n isLoading,\n isMetaLoading: false,\n isSignedIn,\n meta: initialMeta ?? null,\n organizationHandle,\n reInitialize: unsupported('reInitialize'),\n recover: unsupported('recover'),\n resolveFlowTemplateLiterals: (text: string | undefined): string => text ?? '',\n scopes,\n signIn: handleSignIn,\n signInSilently: unsupported('signInSilently'),\n signInUrl,\n signOut: handleSignOut,\n signUp: handleSignUp,\n signUpUrl,\n user,\n userSchema,\n vendor: getVendorPrefix(vendor),\n }),\n [\n applicationId,\n baseUrl,\n clearSession,\n isLoading,\n isSignedIn,\n organizationHandle,\n scopes,\n signInUrl,\n signUpUrl,\n user,\n userSchema,\n initialMeta,\n vendor,\n ],\n );\n\n return (\n <ThunderIDContext.Provider value={contextValue}>\n <ReactThunderIDContext.Provider value={reactContextValue}>\n <I18nProvider preferences={preferences?.i18n}>\n <FlowMetaProvider\n enabled={preferences?.resolveFromMeta !== false}\n initialMeta={initialMeta}\n fetchMeta={fetchMeta}\n >\n <ThemeProvider theme={preferences?.theme?.overrides} mode={preferences?.theme?.mode}>\n <FlowProvider>\n <UserProvider\n profile={userProfile}\n userSchema={userSchema}\n onUpdateProfile={handleProfileUpdate}\n updateProfile={updateProfile}\n >\n {children}\n </UserProvider>\n </FlowProvider>\n </ThemeProvider>\n </FlowMetaProvider>\n </I18nProvider>\n </ReactThunderIDContext.Provider>\n </ThunderIDContext.Provider>\n );\n};\n\nexport default ThunderIDClientProvider;\n"],"mappings":";;;;;;;;;;;;AAyEA,MAAMA,2BAAgF,EACpF,SACA,UACA,QACA,cACA,cACA,SACA,QACA,qBACA,aACA,YACA,WACA,WACA,MAAM,OACN,aAAa,cACb,YAAY,cAAc,MAC1B,eACA,eACA,oBACA,QACA,QACA,cAAc,MACd,gBACqD;CACrD,MAAMC,mBAAuC,OAAO,MAAM;CAC1D,MAAMC,SAA4B,WAAW;CAC7C,MAAMC,eAAwC,iBAAiB;CAC/D,MAAM,CAAC,WAAW,gBAAgB,SAAkB,KAAK;CACzD,MAAM,CAAC,MAAM,WAAW,SAAsB,MAAM;CACpD,MAAM,CAAC,aAAa,kBAAkB,SAAsB,aAAa;CACzE,MAAM,CAAC,YAAY,iBAAiB,SAAiD,YAAY;AAEjG,iBAAgB;AACd,iBAAe,aAAa;IAC3B,CAAC,aAAa,CAAC;AAElB,iBAAgB;AACd,gBAAc,YAAY;IACzB,CAAC,YAAY,CAAC;AAEjB,iBAAgB;AACd,UAAQ,MAAM;IACb,CAAC,MAAM,CAAC;AAGX,iBAAgB;AAOd,MAAI,iBAAiB,QACnB;AAGF,mBAAiB,UAAU;AAG3B,MAAI,WAAY;AAEhB,GAAC,YAA2B;AAC1B,OAAI;IACF,MAAMC,OAAsB,aAAa,IAAI,OAAO;IACpD,MAAMC,QAAuB,aAAa,IAAI,QAAQ;AAItD,QAH6B,aAAa,IAAI,QAAQ,EAG3C;AACT,oBAAO,MAAM,qFAAqF;AAElG;;AAIF,QAAI,QAAQ,OAAO;AACjB,kBAAa,KAAK;KAElB,MAAMC,SAAmE,MAAM,oBAC7E,MACA,MACD;AAED,SAAI,OAAO,QAET,KAAI,OAAO,YACT,QAAO,KAAK,OAAO,YAAY;SAG/B,QAAO,SAAS,QAAQ;SAG1B,gBAAO,MACL,iEAAiE,OAAO,SAAS,0BAClF;;YAGE,OAAO;AACd,mBAAO,MAAM,8DAA8D,MAAM;;MAEjF;IACH,EAAE,CAAC;AAEN,iBAAgB;AAEd,eAAa,MAAM;IAClB,CAAC,YAAY,KAAK,CAAC;CAEtB,MAAM,eAAe,OAAO,SAAc,YAA4D;AACpG,MAAI,CAAC,OACH,OAAM,IAAI,sBACR,uCACA,yDACA,SACD;EAGH,MAAMC,SAAc,MAAM,OAAO,SAAS,QAAQ;AAMlD,MAAI,QAAQ,MAAM,WAAW;AAC3B,UAAO,SAAS,OAAO,OAAO,KAAK;AAEnC;;AAIF,MAAI,QAAQ,MAAM,gBAAgB;AAChC,UAAO,KAAK,OAAO,KAAK,eAAe;AAEvC;;AAGF,MAAI,QAAQ,MACV,OAAM,IAAI,MAAM,OAAO,MAAM;AAG/B,SAAO,QAAQ,QAAQ;;CAGzB,MAAM,eAAe,OAAO,SAAc,YAA4D;AACpG,MAAI,CAAC,OACH,OAAM,IAAI,sBACR,uCACA,yDACA,SACD;EAGH,MAAMA,SAAc,MAAM,OAAO,SAAS,QAAQ;AAGlD,MAAI,QAAQ,MAAM,WAAW;AAC3B,UAAO,KAAK,OAAO,KAAK,UAAU;AAElC;;AAIF,MAAI,QAAQ,MAAM,gBAAgB;AAChC,UAAO,KAAK,OAAO,KAAK,eAAe;AAEvC;;AAGF,MAAI,QAAQ,MACV,OAAM,IAAI,MAAM,OAAO,MAAM;AAG/B,SAAO,QAAQ,QAAQ;;CAGzB,MAAM,gBAAgB,YAA0B;AAC9C,iBAAO,MAAM,mEAAmE;AAEhF,MAAI;GACF,MAAMA,SAAc,MAAM,SAAS;AAEnC,kBAAO,MAAM,6DAA6D,OAAO;AAEjF,OAAI,QAAQ,MAAM,iBAAiB;AACjC,WAAO,KAAK,OAAO,KAAK,gBAAgB;AAExC,WAAO;KAAC,UAAU,OAAO,KAAK;KAAiB,YAAY;KAAK;;AAGlE,OAAI,QAAQ,MACV,gBAAO,MACL,uHACA,OAAO,MACR;AAGH,UAAO,QAAQ,QAAQ;WAChB,OAAO;AACd,kBAAO,MACL,4GACA,MACD;AAED;;;CAIJ,MAAMC,eAAsC,eACnC;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,QAAQ;EACR;EACA,SAAS;EACT,QAAQ;EACR;EACA;EACD,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;CAED,MAAM,uBAAuB,YAAwB;AACnD,UAAQ,QAAQ;AAChB,kBAAgB,UAAuB;GACrC,GAAG;GACH,kBAAkB,6BAA6B,QAAQ;GACvD,SAAS;GACV,EAAE;;CAUL,MAAM,eAAe,SAAyC;AAC5D,eACE,QAAQ,OACN,IAAI,sBACF,KAAK,KAAK,4CACV,2BAA2B,KAAK,yBAChC,SACD,CACF;;CAGL,MAAMC,oBAAgD,eAC7C;EACL,gBAAgB;EAChB;EACA;EACA;EACA,UAAU;EACV,WAAW,EAAC,WAAW,MAAK;EAC5B,eAAe,YAAY,gBAAgB;EAC3C,gBAAgB,YAAY,iBAAiB;EAC7C,mBAAmB,YAAY,oBAAoB;EACnD,YAAY,YAAY,aAAa;EACrC,yBAAyB,QAAQ,QAAQ,KAAK;EAC9C,MAAM;GACJ,SAAS,YAAY,eAAe;GACpC,YAAY,YAAY,kBAAkB;GAC3C;EACD,YAAY;EACZ,eAAe,CAAC;EAChB;EACA,eAAe;EACf;EACA,MAAM,eAAe;EACrB;EACA,cAAc,YAAY,eAAe;EACzC,SAAS,YAAY,UAAU;EAC/B,8BAA8B,SAAqC,QAAQ;EAC3E;EACA,QAAQ;EACR,gBAAgB,YAAY,iBAAiB;EAC7C;EACA,SAAS;EACT,QAAQ;EACR;EACA;EACA;EACA,QAAQ,gBAAgB,OAAO;EAChC,GACD;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACD,CACF;AAED,QACE,oBAACC,yBAAiB;EAAS,OAAO;YAChC,oBAACC,iBAAsB;GAAS,OAAO;aACrC,oBAAC;IAAa,aAAa,aAAa;cACtC,oBAAC;KACC,SAAS,aAAa,oBAAoB;KAC7B;KACF;eAEX,oBAAC;MAAc,OAAO,aAAa,OAAO;MAAW,MAAM,aAAa,OAAO;gBAC7E,oBAAC,0BACC,oBAAC;OACC,SAAS;OACG;OACZ,iBAAiB;OACF;OAEd;QACY,GACF;OACD;MACC;KACN;IACgB;GACP;;AAIhC,gCAAe"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import thunderid_default from "./server/thunderid.js";
|
|
|
3
3
|
import ThunderIDProvider_default from "./server/ThunderIDProvider.js";
|
|
4
4
|
import thunderIDProxy_default, { removeChunkedCookieFromHeader, replaceChunkedCookieInHeader } from "./server/proxy/thunderIDProxy.js";
|
|
5
5
|
import createRouteMatcher_default from "./server/proxy/createRouteMatcher.js";
|
|
6
|
+
import backchannelLogout_default from "./server/backchannelLogout.js";
|
|
6
7
|
import "./server/index.js";
|
|
7
8
|
import useThunderID_default from "./client/contexts/ThunderID/useThunderID.js";
|
|
8
9
|
import SignedIn_default from "./client/components/control/SignedIn/SignedIn.js";
|
|
@@ -19,4 +20,4 @@ import UserAvatar_default from "./client/components/presentation/UserAvatar/User
|
|
|
19
20
|
import ChangeCredential_default from "./client/components/presentation/ChangeCredential/ChangeCredential.js";
|
|
20
21
|
import "./client/index.js";
|
|
21
22
|
|
|
22
|
-
export { ChangeCredential_default as ChangeCredential, SignIn_default as SignIn, SignInButton_default as SignInButton, SignOutButton_default as SignOutButton, SignUp_default as SignUp, SignUpButton_default as SignUpButton, SignedIn_default as SignedIn, SignedOut_default as SignedOut, ThunderIDNextClient_default as ThunderIDNext, ThunderIDProvider_default as ThunderIDProvider, User_default as User, UserAvatar_default as UserAvatar, UserDropdown_default as UserDropdown, UserProfile_default as UserProfile, createRouteMatcher_default as createRouteMatcher, removeChunkedCookieFromHeader, replaceChunkedCookieInHeader, thunderIDProxy_default as thunderIDProxy, thunderid_default as thunderid, useThunderID_default as useThunderID };
|
|
23
|
+
export { ChangeCredential_default as ChangeCredential, SignIn_default as SignIn, SignInButton_default as SignInButton, SignOutButton_default as SignOutButton, SignUp_default as SignUp, SignUpButton_default as SignUpButton, SignedIn_default as SignedIn, SignedOut_default as SignedOut, ThunderIDNextClient_default as ThunderIDNext, ThunderIDProvider_default as ThunderIDProvider, User_default as User, UserAvatar_default as UserAvatar, UserDropdown_default as UserDropdown, UserProfile_default as UserProfile, backchannelLogout_default as backchannelLogout, createRouteMatcher_default as createRouteMatcher, removeChunkedCookieFromHeader, replaceChunkedCookieInHeader, thunderIDProxy_default as thunderIDProxy, thunderid_default as thunderid, useThunderID_default as useThunderID };
|
|
@@ -5,10 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @param code - Authorization code from OAuth provider
|
|
7
7
|
* @param state - State parameter from OAuth provider for CSRF protection
|
|
8
|
-
* @param sessionState - Session state parameter from OAuth provider
|
|
9
8
|
* @returns Promise that resolves with success status and optional error message
|
|
10
9
|
*/
|
|
11
|
-
declare const handleOAuthCallbackAction: (code: string, state: string
|
|
10
|
+
declare const handleOAuthCallbackAction: (code: string, state: string) => Promise<{
|
|
12
11
|
error?: string;
|
|
13
12
|
redirectUrl?: string;
|
|
14
13
|
success: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handleOAuthCallbackAction.d.ts","sourceRoot":"","sources":["../../../src/server/actions/handleOAuthCallbackAction.ts"],"names":[],"mappings":"AAeA
|
|
1
|
+
{"version":3,"file":"handleOAuthCallbackAction.d.ts","sourceRoot":"","sources":["../../../src/server/actions/handleOAuthCallbackAction.ts"],"names":[],"mappings":"AAeA;;;;;;;;GAQG;AACH,QAAA,MAAM,yBAAyB,GAC7B,MAAM,MAAM,EACZ,OAAO,MAAM,KACZ,OAAO,CAAC;IACT,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CA0HA,CAAC;AAEF,eAAe,yBAAyB,CAAC"}
|
|
@@ -15,10 +15,9 @@ import { cookies } from "next/headers";
|
|
|
15
15
|
*
|
|
16
16
|
* @param code - Authorization code from OAuth provider
|
|
17
17
|
* @param state - State parameter from OAuth provider for CSRF protection
|
|
18
|
-
* @param sessionState - Session state parameter from OAuth provider
|
|
19
18
|
* @returns Promise that resolves with success status and optional error message
|
|
20
19
|
*/
|
|
21
|
-
const handleOAuthCallbackAction = async (code, state
|
|
20
|
+
const handleOAuthCallbackAction = async (code, state) => {
|
|
22
21
|
try {
|
|
23
22
|
if (!code || !state) return {
|
|
24
23
|
error: "Missing required OAuth parameters: code and state are required",
|
|
@@ -46,7 +45,6 @@ const handleOAuthCallbackAction = async (code, state, sessionState) => {
|
|
|
46
45
|
}
|
|
47
46
|
const signInResult = await thunderIDClient.signIn({
|
|
48
47
|
code,
|
|
49
|
-
session_state: sessionState,
|
|
50
48
|
state
|
|
51
49
|
}, {}, sessionId);
|
|
52
50
|
const config = await thunderIDClient.getConfiguration();
|
|
@@ -59,7 +57,7 @@ const handleOAuthCallbackAction = async (code, state, sessionState) => {
|
|
|
59
57
|
const organizationId = idToken["user_org"] || idToken["organization_id"];
|
|
60
58
|
const expiresIn = signInResult["expiresIn"];
|
|
61
59
|
const sessionCookieExpiryTime = SessionManager_default.resolveSessionCookieExpiry(config.sessionCookie?.expiryTime);
|
|
62
|
-
const sessionToken = await SessionManager_default.createSessionToken(accessToken, userIdFromToken, sessionId, scopes, expiresIn, refreshToken, organizationId);
|
|
60
|
+
const sessionToken = await SessionManager_default.createSessionToken(accessToken, userIdFromToken, sessionId, scopes, expiresIn, refreshToken, organizationId, { sid: typeof idToken["sid"] === "string" ? idToken["sid"] : void 0 });
|
|
63
61
|
setChunkedCookie(cookieStore, SessionManager_default.getSessionCookieName(), sessionToken, SessionManager_default.getSessionCookieOptions(sessionCookieExpiryTime));
|
|
64
62
|
cookieStore.delete(SessionManager_default.getTempSessionCookieName());
|
|
65
63
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handleOAuthCallbackAction.js","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","tempSessionToken: string | undefined","SessionManager","signInResult: Record<string, unknown>","config: ThunderIDNextConfig","idToken: IdToken","accessToken: string","refreshToken: string","userIdFromToken: string","scopes: string","organizationId: string | undefined","expiresIn: number","sessionCookieExpiryTime: number","sessionToken: string"],"sources":["../../../src/server/actions/handleOAuthCallbackAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {IdToken} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action to handle OAuth callback with authorization code.\n * This action processes the authorization code received from the OAuth provider\n * and exchanges it for tokens to complete the authentication flow.\n *\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter from OAuth provider for CSRF protection\n * @
|
|
1
|
+
{"version":3,"file":"handleOAuthCallbackAction.js","names":["getClient","cookieStore: RequestCookies","sessionId: string | undefined","tempSessionToken: string | undefined","SessionManager","signInResult: Record<string, unknown>","config: ThunderIDNextConfig","idToken: IdToken","accessToken: string","refreshToken: string","userIdFromToken: string","scopes: string","organizationId: string | undefined","expiresIn: number","sessionCookieExpiryTime: number","sessionToken: string"],"sources":["../../../src/server/actions/handleOAuthCallbackAction.ts"],"sourcesContent":["// Copyright 2025 The ThunderID Authors\n// SPDX-License-Identifier: Apache-2.0\n\n'use server';\n\nimport {IdToken} from '@thunderid/node';\nimport {cookies} from 'next/headers';\nimport {ThunderIDNextConfig} from '../../models/config';\nimport {setChunkedCookie} from '../../utils/chunkedCookie';\nimport logger from '../../utils/logger';\nimport SessionManager from '../../utils/SessionManager';\nimport getClient from '../getClient';\n\ntype RequestCookies = Awaited<ReturnType<typeof cookies>>;\n\n/**\n * Server action to handle OAuth callback with authorization code.\n * This action processes the authorization code received from the OAuth provider\n * and exchanges it for tokens to complete the authentication flow.\n *\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter from OAuth provider for CSRF protection\n * @returns Promise that resolves with success status and optional error message\n */\nconst handleOAuthCallbackAction = async (\n code: string,\n state: string,\n): Promise<{\n error?: string;\n redirectUrl?: string;\n success: boolean;\n}> => {\n try {\n if (!code || !state) {\n return {\n error: 'Missing required OAuth parameters: code and state are required',\n success: false,\n };\n }\n\n const thunderIDClient = getClient();\n\n if (!thunderIDClient.isInitialized) {\n return {\n error: 'ThunderID client is not initialized',\n success: false,\n };\n }\n\n const cookieStore: RequestCookies = await cookies();\n let sessionId: string | undefined;\n\n const tempSessionToken: string | undefined = cookieStore.get(SessionManager.getTempSessionCookieName())?.value;\n\n if (tempSessionToken) {\n try {\n const tempSession: {sessionId: string} = await SessionManager.verifyTempSession(tempSessionToken);\n sessionId = tempSession.sessionId;\n } catch {\n logger.error(\n '[handleOAuthCallbackAction] Invalid temporary session token, falling back to session ID from cookies.',\n );\n }\n }\n\n if (!sessionId) {\n logger.error('[handleOAuthCallbackAction] No session ID found in cookies or temporary session token.');\n\n return {\n error: 'No session found. Please start the authentication flow again.',\n success: false,\n };\n }\n\n // Exchange the authorization code for tokens\n const signInResult: Record<string, unknown> = await thunderIDClient.signIn(\n {\n code,\n state,\n } as any,\n {},\n sessionId,\n );\n\n const config: ThunderIDNextConfig = await thunderIDClient.getConfiguration();\n\n if (signInResult) {\n try {\n const idToken: IdToken = await thunderIDClient.getDecodedIdToken(\n sessionId,\n (signInResult['id_token'] || signInResult['idToken']) as string,\n );\n const accessToken: string = (signInResult['accessToken'] || signInResult['access_token']) as string;\n const refreshToken: string = (signInResult['refreshToken'] as string | undefined) ?? '';\n const userIdFromToken: string = (idToken.sub || signInResult['sub'] || sessionId) as string;\n const scopes: string = signInResult['scope'] as string;\n const organizationId: string | undefined = (idToken['user_org'] || idToken['organization_id']) as\n | string\n | undefined;\n const expiresIn: number = signInResult['expiresIn'] as number;\n const sessionCookieExpiryTime: number = SessionManager.resolveSessionCookieExpiry(\n config.sessionCookie?.expiryTime,\n );\n\n const sessionToken: string = await SessionManager.createSessionToken(\n accessToken,\n userIdFromToken,\n sessionId,\n scopes,\n expiresIn,\n refreshToken,\n organizationId,\n {sid: typeof idToken['sid'] === 'string' ? idToken['sid'] : undefined},\n );\n\n setChunkedCookie(\n cookieStore,\n SessionManager.getSessionCookieName(),\n sessionToken,\n SessionManager.getSessionCookieOptions(sessionCookieExpiryTime),\n );\n\n cookieStore.delete(SessionManager.getTempSessionCookieName());\n } catch (error) {\n logger.error(\n `[handleOAuthCallbackAction] Failed to create JWT session, continuing with legacy session:\n ${typeof error === 'string' ? error : JSON.stringify(error)}`,\n );\n }\n }\n\n const afterSignInUrl: string = config.afterSignInUrl || '/';\n\n return {\n redirectUrl: afterSignInUrl,\n success: true,\n };\n } catch (error) {\n let errorMessage = 'Authentication failed';\n\n if (error instanceof Error) {\n errorMessage = error.message;\n } else if (error && typeof error === 'object' && 'message' in error) {\n errorMessage = String((error as {message: unknown}).message);\n } else if (typeof error === 'string') {\n errorMessage = error;\n }\n\n return {\n error: errorMessage,\n success: false,\n };\n }\n};\n\nexport default handleOAuthCallbackAction;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAwBA,MAAM,4BAA4B,OAChC,MACA,UAKI;AACJ,KAAI;AACF,MAAI,CAAC,QAAQ,CAAC,MACZ,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAM,kBAAkBA,mBAAW;AAEnC,MAAI,CAAC,gBAAgB,cACnB,QAAO;GACL,OAAO;GACP,SAAS;GACV;EAGH,MAAMC,cAA8B,MAAM,SAAS;EACnD,IAAIC;EAEJ,MAAMC,mBAAuC,YAAY,IAAIC,uBAAe,0BAA0B,CAAC,EAAE;AAEzG,MAAI,iBACF,KAAI;AAEF,gBADyC,MAAMA,uBAAe,kBAAkB,iBAAiB,EACzE;UAClB;AACN,kBAAO,MACL,wGACD;;AAIL,MAAI,CAAC,WAAW;AACd,kBAAO,MAAM,yFAAyF;AAEtG,UAAO;IACL,OAAO;IACP,SAAS;IACV;;EAIH,MAAMC,eAAwC,MAAM,gBAAgB,OAClE;GACE;GACA;GACD,EACD,EAAE,EACF,UACD;EAED,MAAMC,SAA8B,MAAM,gBAAgB,kBAAkB;AAE5E,MAAI,aACF,KAAI;GACF,MAAMC,UAAmB,MAAM,gBAAgB,kBAC7C,WACC,aAAa,eAAe,aAAa,WAC3C;GACD,MAAMC,cAAuB,aAAa,kBAAkB,aAAa;GACzE,MAAMC,eAAwB,aAAa,mBAA0C;GACrF,MAAMC,kBAA2B,QAAQ,OAAO,aAAa,UAAU;GACvE,MAAMC,SAAiB,aAAa;GACpC,MAAMC,iBAAsC,QAAQ,eAAe,QAAQ;GAG3E,MAAMC,YAAoB,aAAa;GACvC,MAAMC,0BAAkCV,uBAAe,2BACrD,OAAO,eAAe,WACvB;GAED,MAAMW,eAAuB,MAAMX,uBAAe,mBAChD,aACA,iBACA,WACA,QACA,WACA,cACA,gBACA,EAAC,KAAK,OAAO,QAAQ,WAAW,WAAW,QAAQ,SAAS,QAAU,CACvE;AAED,oBACE,aACAA,uBAAe,sBAAsB,EACrC,cACAA,uBAAe,wBAAwB,wBAAwB,CAChE;AAED,eAAY,OAAOA,uBAAe,0BAA0B,CAAC;WACtD,OAAO;AACd,kBAAO,MACL;YACE,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,MAAM,GAC5D;;AAML,SAAO;GACL,aAH6B,OAAO,kBAAkB;GAItD,SAAS;GACV;UACM,OAAO;EACd,IAAI,eAAe;AAEnB,MAAI,iBAAiB,MACnB,gBAAe,MAAM;WACZ,SAAS,OAAO,UAAU,YAAY,aAAa,MAC5D,gBAAe,OAAQ,MAA6B,QAAQ;WACnD,OAAO,UAAU,SAC1B,gBAAe;AAGjB,SAAO;GACL,OAAO;GACP,SAAS;GACV;;;AAIL,wCAAe"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signInAction.d.ts","sourceRoot":"","sources":["../../../src/server/actions/signInAction.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,gCAAgC,EAGjC,MAAM,iBAAiB,CAAC;AAUzB;;;;;;;GAOG;AACH,QAAA,MAAM,YAAY,GAChB,UAAU,GAAG,EACb,UAAU,gCAAgC,KACzC,OAAO,CAAC;IACT,IAAI,CAAC,EACD;QACE,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,
|
|
1
|
+
{"version":3,"file":"signInAction.d.ts","sourceRoot":"","sources":["../../../src/server/actions/signInAction.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,gCAAgC,EAGjC,MAAM,iBAAiB,CAAC;AAUzB;;;;;;;GAOG;AACH,QAAA,MAAM,YAAY,GAChB,UAAU,GAAG,EACb,UAAU,gCAAgC,KACzC,OAAO,CAAC;IACT,IAAI,CAAC,EACD;QACE,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GACD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB,CAkHA,CAAC;AAEF,eAAe,YAAY,CAAC"}
|
|
@@ -56,7 +56,7 @@ const signInAction = async (payload, request) => {
|
|
|
56
56
|
const expiresIn = Math.max(exp - iat, 0);
|
|
57
57
|
const config = await client.getConfiguration();
|
|
58
58
|
const sessionCookieExpiryTime = SessionManager_default.resolveSessionCookieExpiry(config.sessionCookie?.expiryTime);
|
|
59
|
-
const sessionToken = await SessionManager_default.createSessionToken(response.assertion, userIdFromToken, sessionId, scopes, expiresIn, "", organizationId);
|
|
59
|
+
const sessionToken = await SessionManager_default.createSessionToken(response.assertion, userIdFromToken, sessionId, scopes, expiresIn, "", organizationId, { sid: typeof idToken["sid"] === "string" ? idToken["sid"] : void 0 });
|
|
60
60
|
setChunkedCookie(cookieStore, SessionManager_default.getSessionCookieName(), sessionToken, SessionManager_default.getSessionCookieOptions(sessionCookieExpiryTime));
|
|
61
61
|
cookieStore.delete(SessionManager_default.getTempSessionCookieName());
|
|
62
62
|
const afterSignInUrl = await (await client.getStorageManager()).getConfigDataParameter("afterSignInUrl");
|