@eventuras/fides-auth 0.11.0 → 0.12.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.
@@ -1 +1 @@
1
- {"version":3,"file":"heartbeat-handler.d.ts","sourceRoot":"","sources":["../../src/server/heartbeat-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IAEzB,wCAAwC;IACxC,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAwCnB"}
1
+ {"version":3,"file":"heartbeat-handler.d.ts","sourceRoot":"","sources":["../../src/server/heartbeat-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IAEzB,wCAAwC;IACxC,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CA0CnB"}
@@ -35,8 +35,8 @@ async function persistSession(store, session, secret) {
35
35
  }
36
36
  /**
37
37
  * Reads and reassembles the current session from the cookie store, or null when
38
- * there is no valid session. All the split/expiry/legacy handling lives in
39
- * {@link decodeSessionCookies}; this just supplies the cookie values.
38
+ * there is none. Decode/validation rules live in {@link decodeSessionCookies};
39
+ * the returned session may carry an expired access token.
40
40
  */
41
41
  async function readSession(store, secret) {
42
42
  try {
@@ -82,9 +82,8 @@ async function refreshSessionInStore(store, config, secret, options = {}) {
82
82
  }
83
83
  }
84
84
  /**
85
- * Reads the raw ID token, whether or not the session is still valid. Logout needs
86
- * the hint after the access token has expired, which is exactly when
87
- * {@link readSession} returns null.
85
+ * Reads the raw ID token independently of the session cookies — logout needs
86
+ * the hint even when {@link readSession} returns null.
88
87
  */
89
88
  async function readIdToken(store, secret) {
90
89
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/server/session.ts","../../src/server/oidc-login.ts","../../src/server/oidc-callback.ts","../../src/server/oidc-logout.ts","../../src/server/heartbeat-handler.ts"],"sourcesContent":["// server/session.ts\n//\n// Framework-agnostic session persistence over a {@link CookieStore}. The split\n// encode/decode lives in `../session-cookies`; these helpers wire it to a cookie\n// store, apply the per-cookie size guard, and own the refresh flow — with no\n// dependency on any framework's cookie API.\n\nimport {\n ACCESS_TOKEN_COOKIE_NAME,\n COOKIE_INFO_BYTES,\n ID_TOKEN_COOKIE_NAME,\n SESSION_COOKIE_NAME,\n assertCookieWithinLimit,\n defaultSessionCookieOptions,\n} from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { getOAuthErrorLogContext } from '../oauth-logging';\nimport { refreshSession } from '../session-refresh';\nimport {\n decodeIdTokenCookie,\n decodeSessionCookies,\n encodeSessionCookies,\n} from '../session-cookies';\nimport type { CreateSessionOptions, Session } from '../types';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:session' });\n\ntype Secret = string | Uint8Array;\n\n/** Applies the browser size guard to one cookie value; logs as it nears the limit. */\nfunction checkSize(name: string, value: string): void {\n // Fail loudly above the browser per-cookie limit (the browser would otherwise\n // drop the cookie silently, producing a broken login).\n const size = assertCookieWithinLimit(name, value);\n if (size >= COOKIE_INFO_BYTES) {\n logger.info({ cookieName: name, size }, 'Cookie approaching browser size limit');\n }\n}\n\n/**\n * Encrypts a session and writes it across the \"session\", \"session_at\" and\n * \"session_it\" cookies, clearing the split-out cookies the session has no token for.\n *\n * @returns The encrypted JWT stored in the main \"session\" cookie.\n */\nexport async function persistSession(\n store: CookieStore,\n session: Session,\n secret: Secret,\n): Promise<string> {\n const encoded = await encodeSessionCookies(session, secret);\n\n const values: Array<[string, string | undefined]> = [\n [SESSION_COOKIE_NAME, encoded.session],\n [ACCESS_TOKEN_COOKIE_NAME, encoded.accessToken],\n [ID_TOKEN_COOKIE_NAME, encoded.idToken],\n ];\n\n // Size-check everything before touching the store. Throwing part-way through\n // would leave one user's session cookie next to another user's tokens — the\n // caller sees an error while the browser holds a working, mixed-up session.\n for (const [name, value] of values) {\n if (value) checkSize(name, value);\n }\n\n for (const [name, value] of values) {\n if (value) {\n await store.set(name, value, defaultSessionCookieOptions);\n } else {\n await store.delete(name);\n }\n }\n\n return encoded.session;\n}\n\n/**\n * Reads and reassembles the current session from the cookie store, or null when\n * there is no valid session. All the split/expiry/legacy handling lives in\n * {@link decodeSessionCookies}; this just supplies the cookie values.\n */\nexport async function readSession(store: CookieStore, secret: Secret): Promise<Session | null> {\n try {\n return await decodeSessionCookies(\n {\n session: (await store.get(SESSION_COOKIE_NAME)) ?? null,\n accessToken: (await store.get(ACCESS_TOKEN_COOKIE_NAME)) ?? null,\n idToken: (await store.get(ID_TOKEN_COOKIE_NAME)) ?? null,\n },\n secret,\n );\n } catch (error) {\n // Worker thread errors (e.g. from the crypto worker) should not crash the app.\n if (error instanceof Error && error.message.includes('worker')) {\n logger.error({ error }, 'Worker thread error reading session');\n } else {\n logger.error({ error }, 'Unexpected error reading session');\n }\n return null;\n }\n}\n\n/**\n * Refreshes the stored session using its refresh token and persists the result.\n * Returns the updated session, or null when there is nothing to refresh or the\n * refresh token is no longer valid.\n */\nexport async function refreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n secret: Secret,\n options: CreateSessionOptions = {},\n): Promise<Session | null> {\n try {\n const current = await readSession(store, secret);\n\n if (!current) {\n logger.warn('No current session to refresh');\n return null;\n }\n if (!current.tokens?.refreshToken) {\n logger.error('Current session has no refresh token');\n return null;\n }\n\n const updated = await refreshSession(current, config, options);\n if (!updated) {\n logger.error('Session refresh returned null');\n return null;\n }\n\n await persistSession(store, updated, secret);\n return updated;\n } catch (error) {\n // invalid_grant is expected during logout/session expiry — log it quietly.\n // getOAuthErrorLogContext unwraps errors (fields may live on `cause`), which a\n // direct property read would miss and misclassify.\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n logger.info('Session refresh failed - refresh token expired or invalid');\n } else {\n logger.error({ error: errorContext }, 'Failed to refresh session');\n }\n return null;\n }\n}\n\n/**\n * Reads the raw ID token, whether or not the session is still valid. Logout needs\n * the hint after the access token has expired, which is exactly when\n * {@link readSession} returns null.\n */\nexport async function readIdToken(store: CookieStore, secret: Secret): Promise<string | undefined> {\n try {\n const raw = await store.get(ID_TOKEN_COOKIE_NAME);\n return raw ? await decodeIdTokenCookie(raw, secret) : undefined;\n } catch (error) {\n logger.error({ error }, 'Unexpected error reading ID token');\n return undefined;\n }\n}\n\n/** Deletes all session cookies. */\nexport async function clearSession(store: CookieStore): Promise<void> {\n await store.delete(SESSION_COOKIE_NAME);\n await store.delete(ACCESS_TOKEN_COOKIE_NAME);\n await store.delete(ID_TOKEN_COOKIE_NAME);\n}\n","// server/oidc-login.ts\n//\n// Framework-agnostic OIDC login initiation. Builds PKCE parameters, stores them\n// in the cookie store, and redirects to the provider's authorization endpoint.\n// Uses the standard Request/Response, a CookieStore, and an optional rate-limit\n// callback, so any server runtime can call it.\n\nimport { defaultOAuthCookieOptions } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildPKCEOptions, discoverAndBuildAuthorizationUrl } from '../oauth';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-login' });\n\nexport interface OidcLoginConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store used to persist the PKCE state. */\n cookies: CookieStore;\n\n /**\n * Optional rate-limit gate. When it resolves false, the handler responds 429.\n */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /**\n * Validates and sanitizes the returnTo parameter, returning a safe path or null.\n * @default Only allows relative paths starting with a single `/`.\n */\n validateReturnTo?: (rawReturnTo: string) => string | null;\n}\n\nconst defaultValidateReturnTo = (raw: string): string | null =>\n /^\\/(?!\\/)/.test(raw) ? raw : null;\n\n/**\n * Handles OIDC login initiation: rate-limit, build PKCE, persist it, and redirect\n * to the authorization endpoint.\n */\nexport async function handleOidcLogin(\n request: Request,\n config: OidcLoginConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, validateReturnTo = defaultValidateReturnTo } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n const url = new URL(request.url);\n const rawReturnTo = url.searchParams.get('returnTo');\n const returnTo = rawReturnTo ? validateReturnTo(rawReturnTo) : null;\n\n const pkce = await buildPKCEOptions(oauthConfig);\n const authorizationUrl = await discoverAndBuildAuthorizationUrl(oauthConfig, pkce);\n\n logger.info('Redirecting to OIDC provider for login');\n\n await cookies.set('oauth_state', pkce.state, defaultOAuthCookieOptions);\n await cookies.set('oauth_code_verifier', pkce.code_verifier, defaultOAuthCookieOptions);\n if (returnTo) {\n await cookies.set('returnTo', returnTo, defaultOAuthCookieOptions);\n } else {\n // Clear any stale returnTo from an earlier, abandoned login so this one's\n // post-login redirect is deterministic rather than inheriting a past path.\n await cookies.delete('returnTo');\n }\n\n return new Response(null, {\n status: 302,\n headers: { Location: authorizationUrl.toString() },\n });\n}\n","// server/oidc-callback.ts\n//\n// Framework-agnostic OIDC callback handler. Orchestrates the OIDC concerns —\n// rate limit, PKCE cookie read, code exchange, session persistence, safe redirect\n// — over the standard Request/Response and a CookieStore, delegating the protocol\n// to the core oauth module and persistence to the server session helpers.\n\nimport { CookieTooLargeError } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport {\n buildSessionFromTokens,\n exchangeAuthorizationCode,\n validateReturnUrl,\n} from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { persistSession } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-callback' });\n\nexport interface OidcCallbackConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Public application URL (used to reconstruct the callback URL and validate redirects). */\n applicationUrl: string;\n\n /** Cookie store holding the PKCE state and receiving the session cookies. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /**\n * Name of the ID token claim that contains user roles.\n * @default 'roles'\n */\n rolesClaim?: string;\n\n /**\n * Default path to redirect to after login if no returnTo cookie is set.\n * @default '/'\n */\n defaultRedirectPath?: string;\n\n /**\n * Optional validator for the returnTo path. If omitted, only same-origin paths\n * are allowed.\n */\n validateReturnTo?: (path: string) => boolean;\n}\n\n/**\n * Handles the OIDC callback: rate-limit, exchange the code for tokens, persist the\n * session across the cookie store, and redirect to a validated returnTo.\n */\nexport async function handleOidcCallback(\n request: Request,\n config: OidcCallbackConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n applicationUrl,\n cookies,\n rateLimit,\n secret = getSessionSecret(),\n rolesClaim = 'roles',\n defaultRedirectPath = '/',\n validateReturnTo,\n } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n try {\n // Reconstruct the public callback URL. Behind a TLS-terminating proxy\n // applicationUrl is just the origin (\"/\"), and the token-exchange redirect_uri\n // must match the callback path used at authorize — so keep the request path.\n const currentUrl = new URL(request.url);\n const publicUrl = new URL(applicationUrl);\n publicUrl.pathname = currentUrl.pathname;\n publicUrl.search = currentUrl.search;\n\n const storedState = await cookies.get('oauth_state');\n const storedCodeVerifier = await cookies.get('oauth_code_verifier');\n\n if (!storedState || !storedCodeVerifier) {\n logger.warn('Missing state or code verifier');\n return new Response('Please restart the login process.', { status: 400 });\n }\n\n const tokens = await exchangeAuthorizationCode(\n oauthConfig,\n publicUrl,\n storedCodeVerifier,\n storedState,\n );\n\n // Build the session and persist it (split across session/session_at).\n const session = buildSessionFromTokens(tokens, rolesClaim);\n await persistSession(cookies, session, secret);\n\n // Clean up PKCE & returnTo cookies (read returnTo first).\n const returnTo = await cookies.get('returnTo');\n await cookies.delete('oauth_state');\n await cookies.delete('oauth_code_verifier');\n await cookies.delete('returnTo');\n\n const redirectUrl = validateReturnUrl(\n returnTo ?? null,\n applicationUrl,\n defaultRedirectPath,\n validateReturnTo,\n );\n redirectUrl.searchParams.set('login', 'success');\n\n logger.info({ redirectUrl: redirectUrl.toString() }, 'Login successful, redirecting');\n\n return new Response(null, {\n status: 302,\n headers: { Location: redirectUrl.toString() },\n });\n } catch (error) {\n // The session was too large for a cookie — surface this distinctly instead of\n // hiding it in a generic 500, so it's diagnosable in production.\n if (error instanceof CookieTooLargeError) {\n logger.error(\n { error, cookieName: error.cookieName, size: error.size },\n 'Session cookie too large to store',\n );\n return new Response('The session is too large to store. Please contact support.', {\n status: 431,\n });\n }\n\n logger.error({ error }, 'OIDC callback error');\n return new Response('An unexpected error occurred. Please restart the login process.', {\n status: 500,\n });\n }\n}\n","// server/oidc-logout.ts\n//\n// Framework-agnostic RP-initiated logout. Clearing only the local session cookie\n// leaves the provider's session alive, so the next /authorize silently\n// re-authenticates — this handler ends both.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildOidcLogoutUrl } from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { clearSession, readIdToken } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-logout' });\n\n/**\n * Whether the request came from the application itself. POST alone is no defence:\n * a third-party page can auto-submit a form at this endpoint, and the response\n * clears the cookies whether or not the browser attached them.\n *\n * `Sec-Fetch-Site` is authoritative where the browser sends it. Otherwise fall back\n * to `Origin`. A request with neither is not a browser form post, so it is allowed.\n */\nfunction isSameOrigin(request: Request, applicationUrl?: string): boolean {\n const site = request.headers.get('sec-fetch-site');\n if (site) {\n return site === 'same-origin' || site === 'none';\n }\n\n const origin = request.headers.get('origin');\n if (!origin) return true;\n\n try {\n return new URL(origin).origin === new URL(applicationUrl ?? request.url).origin;\n } catch {\n return false;\n }\n}\n\nexport interface OidcLogoutConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session cookies to read and then clear. */\n cookies: CookieStore;\n\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri: string;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Passed through as `state`; verifying it on the way back is the caller's job. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /** Whether to send `client_id`. Default true — see `OidcLogoutOptions`. */\n includeClientId?: boolean;\n\n /**\n * Request methods that may trigger a logout. Defaults to POST only: on GET, any\n * third-party page can force a logout by navigating the browser.\n * @default ['POST']\n */\n allowedMethods?: string[];\n\n /**\n * Public application URL, used to check the `Origin` header behind a proxy where\n * the request URL carries the internal origin. Defaults to the request's own\n * origin.\n */\n applicationUrl?: string;\n\n /**\n * Where to send the user when logout is local-only (no end_session_endpoint).\n * @default postLogoutRedirectUri\n */\n fallbackRedirectUri?: string;\n}\n\n/**\n * Clears the session cookies and redirects to the provider's end-session endpoint\n * with `id_token_hint`. Local cookies are cleared even when the provider hop is\n * unavailable, so the user is always logged out of this application.\n */\nexport async function handleOidcLogout(\n request: Request,\n config: OidcLogoutConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n cookies,\n postLogoutRedirectUri,\n secret = getSessionSecret(),\n rateLimit,\n state,\n logoutHint,\n includeClientId,\n fallbackRedirectUri = postLogoutRedirectUri,\n allowedMethods = ['POST'],\n applicationUrl,\n } = config;\n\n if (!allowedMethods.includes(request.method)) {\n return new Response(null, {\n status: 405,\n headers: { Allow: allowedMethods.join(', ') },\n });\n }\n\n if (!isSameOrigin(request, applicationUrl)) {\n logger.warn(\n { origin: request.headers.get('origin'), site: request.headers.get('sec-fetch-site') },\n 'Cross-origin logout request rejected',\n );\n return new Response('Forbidden', { status: 403 });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n // Read before clearing. Deliberately not readSession: that returns null once the\n // access token has expired, which is exactly when the hint still matters.\n const idTokenHint = await readIdToken(cookies, secret);\n\n try {\n await clearSession(cookies);\n } catch (error) {\n // Never redirect to the provider on a half-cleared local session — the user\n // would come back looking logged in.\n logger.error({ error }, 'Failed to clear session cookies');\n return new Response('Logout failed', { status: 500 });\n }\n\n const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId,\n });\n\n if (!logoutUrl) {\n // Either the provider advertises no end_session_endpoint or discovery failed;\n // buildOidcLogoutUrl logs which. Local logout stands either way.\n logger.info(\n { issuer: oauthConfig.issuer },\n 'No logout URL — local session cleared, provider session may persist',\n );\n return new Response(null, { status: 302, headers: { Location: fallbackRedirectUri } });\n }\n\n logger.info({ hasIdTokenHint: !!idTokenHint }, 'Redirecting to provider for logout');\n\n return new Response(null, { status: 302, headers: { Location: logoutUrl.toString() } });\n}\n","// server/heartbeat-handler.ts\n//\n// Framework-agnostic heartbeat endpoint. Pairs with the client-side heartbeat:\n// when the user is active the client POSTs here, and this handler exchanges the\n// current refresh token for a fresh access token. Glue only — rate limit, load\n// session, refresh, respond — over the standard Request/Response and a CookieStore.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { readSession, refreshSessionInStore } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:heartbeat' });\n\nexport interface HeartbeatHandlerConfig {\n /** OAuth/OIDC configuration used to refresh the access token. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n}\n\n/**\n * Handles a heartbeat request — refresh the active session if there is one.\n *\n * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 when there is no\n * session or the refresh token is dead, 405 for non-POST methods, and 429 when\n * rate-limited.\n */\nexport async function handleHeartbeat(\n request: Request,\n config: HeartbeatHandlerConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, secret = getSessionSecret() } = config;\n\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Heartbeat rate limit exceeded');\n return new Response(null, { status: 429 });\n }\n\n const session = await readSession(cookies, secret);\n if (!session) {\n logger.debug('Heartbeat with no session');\n return new Response(null, { status: 401 });\n }\n\n if (!session.tokens?.refreshToken) {\n logger.warn('Heartbeat with session but no refresh token');\n return new Response(null, { status: 401 });\n }\n\n const updated = await refreshSessionInStore(cookies, oauthConfig, secret);\n if (!updated) {\n // Refresh failed — most likely invalid_grant (refresh token expired).\n logger.info('Heartbeat refresh failed');\n return new Response(null, { status: 401 });\n }\n\n logger.debug('Heartbeat refresh succeeded');\n return Response.json(\n { accessTokenExpiresAt: updated.tokens?.accessTokenExpiresAt ?? null },\n {\n // Auth/session endpoint — must never be cached by the browser or any\n // intermediary, or a stale 200 could fool the client into thinking a refresh\n // succeeded without actually hitting the server.\n headers: { 'Cache-Control': 'private, no-store' },\n },\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/server/session.ts","../../src/server/oidc-login.ts","../../src/server/oidc-callback.ts","../../src/server/oidc-logout.ts","../../src/server/heartbeat-handler.ts"],"sourcesContent":["// server/session.ts\n//\n// Framework-agnostic session persistence over a {@link CookieStore}. The split\n// encode/decode lives in `../session-cookies`; these helpers wire it to a cookie\n// store, apply the per-cookie size guard, and own the refresh flow — with no\n// dependency on any framework's cookie API.\n\nimport {\n ACCESS_TOKEN_COOKIE_NAME,\n COOKIE_INFO_BYTES,\n ID_TOKEN_COOKIE_NAME,\n SESSION_COOKIE_NAME,\n assertCookieWithinLimit,\n defaultSessionCookieOptions,\n} from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { getOAuthErrorLogContext } from '../oauth-logging';\nimport { refreshSession } from '../session-refresh';\nimport {\n decodeIdTokenCookie,\n decodeSessionCookies,\n encodeSessionCookies,\n} from '../session-cookies';\nimport type { CreateSessionOptions, Session } from '../types';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:session' });\n\ntype Secret = string | Uint8Array;\n\n/** Applies the browser size guard to one cookie value; logs as it nears the limit. */\nfunction checkSize(name: string, value: string): void {\n // Fail loudly above the browser per-cookie limit (the browser would otherwise\n // drop the cookie silently, producing a broken login).\n const size = assertCookieWithinLimit(name, value);\n if (size >= COOKIE_INFO_BYTES) {\n logger.info({ cookieName: name, size }, 'Cookie approaching browser size limit');\n }\n}\n\n/**\n * Encrypts a session and writes it across the \"session\", \"session_at\" and\n * \"session_it\" cookies, clearing the split-out cookies the session has no token for.\n *\n * @returns The encrypted JWT stored in the main \"session\" cookie.\n */\nexport async function persistSession(\n store: CookieStore,\n session: Session,\n secret: Secret,\n): Promise<string> {\n const encoded = await encodeSessionCookies(session, secret);\n\n const values: Array<[string, string | undefined]> = [\n [SESSION_COOKIE_NAME, encoded.session],\n [ACCESS_TOKEN_COOKIE_NAME, encoded.accessToken],\n [ID_TOKEN_COOKIE_NAME, encoded.idToken],\n ];\n\n // Size-check everything before touching the store. Throwing part-way through\n // would leave one user's session cookie next to another user's tokens — the\n // caller sees an error while the browser holds a working, mixed-up session.\n for (const [name, value] of values) {\n if (value) checkSize(name, value);\n }\n\n for (const [name, value] of values) {\n if (value) {\n await store.set(name, value, defaultSessionCookieOptions);\n } else {\n await store.delete(name);\n }\n }\n\n return encoded.session;\n}\n\n/**\n * Reads and reassembles the current session from the cookie store, or null when\n * there is none. Decode/validation rules live in {@link decodeSessionCookies};\n * the returned session may carry an expired access token.\n */\nexport async function readSession(store: CookieStore, secret: Secret): Promise<Session | null> {\n try {\n return await decodeSessionCookies(\n {\n session: (await store.get(SESSION_COOKIE_NAME)) ?? null,\n accessToken: (await store.get(ACCESS_TOKEN_COOKIE_NAME)) ?? null,\n idToken: (await store.get(ID_TOKEN_COOKIE_NAME)) ?? null,\n },\n secret,\n );\n } catch (error) {\n // Worker thread errors (e.g. from the crypto worker) should not crash the app.\n if (error instanceof Error && error.message.includes('worker')) {\n logger.error({ error }, 'Worker thread error reading session');\n } else {\n logger.error({ error }, 'Unexpected error reading session');\n }\n return null;\n }\n}\n\n/**\n * Refreshes the stored session using its refresh token and persists the result.\n * Returns the updated session, or null when there is nothing to refresh or the\n * refresh token is no longer valid.\n */\nexport async function refreshSessionInStore(\n store: CookieStore,\n config: OAuthConfig,\n secret: Secret,\n options: CreateSessionOptions = {},\n): Promise<Session | null> {\n try {\n // readSession also returns sessions with expired access tokens — exactly\n // what refresh is for.\n const current = await readSession(store, secret);\n\n if (!current) {\n logger.warn('No current session to refresh');\n return null;\n }\n if (!current.tokens?.refreshToken) {\n logger.error('Current session has no refresh token');\n return null;\n }\n\n const updated = await refreshSession(current, config, options);\n if (!updated) {\n logger.error('Session refresh returned null');\n return null;\n }\n\n await persistSession(store, updated, secret);\n return updated;\n } catch (error) {\n // invalid_grant is expected during logout/session expiry — log it quietly.\n // getOAuthErrorLogContext unwraps errors (fields may live on `cause`), which a\n // direct property read would miss and misclassify.\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n logger.info('Session refresh failed - refresh token expired or invalid');\n } else {\n logger.error({ error: errorContext }, 'Failed to refresh session');\n }\n return null;\n }\n}\n\n/**\n * Reads the raw ID token independently of the session cookies — logout needs\n * the hint even when {@link readSession} returns null.\n */\nexport async function readIdToken(store: CookieStore, secret: Secret): Promise<string | undefined> {\n try {\n const raw = await store.get(ID_TOKEN_COOKIE_NAME);\n return raw ? await decodeIdTokenCookie(raw, secret) : undefined;\n } catch (error) {\n logger.error({ error }, 'Unexpected error reading ID token');\n return undefined;\n }\n}\n\n/** Deletes all session cookies. */\nexport async function clearSession(store: CookieStore): Promise<void> {\n await store.delete(SESSION_COOKIE_NAME);\n await store.delete(ACCESS_TOKEN_COOKIE_NAME);\n await store.delete(ID_TOKEN_COOKIE_NAME);\n}\n","// server/oidc-login.ts\n//\n// Framework-agnostic OIDC login initiation. Builds PKCE parameters, stores them\n// in the cookie store, and redirects to the provider's authorization endpoint.\n// Uses the standard Request/Response, a CookieStore, and an optional rate-limit\n// callback, so any server runtime can call it.\n\nimport { defaultOAuthCookieOptions } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildPKCEOptions, discoverAndBuildAuthorizationUrl } from '../oauth';\nimport type { CookieStore } from './cookie-store';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-login' });\n\nexport interface OidcLoginConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store used to persist the PKCE state. */\n cookies: CookieStore;\n\n /**\n * Optional rate-limit gate. When it resolves false, the handler responds 429.\n */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /**\n * Validates and sanitizes the returnTo parameter, returning a safe path or null.\n * @default Only allows relative paths starting with a single `/`.\n */\n validateReturnTo?: (rawReturnTo: string) => string | null;\n}\n\nconst defaultValidateReturnTo = (raw: string): string | null =>\n /^\\/(?!\\/)/.test(raw) ? raw : null;\n\n/**\n * Handles OIDC login initiation: rate-limit, build PKCE, persist it, and redirect\n * to the authorization endpoint.\n */\nexport async function handleOidcLogin(\n request: Request,\n config: OidcLoginConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, validateReturnTo = defaultValidateReturnTo } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n const url = new URL(request.url);\n const rawReturnTo = url.searchParams.get('returnTo');\n const returnTo = rawReturnTo ? validateReturnTo(rawReturnTo) : null;\n\n const pkce = await buildPKCEOptions(oauthConfig);\n const authorizationUrl = await discoverAndBuildAuthorizationUrl(oauthConfig, pkce);\n\n logger.info('Redirecting to OIDC provider for login');\n\n await cookies.set('oauth_state', pkce.state, defaultOAuthCookieOptions);\n await cookies.set('oauth_code_verifier', pkce.code_verifier, defaultOAuthCookieOptions);\n if (returnTo) {\n await cookies.set('returnTo', returnTo, defaultOAuthCookieOptions);\n } else {\n // Clear any stale returnTo from an earlier, abandoned login so this one's\n // post-login redirect is deterministic rather than inheriting a past path.\n await cookies.delete('returnTo');\n }\n\n return new Response(null, {\n status: 302,\n headers: { Location: authorizationUrl.toString() },\n });\n}\n","// server/oidc-callback.ts\n//\n// Framework-agnostic OIDC callback handler. Orchestrates the OIDC concerns —\n// rate limit, PKCE cookie read, code exchange, session persistence, safe redirect\n// — over the standard Request/Response and a CookieStore, delegating the protocol\n// to the core oauth module and persistence to the server session helpers.\n\nimport { CookieTooLargeError } from '../cookies';\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport {\n buildSessionFromTokens,\n exchangeAuthorizationCode,\n validateReturnUrl,\n} from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { persistSession } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-callback' });\n\nexport interface OidcCallbackConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Public application URL (used to reconstruct the callback URL and validate redirects). */\n applicationUrl: string;\n\n /** Cookie store holding the PKCE state and receiving the session cookies. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /**\n * Name of the ID token claim that contains user roles.\n * @default 'roles'\n */\n rolesClaim?: string;\n\n /**\n * Default path to redirect to after login if no returnTo cookie is set.\n * @default '/'\n */\n defaultRedirectPath?: string;\n\n /**\n * Optional validator for the returnTo path. If omitted, only same-origin paths\n * are allowed.\n */\n validateReturnTo?: (path: string) => boolean;\n}\n\n/**\n * Handles the OIDC callback: rate-limit, exchange the code for tokens, persist the\n * session across the cookie store, and redirect to a validated returnTo.\n */\nexport async function handleOidcCallback(\n request: Request,\n config: OidcCallbackConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n applicationUrl,\n cookies,\n rateLimit,\n secret = getSessionSecret(),\n rolesClaim = 'roles',\n defaultRedirectPath = '/',\n validateReturnTo,\n } = config;\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n try {\n // Reconstruct the public callback URL. Behind a TLS-terminating proxy\n // applicationUrl is just the origin (\"/\"), and the token-exchange redirect_uri\n // must match the callback path used at authorize — so keep the request path.\n const currentUrl = new URL(request.url);\n const publicUrl = new URL(applicationUrl);\n publicUrl.pathname = currentUrl.pathname;\n publicUrl.search = currentUrl.search;\n\n const storedState = await cookies.get('oauth_state');\n const storedCodeVerifier = await cookies.get('oauth_code_verifier');\n\n if (!storedState || !storedCodeVerifier) {\n logger.warn('Missing state or code verifier');\n return new Response('Please restart the login process.', { status: 400 });\n }\n\n const tokens = await exchangeAuthorizationCode(\n oauthConfig,\n publicUrl,\n storedCodeVerifier,\n storedState,\n );\n\n // Build the session and persist it (split across session/session_at).\n const session = buildSessionFromTokens(tokens, rolesClaim);\n await persistSession(cookies, session, secret);\n\n // Clean up PKCE & returnTo cookies (read returnTo first).\n const returnTo = await cookies.get('returnTo');\n await cookies.delete('oauth_state');\n await cookies.delete('oauth_code_verifier');\n await cookies.delete('returnTo');\n\n const redirectUrl = validateReturnUrl(\n returnTo ?? null,\n applicationUrl,\n defaultRedirectPath,\n validateReturnTo,\n );\n redirectUrl.searchParams.set('login', 'success');\n\n logger.info({ redirectUrl: redirectUrl.toString() }, 'Login successful, redirecting');\n\n return new Response(null, {\n status: 302,\n headers: { Location: redirectUrl.toString() },\n });\n } catch (error) {\n // The session was too large for a cookie — surface this distinctly instead of\n // hiding it in a generic 500, so it's diagnosable in production.\n if (error instanceof CookieTooLargeError) {\n logger.error(\n { error, cookieName: error.cookieName, size: error.size },\n 'Session cookie too large to store',\n );\n return new Response('The session is too large to store. Please contact support.', {\n status: 431,\n });\n }\n\n logger.error({ error }, 'OIDC callback error');\n return new Response('An unexpected error occurred. Please restart the login process.', {\n status: 500,\n });\n }\n}\n","// server/oidc-logout.ts\n//\n// Framework-agnostic RP-initiated logout. Clearing only the local session cookie\n// leaves the provider's session alive, so the next /authorize silently\n// re-authenticates — this handler ends both.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { buildOidcLogoutUrl } from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { clearSession, readIdToken } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:oidc-logout' });\n\n/**\n * Whether the request came from the application itself. POST alone is no defence:\n * a third-party page can auto-submit a form at this endpoint, and the response\n * clears the cookies whether or not the browser attached them.\n *\n * `Sec-Fetch-Site` is authoritative where the browser sends it. Otherwise fall back\n * to `Origin`. A request with neither is not a browser form post, so it is allowed.\n */\nfunction isSameOrigin(request: Request, applicationUrl?: string): boolean {\n const site = request.headers.get('sec-fetch-site');\n if (site) {\n return site === 'same-origin' || site === 'none';\n }\n\n const origin = request.headers.get('origin');\n if (!origin) return true;\n\n try {\n return new URL(origin).origin === new URL(applicationUrl ?? request.url).origin;\n } catch {\n return false;\n }\n}\n\nexport interface OidcLogoutConfig {\n /** OAuth/OIDC configuration. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session cookies to read and then clear. */\n cookies: CookieStore;\n\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri: string;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Passed through as `state`; verifying it on the way back is the caller's job. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /** Whether to send `client_id`. Default true — see `OidcLogoutOptions`. */\n includeClientId?: boolean;\n\n /**\n * Request methods that may trigger a logout. Defaults to POST only: on GET, any\n * third-party page can force a logout by navigating the browser.\n * @default ['POST']\n */\n allowedMethods?: string[];\n\n /**\n * Public application URL, used to check the `Origin` header behind a proxy where\n * the request URL carries the internal origin. Defaults to the request's own\n * origin.\n */\n applicationUrl?: string;\n\n /**\n * Where to send the user when logout is local-only (no end_session_endpoint).\n * @default postLogoutRedirectUri\n */\n fallbackRedirectUri?: string;\n}\n\n/**\n * Clears the session cookies and redirects to the provider's end-session endpoint\n * with `id_token_hint`. Local cookies are cleared even when the provider hop is\n * unavailable, so the user is always logged out of this application.\n */\nexport async function handleOidcLogout(\n request: Request,\n config: OidcLogoutConfig,\n): Promise<Response> {\n const {\n oauthConfig,\n cookies,\n postLogoutRedirectUri,\n secret = getSessionSecret(),\n rateLimit,\n state,\n logoutHint,\n includeClientId,\n fallbackRedirectUri = postLogoutRedirectUri,\n allowedMethods = ['POST'],\n applicationUrl,\n } = config;\n\n if (!allowedMethods.includes(request.method)) {\n return new Response(null, {\n status: 405,\n headers: { Allow: allowedMethods.join(', ') },\n });\n }\n\n if (!isSameOrigin(request, applicationUrl)) {\n logger.warn(\n { origin: request.headers.get('origin'), site: request.headers.get('sec-fetch-site') },\n 'Cross-origin logout request rejected',\n );\n return new Response('Forbidden', { status: 403 });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Rate limit exceeded');\n return new Response('Too many requests', { status: 429 });\n }\n\n // Read before clearing. Deliberately not readSession: that returns null once the\n // access token has expired, which is exactly when the hint still matters.\n const idTokenHint = await readIdToken(cookies, secret);\n\n try {\n await clearSession(cookies);\n } catch (error) {\n // Never redirect to the provider on a half-cleared local session — the user\n // would come back looking logged in.\n logger.error({ error }, 'Failed to clear session cookies');\n return new Response('Logout failed', { status: 500 });\n }\n\n const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId,\n });\n\n if (!logoutUrl) {\n // Either the provider advertises no end_session_endpoint or discovery failed;\n // buildOidcLogoutUrl logs which. Local logout stands either way.\n logger.info(\n { issuer: oauthConfig.issuer },\n 'No logout URL — local session cleared, provider session may persist',\n );\n return new Response(null, { status: 302, headers: { Location: fallbackRedirectUri } });\n }\n\n logger.info({ hasIdTokenHint: !!idTokenHint }, 'Redirecting to provider for logout');\n\n return new Response(null, { status: 302, headers: { Location: logoutUrl.toString() } });\n}\n","// server/heartbeat-handler.ts\n//\n// Framework-agnostic heartbeat endpoint. Pairs with the client-side heartbeat:\n// when the user is active the client POSTs here, and this handler exchanges the\n// current refresh token for a fresh access token. Glue only — rate limit, load\n// session, refresh, respond — over the standard Request/Response and a CookieStore.\n\nimport { createLogger } from '../logger';\nimport type { OAuthConfig } from '../oauth';\nimport { getSessionSecret } from '../utils';\nimport type { CookieStore } from './cookie-store';\nimport { readSession, refreshSessionInStore } from './session';\n\nconst logger = createLogger({ namespace: 'fides-auth:server:heartbeat' });\n\nexport interface HeartbeatHandlerConfig {\n /** OAuth/OIDC configuration used to refresh the access token. */\n oauthConfig: OAuthConfig;\n\n /** Cookie store holding the session. */\n cookies: CookieStore;\n\n /** Optional rate-limit gate. When it resolves false, the handler responds 429. */\n rateLimit?: () => boolean | Promise<boolean>;\n\n /** Session encryption secret. Defaults to {@link getSessionSecret}. */\n secret?: string | Uint8Array;\n}\n\n/**\n * Handles a heartbeat request — refresh the active session if there is one.\n *\n * Returns 200 with `{ accessTokenExpiresAt }` on success, 401 when there is no\n * session or the refresh token is dead, 405 for non-POST methods, and 429 when\n * rate-limited.\n */\nexport async function handleHeartbeat(\n request: Request,\n config: HeartbeatHandlerConfig,\n): Promise<Response> {\n const { oauthConfig, cookies, rateLimit, secret = getSessionSecret() } = config;\n\n if (request.method !== 'POST') {\n return new Response(null, { status: 405, headers: { Allow: 'POST' } });\n }\n\n if (rateLimit && !(await rateLimit())) {\n logger.warn('Heartbeat rate limit exceeded');\n return new Response(null, { status: 429 });\n }\n\n // An expired access token is what the heartbeat recovers from, not 401 on —\n // only a missing session or dead refresh token logs the user out.\n const session = await readSession(cookies, secret);\n if (!session) {\n logger.debug('Heartbeat with no session');\n return new Response(null, { status: 401 });\n }\n\n if (!session.tokens?.refreshToken) {\n logger.warn('Heartbeat with session but no refresh token');\n return new Response(null, { status: 401 });\n }\n\n const updated = await refreshSessionInStore(cookies, oauthConfig, secret);\n if (!updated) {\n // Refresh failed — most likely invalid_grant (refresh token expired).\n logger.info('Heartbeat refresh failed');\n return new Response(null, { status: 401 });\n }\n\n logger.debug('Heartbeat refresh succeeded');\n return Response.json(\n { accessTokenExpiresAt: updated.tokens?.accessTokenExpiresAt ?? null },\n {\n // Auth/session endpoint — must never be cached by the browser or any\n // intermediary, or a stale 200 could fool the client into thinking a refresh\n // succeeded without actually hitting the server.\n headers: { 'Cache-Control': 'private, no-store' },\n },\n );\n}\n"],"mappings":"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"}
@@ -11,8 +11,8 @@ type Secret = string | Uint8Array;
11
11
  export declare function persistSession(store: CookieStore, session: Session, secret: Secret): Promise<string>;
12
12
  /**
13
13
  * Reads and reassembles the current session from the cookie store, or null when
14
- * there is no valid session. All the split/expiry/legacy handling lives in
15
- * {@link decodeSessionCookies}; this just supplies the cookie values.
14
+ * there is none. Decode/validation rules live in {@link decodeSessionCookies};
15
+ * the returned session may carry an expired access token.
16
16
  */
17
17
  export declare function readSession(store: CookieStore, secret: Secret): Promise<Session | null>;
18
18
  /**
@@ -22,9 +22,8 @@ export declare function readSession(store: CookieStore, secret: Secret): Promise
22
22
  */
23
23
  export declare function refreshSessionInStore(store: CookieStore, config: OAuthConfig, secret: Secret, options?: CreateSessionOptions): Promise<Session | null>;
24
24
  /**
25
- * Reads the raw ID token, whether or not the session is still valid. Logout needs
26
- * the hint after the access token has expired, which is exactly when
27
- * {@link readSession} returns null.
25
+ * Reads the raw ID token independently of the session cookies — logout needs
26
+ * the hint even when {@link readSession} returns null.
28
27
  */
29
28
  export declare function readIdToken(store: CookieStore, secret: Secret): Promise<string | undefined>;
30
29
  /** Deletes all session cookies. */
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5C,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,KAAK,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAYlC;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAmB7F;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAoCzB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQjG;AAED,mCAAmC;AACnC,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/server/session.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAQ5C,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAIlD,KAAK,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAYlC;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAyBjB;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAmB7F;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAsCzB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQjG;AAED,mCAAmC;AACnC,wBAAsB,YAAY,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAIpE"}
@@ -31,10 +31,11 @@ export declare function encodeSessionCookies(session: Session, secret: string |
31
31
  /**
32
32
  * Decodes the cookie values back into a session, reattaching the access and ID tokens.
33
33
  *
34
- * Returns null when there is no session, the main value fails validation, or the
35
- * access token has expired — preserving the contract that an expired access token
36
- * means "no session". A corrupt access-token value is tolerated: the session is
37
- * returned without an access token rather than discarded.
34
+ * Returns the session whenever the main value decrypts and validates — an
35
+ * expired access token does not hide it; token freshness is the caller's
36
+ * concern. Returns null for a missing/invalid main value, or a stale legacy
37
+ * single-cookie session (one re-login). A corrupt access-token value is
38
+ * tolerated: the session is returned without it.
38
39
  *
39
40
  * @param raw - The raw cookie values read from the store.
40
41
  * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).
@@ -1 +1 @@
1
- {"version":3,"file":"session-cookies.d.ts","sourceRoot":"","sources":["../src/session-cookies.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAsBlC,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAmBhC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,iBAAiB,EACtB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA0CzB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
1
+ {"version":3,"file":"session-cookies.d.ts","sourceRoot":"","sources":["../src/session-cookies.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIlC,uDAAuD;AACvD,MAAM,WAAW,qBAAqB;IACpC,sFAAsF;IACtF,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mFAAmF;IACnF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wDAAwD;IACxD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAmBhC;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,iBAAiB,EACtB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAoCzB;AAED;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B"}
@@ -1,25 +1,9 @@
1
1
  import { createLogger } from "./logger.js";
2
2
  import { createEncryptedJWT, decryptJWT } from "./utils.js";
3
3
  import { validateSessionJwt } from "./session-validation.js";
4
- import { decodeJwt } from "jose";
5
4
  //#region src/session-cookies.ts
6
5
  var logger = createLogger({ namespace: "fides-auth:session-cookies" });
7
6
  /**
8
- * True only when the access token is a JWT carrying an `exp` claim that is in the
9
- * past. Mirrors {@link validateSessionJwt}: an access token we can't decode (an
10
- * opaque token, or one without `exp`) has unknown expiry and must NOT be treated
11
- * as expired — otherwise opaque tokens would silently discard the whole session.
12
- */
13
- function accessTokenIsExpired(accessToken) {
14
- try {
15
- const { exp } = decodeJwt(accessToken);
16
- if (typeof exp !== "number") return false;
17
- return exp - Date.now() / 1e3 <= 0;
18
- } catch {
19
- return false;
20
- }
21
- }
22
- /**
23
7
  * Encrypts a session into the three cookie values, splitting the raw access and ID
24
8
  * tokens out.
25
9
  *
@@ -47,10 +31,11 @@ async function encodeSessionCookies(session, secret) {
47
31
  /**
48
32
  * Decodes the cookie values back into a session, reattaching the access and ID tokens.
49
33
  *
50
- * Returns null when there is no session, the main value fails validation, or the
51
- * access token has expired — preserving the contract that an expired access token
52
- * means "no session". A corrupt access-token value is tolerated: the session is
53
- * returned without an access token rather than discarded.
34
+ * Returns the session whenever the main value decrypts and validates — an
35
+ * expired access token does not hide it; token freshness is the caller's
36
+ * concern. Returns null for a missing/invalid main value, or a stale legacy
37
+ * single-cookie session (one re-login). A corrupt access-token value is
38
+ * tolerated: the session is returned without it.
54
39
  *
55
40
  * @param raw - The raw cookie values read from the store.
56
41
  * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).
@@ -65,16 +50,10 @@ async function decodeSessionCookies(raw, secret) {
65
50
  if (raw.accessToken) try {
66
51
  const payload = await decryptJWT(raw.accessToken, secret);
67
52
  const accessToken = typeof payload.accessToken === "string" ? payload.accessToken : void 0;
68
- if (accessToken) {
69
- if (accessTokenIsExpired(accessToken)) {
70
- logger.info("Access token has expired");
71
- return null;
72
- }
73
- session.tokens = {
74
- ...session.tokens,
75
- accessToken
76
- };
77
- }
53
+ if (accessToken) session.tokens = {
54
+ ...session.tokens,
55
+ accessToken
56
+ };
78
57
  } catch (error) {
79
58
  logger.warn({ error }, "Failed to decode access-token cookie");
80
59
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-cookies.js","names":[],"sources":["../src/session-cookies.ts"],"sourcesContent":["// session-cookies.ts\n//\n// Framework-agnostic encoding/decoding of a session into cookie *values*. This\n// module performs no I/O — it never reads or writes cookies. Framework adapters\n// (Next.js, React Router, …) own the actual cookie store and call these helpers\n// to split a session across two cookie values and to reassemble it again.\n//\n// The split: the large raw JWTs — the access token and the ID token (kept for\n// `id_token_hint` on logout) — each get their own encrypted value, everything else\n// goes in the main one, so no cookie has to carry two JWTs against the ~4KB limit.\n\nimport { decodeJwt } from 'jose';\n\nimport { createEncryptedJWT, decryptJWT } from './utils';\nimport { validateSessionJwt } from './session-validation';\nimport { createLogger } from './logger';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-cookies' });\n\n/**\n * True only when the access token is a JWT carrying an `exp` claim that is in the\n * past. Mirrors {@link validateSessionJwt}: an access token we can't decode (an\n * opaque token, or one without `exp`) has unknown expiry and must NOT be treated\n * as expired — otherwise opaque tokens would silently discard the whole session.\n */\nfunction accessTokenIsExpired(accessToken: string): boolean {\n try {\n const { exp } = decodeJwt(accessToken);\n if (typeof exp !== 'number') {\n return false;\n }\n return exp - Date.now() / 1000 <= 0;\n } catch {\n return false;\n }\n}\n\n/** Encrypted cookie values produced from a session. */\nexport interface EncodedSessionCookies {\n /** Value for the main session cookie (everything except the raw access/ID tokens). */\n session: string;\n /**\n * Value for the access-token cookie, or `undefined` when the session carries\n * no access token. Adapters should delete the access-token cookie in that case.\n */\n accessToken?: string;\n /** Value for the ID-token cookie, or `undefined` when the session carries none. */\n idToken?: string;\n}\n\n/** Raw cookie values read from the store, before decoding. */\nexport interface RawSessionCookies {\n /** The main session cookie value, or null if absent. */\n session: string | null;\n /** The access-token cookie value, or null if absent. */\n accessToken: string | null;\n /** The ID-token cookie value, or null if absent. */\n idToken?: string | null;\n}\n\n/**\n * Encrypts a session into the three cookie values, splitting the raw access and ID\n * tokens out.\n *\n * @param session - The session to encode.\n * @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function encodeSessionCookies(\n session: Session,\n secret: string | Uint8Array,\n): Promise<EncodedSessionCookies> {\n const { tokens, ...rest } = session;\n const accessToken = tokens?.accessToken;\n const idToken = tokens?.idToken;\n\n // Main value: the whole session minus the raw access and ID tokens (undefined\n // keys are dropped during JWT serialization).\n const coreSession: Session = {\n ...rest,\n tokens: tokens ? { ...tokens, accessToken: undefined, idToken: undefined } : undefined,\n };\n\n const main = await createEncryptedJWT(coreSession, secret);\n\n return {\n session: main,\n accessToken: accessToken ? await createEncryptedJWT({ accessToken }, secret) : undefined,\n idToken: idToken ? await createEncryptedJWT({ idToken }, secret) : undefined,\n };\n}\n\n/**\n * Decodes the cookie values back into a session, reattaching the access and ID tokens.\n *\n * Returns null when there is no session, the main value fails validation, or the\n * access token has expired — preserving the contract that an expired access token\n * means \"no session\". A corrupt access-token value is tolerated: the session is\n * returned without an access token rather than discarded.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<Session | null> {\n if (!raw.session) {\n return null;\n }\n\n const { status, session } = await validateSessionJwt(raw.session, secret);\n\n // status === 'EXPIRED' only fires for legacy single-cookie sessions, where the\n // access token still lives in the main value. Split sessions are checked below.\n if (status !== 'VALID' || !session) {\n logger.debug({ status }, 'Session cookie did not validate');\n return null;\n }\n\n if (raw.accessToken) {\n try {\n const payload = await decryptJWT(raw.accessToken, secret);\n const accessToken = typeof payload.accessToken === 'string' ? payload.accessToken : undefined;\n\n if (accessToken) {\n // Preserve the contract: a verifiably-expired access token means no\n // session. Opaque/undecodable tokens are kept (unknown expiry).\n if (accessTokenIsExpired(accessToken)) {\n logger.info('Access token has expired');\n return null;\n }\n session.tokens = { ...session.tokens, accessToken };\n }\n } catch (error) {\n // A corrupt/forged access-token value shouldn't take down the session.\n logger.warn({ error }, 'Failed to decode access-token cookie');\n }\n }\n\n if (raw.idToken) {\n const idToken = await decodeIdTokenCookie(raw.idToken, secret);\n if (idToken) {\n session.tokens = { ...session.tokens, idToken };\n }\n }\n\n return session;\n}\n\n/**\n * Decrypts the ID-token cookie value on its own, independent of session validity.\n * Logout needs the hint precisely when the session has gone stale, so this must not\n * go through {@link decodeSessionCookies}.\n *\n * @param raw - The ID-token cookie value.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeIdTokenCookie(\n raw: string,\n secret: string | Uint8Array,\n): Promise<string | undefined> {\n try {\n const payload = await decryptJWT(raw, secret);\n return typeof payload.idToken === 'string' ? payload.idToken : undefined;\n } catch (error) {\n logger.warn({ error }, 'Failed to decode ID-token cookie');\n return undefined;\n }\n}\n"],"mappings":";;;;;AAkBA,IAAM,SAAS,aAAa,EAAE,WAAW,6BAA6B,CAAC;;;;;;;AAQvE,SAAS,qBAAqB,aAA8B;CAC1D,IAAI;EACF,MAAM,EAAE,QAAQ,UAAU,WAAW;EACrC,IAAI,OAAO,QAAQ,UACjB,OAAO;EAET,OAAO,MAAM,KAAK,IAAI,IAAI,OAAQ;CACpC,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;;AAgCA,eAAsB,qBACpB,SACA,QACgC;CAChC,MAAM,EAAE,QAAQ,GAAG,SAAS;CAC5B,MAAM,cAAc,QAAQ;CAC5B,MAAM,UAAU,QAAQ;CAIxB,MAAM,cAAuB;EAC3B,GAAG;EACH,QAAQ,SAAS;GAAE,GAAG;GAAQ,aAAa,KAAA;GAAW,SAAS,KAAA;EAAU,IAAI,KAAA;CAC/E;CAIA,OAAO;EACL,SAAS,MAHQ,mBAAmB,aAAa,MAAM;EAIvD,aAAa,cAAc,MAAM,mBAAmB,EAAE,YAAY,GAAG,MAAM,IAAI,KAAA;EAC/E,SAAS,UAAU,MAAM,mBAAmB,EAAE,QAAQ,GAAG,MAAM,IAAI,KAAA;CACrE;AACF;;;;;;;;;;;;AAaA,eAAsB,qBACpB,KACA,QACyB;CACzB,IAAI,CAAC,IAAI,SACP,OAAO;CAGT,MAAM,EAAE,QAAQ,YAAY,MAAM,mBAAmB,IAAI,SAAS,MAAM;CAIxE,IAAI,WAAW,WAAW,CAAC,SAAS;EAClC,OAAO,MAAM,EAAE,OAAO,GAAG,iCAAiC;EAC1D,OAAO;CACT;CAEA,IAAI,IAAI,aACN,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,IAAI,aAAa,MAAM;EACxD,MAAM,cAAc,OAAO,QAAQ,gBAAgB,WAAW,QAAQ,cAAc,KAAA;EAEpF,IAAI,aAAa;GAGf,IAAI,qBAAqB,WAAW,GAAG;IACrC,OAAO,KAAK,0BAA0B;IACtC,OAAO;GACT;GACA,QAAQ,SAAS;IAAE,GAAG,QAAQ;IAAQ;GAAY;EACpD;CACF,SAAS,OAAO;EAEd,OAAO,KAAK,EAAE,MAAM,GAAG,sCAAsC;CAC/D;CAGF,IAAI,IAAI,SAAS;EACf,MAAM,UAAU,MAAM,oBAAoB,IAAI,SAAS,MAAM;EAC7D,IAAI,SACF,QAAQ,SAAS;GAAE,GAAG,QAAQ;GAAQ;EAAQ;CAElD;CAEA,OAAO;AACT;;;;;;;;;AAUA,eAAsB,oBACpB,KACA,QAC6B;CAC7B,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,KAAK,MAAM;EAC5C,OAAO,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,KAAA;CACjE,SAAS,OAAO;EACd,OAAO,KAAK,EAAE,MAAM,GAAG,kCAAkC;EACzD;CACF;AACF"}
1
+ {"version":3,"file":"session-cookies.js","names":[],"sources":["../src/session-cookies.ts"],"sourcesContent":["// session-cookies.ts\n//\n// Framework-agnostic encoding/decoding of a session into cookie *values*. This\n// module performs no I/O — it never reads or writes cookies. Framework adapters\n// (Next.js, React Router, …) own the actual cookie store and call these helpers\n// to split a session across two cookie values and to reassemble it again.\n//\n// The split: the large raw JWTs — the access token and the ID token (kept for\n// `id_token_hint` on logout) — each get their own encrypted value, everything else\n// goes in the main one, so no cookie has to carry two JWTs against the ~4KB limit.\n\nimport { createEncryptedJWT, decryptJWT } from './utils';\nimport { validateSessionJwt } from './session-validation';\nimport { createLogger } from './logger';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-cookies' });\n\n/** Encrypted cookie values produced from a session. */\nexport interface EncodedSessionCookies {\n /** Value for the main session cookie (everything except the raw access/ID tokens). */\n session: string;\n /**\n * Value for the access-token cookie, or `undefined` when the session carries\n * no access token. Adapters should delete the access-token cookie in that case.\n */\n accessToken?: string;\n /** Value for the ID-token cookie, or `undefined` when the session carries none. */\n idToken?: string;\n}\n\n/** Raw cookie values read from the store, before decoding. */\nexport interface RawSessionCookies {\n /** The main session cookie value, or null if absent. */\n session: string | null;\n /** The access-token cookie value, or null if absent. */\n accessToken: string | null;\n /** The ID-token cookie value, or null if absent. */\n idToken?: string | null;\n}\n\n/**\n * Encrypts a session into the three cookie values, splitting the raw access and ID\n * tokens out.\n *\n * @param session - The session to encode.\n * @param secret - The encryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function encodeSessionCookies(\n session: Session,\n secret: string | Uint8Array,\n): Promise<EncodedSessionCookies> {\n const { tokens, ...rest } = session;\n const accessToken = tokens?.accessToken;\n const idToken = tokens?.idToken;\n\n // Main value: the whole session minus the raw access and ID tokens (undefined\n // keys are dropped during JWT serialization).\n const coreSession: Session = {\n ...rest,\n tokens: tokens ? { ...tokens, accessToken: undefined, idToken: undefined } : undefined,\n };\n\n const main = await createEncryptedJWT(coreSession, secret);\n\n return {\n session: main,\n accessToken: accessToken ? await createEncryptedJWT({ accessToken }, secret) : undefined,\n idToken: idToken ? await createEncryptedJWT({ idToken }, secret) : undefined,\n };\n}\n\n/**\n * Decodes the cookie values back into a session, reattaching the access and ID tokens.\n *\n * Returns the session whenever the main value decrypts and validates — an\n * expired access token does not hide it; token freshness is the caller's\n * concern. Returns null for a missing/invalid main value, or a stale legacy\n * single-cookie session (one re-login). A corrupt access-token value is\n * tolerated: the session is returned without it.\n *\n * @param raw - The raw cookie values read from the store.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeSessionCookies(\n raw: RawSessionCookies,\n secret: string | Uint8Array,\n): Promise<Session | null> {\n if (!raw.session) {\n return null;\n }\n\n const { status, session } = await validateSessionJwt(raw.session, secret);\n\n // EXPIRED only fires for legacy single-cookie sessions — dropped, not migrated.\n if (status !== 'VALID' || !session) {\n logger.debug({ status }, 'Session cookie did not validate');\n return null;\n }\n\n if (raw.accessToken) {\n try {\n const payload = await decryptJWT(raw.accessToken, secret);\n const accessToken = typeof payload.accessToken === 'string' ? payload.accessToken : undefined;\n\n if (accessToken) {\n // Attached as stored, expired or not — freshness is the caller's concern.\n session.tokens = { ...session.tokens, accessToken };\n }\n } catch (error) {\n // A corrupt/forged access-token value shouldn't take down the session.\n logger.warn({ error }, 'Failed to decode access-token cookie');\n }\n }\n\n if (raw.idToken) {\n const idToken = await decodeIdTokenCookie(raw.idToken, secret);\n if (idToken) {\n session.tokens = { ...session.tokens, idToken };\n }\n }\n\n return session;\n}\n\n/**\n * Decrypts the ID-token cookie value on its own, independent of session validity.\n * Logout needs the hint precisely when the session has gone stale, so this must not\n * go through {@link decodeSessionCookies}.\n *\n * @param raw - The ID-token cookie value.\n * @param secret - The decryption key as a hex string or Uint8Array (32 bytes for A256GCM).\n */\nexport async function decodeIdTokenCookie(\n raw: string,\n secret: string | Uint8Array,\n): Promise<string | undefined> {\n try {\n const payload = await decryptJWT(raw, secret);\n return typeof payload.idToken === 'string' ? payload.idToken : undefined;\n } catch (error) {\n logger.warn({ error }, 'Failed to decode ID-token cookie');\n return undefined;\n }\n}\n"],"mappings":";;;;AAgBA,IAAM,SAAS,aAAa,EAAE,WAAW,6BAA6B,CAAC;;;;;;;;AAgCvE,eAAsB,qBACpB,SACA,QACgC;CAChC,MAAM,EAAE,QAAQ,GAAG,SAAS;CAC5B,MAAM,cAAc,QAAQ;CAC5B,MAAM,UAAU,QAAQ;CAIxB,MAAM,cAAuB;EAC3B,GAAG;EACH,QAAQ,SAAS;GAAE,GAAG;GAAQ,aAAa,KAAA;GAAW,SAAS,KAAA;EAAU,IAAI,KAAA;CAC/E;CAIA,OAAO;EACL,SAAS,MAHQ,mBAAmB,aAAa,MAAM;EAIvD,aAAa,cAAc,MAAM,mBAAmB,EAAE,YAAY,GAAG,MAAM,IAAI,KAAA;EAC/E,SAAS,UAAU,MAAM,mBAAmB,EAAE,QAAQ,GAAG,MAAM,IAAI,KAAA;CACrE;AACF;;;;;;;;;;;;;AAcA,eAAsB,qBACpB,KACA,QACyB;CACzB,IAAI,CAAC,IAAI,SACP,OAAO;CAGT,MAAM,EAAE,QAAQ,YAAY,MAAM,mBAAmB,IAAI,SAAS,MAAM;CAGxE,IAAI,WAAW,WAAW,CAAC,SAAS;EAClC,OAAO,MAAM,EAAE,OAAO,GAAG,iCAAiC;EAC1D,OAAO;CACT;CAEA,IAAI,IAAI,aACN,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,IAAI,aAAa,MAAM;EACxD,MAAM,cAAc,OAAO,QAAQ,gBAAgB,WAAW,QAAQ,cAAc,KAAA;EAEpF,IAAI,aAEF,QAAQ,SAAS;GAAE,GAAG,QAAQ;GAAQ;EAAY;CAEtD,SAAS,OAAO;EAEd,OAAO,KAAK,EAAE,MAAM,GAAG,sCAAsC;CAC/D;CAGF,IAAI,IAAI,SAAS;EACf,MAAM,UAAU,MAAM,oBAAoB,IAAI,SAAS,MAAM;EAC7D,IAAI,SACF,QAAQ,SAAS;GAAE,GAAG,QAAQ;GAAQ;EAAQ;CAElD;CAEA,OAAO;AACT;;;;;;;;;AAUA,eAAsB,oBACpB,KACA,QAC6B;CAC7B,IAAI;EACF,MAAM,UAAU,MAAM,WAAW,KAAK,MAAM;EAC5C,OAAO,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU,KAAA;CACjE,SAAS,OAAO;EACd,OAAO,KAAK,EAAE,MAAM,GAAG,kCAAkC;EACzD;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventuras/fides-auth",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Framework-agnostic OAuth/OIDC authentication library with PKCE, session management, and pluggable logging",
5
5
  "keywords": [
6
6
  "oauth",