@workos/authkit-session
Advanced tools
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import type { CookieOptions } from '../session/types.js'; | ||
| /** Name of the PKCE verifier cookie on the wire. */ | ||
| export declare const PKCE_COOKIE_NAME = "wos-auth-verifier"; | ||
| /** | ||
| * PKCE verifier cookie lifetime (seconds). Matches the 10-minute convention | ||
| * used by Arctic, openid-client, Clerk, and Okta for short-lived OAuth state | ||
| * cookies — long enough to accommodate slow sign-ins (out-of-band 2FA, tab | ||
| * switching) and short enough to keep the replay window tight if the sealed | ||
| * blob leaks. | ||
| * | ||
| * The same value is used as both the `Max-Age` cookie attribute and the | ||
| * sealed payload's TTL, so both layers reject a stale blob. | ||
| */ | ||
| export declare const PKCE_COOKIE_MAX_AGE = 600; | ||
| /** | ||
| * Compute PKCE verifier cookie options from config + the resolved redirect URI. | ||
| * | ||
| * Behavior: | ||
| * - `sameSite: 'strict'` is downgraded to `'lax'` so the cookie survives the | ||
| * cross-site redirect back from WorkOS. | ||
| * - `sameSite: 'none'` is preserved (iframe/embed flows require it). | ||
| * - `secure` is forced to `true` when `sameSite === 'none'` (required by | ||
| * modern browsers). Otherwise it is inferred from the redirect URI's | ||
| * protocol, defaulting fail-closed to `true` on invalid/missing URLs. | ||
| * - `path` is always `/`. The cookie gets sent on every same-origin request | ||
| * during the 10-minute window, which is fine — it's HttpOnly and expires | ||
| * quickly. The tradeoff favors DX: path-scoped cookies are invisible in | ||
| * Chrome DevTools' Application panel from any page outside the scoped | ||
| * path, which makes PKCE wiring look broken even when it works. | ||
| * | ||
| * Internal helper — not exported from the package. Callers get cookie options | ||
| * indirectly via `AuthService.createSignIn` / `clearPendingVerifier`. | ||
| */ | ||
| export declare function getPKCECookieOptions(config: AuthKitConfig, redirectUri?: string): CookieOptions; | ||
| //# sourceMappingURL=cookieOptions.d.ts.map |
| {"version":3,"file":"cookieOptions.d.ts","sourceRoot":"","sources":["../../../src/core/pkce/cookieOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,oDAAoD;AACpD,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,MAAM,CAAC;AAEvC;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,EACrB,WAAW,CAAC,EAAE,MAAM,GACnB,aAAa,CAuBf"} |
| /** Name of the PKCE verifier cookie on the wire. */ | ||
| export const PKCE_COOKIE_NAME = 'wos-auth-verifier'; | ||
| /** | ||
| * PKCE verifier cookie lifetime (seconds). Matches the 10-minute convention | ||
| * used by Arctic, openid-client, Clerk, and Okta for short-lived OAuth state | ||
| * cookies — long enough to accommodate slow sign-ins (out-of-band 2FA, tab | ||
| * switching) and short enough to keep the replay window tight if the sealed | ||
| * blob leaks. | ||
| * | ||
| * The same value is used as both the `Max-Age` cookie attribute and the | ||
| * sealed payload's TTL, so both layers reject a stale blob. | ||
| */ | ||
| export const PKCE_COOKIE_MAX_AGE = 600; | ||
| /** | ||
| * Compute PKCE verifier cookie options from config + the resolved redirect URI. | ||
| * | ||
| * Behavior: | ||
| * - `sameSite: 'strict'` is downgraded to `'lax'` so the cookie survives the | ||
| * cross-site redirect back from WorkOS. | ||
| * - `sameSite: 'none'` is preserved (iframe/embed flows require it). | ||
| * - `secure` is forced to `true` when `sameSite === 'none'` (required by | ||
| * modern browsers). Otherwise it is inferred from the redirect URI's | ||
| * protocol, defaulting fail-closed to `true` on invalid/missing URLs. | ||
| * - `path` is always `/`. The cookie gets sent on every same-origin request | ||
| * during the 10-minute window, which is fine — it's HttpOnly and expires | ||
| * quickly. The tradeoff favors DX: path-scoped cookies are invisible in | ||
| * Chrome DevTools' Application panel from any page outside the scoped | ||
| * path, which makes PKCE wiring look broken even when it works. | ||
| * | ||
| * Internal helper — not exported from the package. Callers get cookie options | ||
| * indirectly via `AuthService.createSignIn` / `clearPendingVerifier`. | ||
| */ | ||
| export function getPKCECookieOptions(config, redirectUri) { | ||
| const configuredSameSite = (config.cookieSameSite ?? 'lax').toLowerCase(); | ||
| const sameSite = configuredSameSite === 'none' ? 'none' : 'lax'; | ||
| const urlString = redirectUri ?? config.redirectUri; | ||
| let secure = true; | ||
| if (urlString && sameSite !== 'none') { | ||
| try { | ||
| secure = new URL(urlString).protocol === 'https:'; | ||
| } | ||
| catch { | ||
| // Fail-closed: secure stays true. | ||
| } | ||
| } | ||
| return { | ||
| path: '/', | ||
| httpOnly: true, | ||
| secure, | ||
| sameSite, | ||
| maxAge: PKCE_COOKIE_MAX_AGE, | ||
| ...(config.cookieDomain ? { domain: config.cookieDomain } : {}), | ||
| }; | ||
| } | ||
| //# sourceMappingURL=cookieOptions.js.map |
| {"version":3,"file":"cookieOptions.js","sourceRoot":"","sources":["../../../src/core/pkce/cookieOptions.ts"],"names":[],"mappings":"AAGA,oDAAoD;AACpD,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAqB,EACrB,WAAoB;IAEpB,MAAM,kBAAkB,GAAG,CAAC,MAAM,CAAC,cAAc,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1E,MAAM,QAAQ,GACZ,kBAAkB,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAEjD,MAAM,SAAS,GAAG,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC;IACpD,IAAI,MAAM,GAAG,IAAI,CAAC;IAClB,IAAI,SAAS,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACrC,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,kCAAkC;QACpC,CAAC;IACH,CAAC;IAED,OAAO;QACL,IAAI,EAAE,GAAG;QACT,QAAQ,EAAE,IAAI;QACd,MAAM;QACN,QAAQ;QACR,MAAM,EAAE,mBAAmB;QAC3B,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChE,CAAC;AACJ,CAAC"} |
| import type { WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import type { CookieOptions, GetAuthorizationUrlOptions, SessionEncryption } from '../session/types.js'; | ||
| /** | ||
| * Maximum UTF-8 byte length for caller-supplied `options.state`. Enforced | ||
| * as an early, user-facing check so large custom state fails with a clear | ||
| * error before any crypto work — ahead of the authoritative sealed-cookie | ||
| * length guard below. | ||
| */ | ||
| export declare const PKCE_MAX_STATE_BYTES = 2048; | ||
| /** | ||
| * Maximum serialized `Set-Cookie` header length (bytes) for the verifier. | ||
| * Browsers start dropping cookies past ~4096 bytes per RFC 6265; 3800 | ||
| * leaves headroom for proxies that cap lower and for the handful of | ||
| * cookie attributes whose size isn't known until serialization time | ||
| * (notably `Domain=` when `cookieDomain` is set). | ||
| */ | ||
| export declare const PKCE_MAX_COOKIE_BYTES = 3800; | ||
| /** | ||
| * Internal authorization-URL generation result. | ||
| * | ||
| * Not exported: AuthService consumes `sealedState` + `cookieOptions` to write | ||
| * the verifier cookie via `storage.setCookie`, then returns only `{ url, response?, headers? }` | ||
| * to callers. | ||
| */ | ||
| export interface GeneratedAuthorizationUrl { | ||
| url: string; | ||
| sealedState: string; | ||
| cookieOptions: CookieOptions; | ||
| } | ||
| /** | ||
| * Generate a WorkOS authorization URL bound to a PKCE verifier. | ||
| * | ||
| * Returns the URL, the sealed state blob (used as both the OAuth `state` | ||
| * query param AND the cookie value — identical string; the callback does a | ||
| * byte-compare before decrypting), and the cookie options the verifier | ||
| * cookie should be written with. | ||
| */ | ||
| export declare function generateAuthorizationUrl(params: { | ||
| client: WorkOS; | ||
| config: AuthKitConfig; | ||
| encryption: SessionEncryption; | ||
| options: GetAuthorizationUrlOptions; | ||
| }): Promise<GeneratedAuthorizationUrl>; | ||
| //# sourceMappingURL=generateAuthorizationUrl.d.ts.map |
| {"version":3,"file":"generateAuthorizationUrl.d.ts","sourceRoot":"","sources":["../../../src/core/pkce/generateAuthorizationUrl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGxD,OAAO,KAAK,EACV,aAAa,EACb,0BAA0B,EAC1B,iBAAiB,EAClB,MAAM,qBAAqB,CAAC;AAI7B;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,iBAAiB,CAAC;IAC9B,OAAO,EAAE,0BAA0B,CAAC;CACrC,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAmErC"} |
| import { PKCEPayloadTooLargeError } from '../errors.js'; | ||
| import { serializeCookie } from '../session/serializeCookie.js'; | ||
| import { getPKCECookieOptions, PKCE_COOKIE_NAME } from './cookieOptions.js'; | ||
| import { sealState } from './state.js'; | ||
| /** | ||
| * Maximum UTF-8 byte length for caller-supplied `options.state`. Enforced | ||
| * as an early, user-facing check so large custom state fails with a clear | ||
| * error before any crypto work — ahead of the authoritative sealed-cookie | ||
| * length guard below. | ||
| */ | ||
| export const PKCE_MAX_STATE_BYTES = 2048; | ||
| /** | ||
| * Maximum serialized `Set-Cookie` header length (bytes) for the verifier. | ||
| * Browsers start dropping cookies past ~4096 bytes per RFC 6265; 3800 | ||
| * leaves headroom for proxies that cap lower and for the handful of | ||
| * cookie attributes whose size isn't known until serialization time | ||
| * (notably `Domain=` when `cookieDomain` is set). | ||
| */ | ||
| export const PKCE_MAX_COOKIE_BYTES = 3800; | ||
| /** | ||
| * Generate a WorkOS authorization URL bound to a PKCE verifier. | ||
| * | ||
| * Returns the URL, the sealed state blob (used as both the OAuth `state` | ||
| * query param AND the cookie value — identical string; the callback does a | ||
| * byte-compare before decrypting), and the cookie options the verifier | ||
| * cookie should be written with. | ||
| */ | ||
| export async function generateAuthorizationUrl(params) { | ||
| const { client, config, encryption, options } = params; | ||
| const redirectUri = options.redirectUri ?? config.redirectUri; | ||
| // Early bound on caller-supplied `state` so oversized input fails with a | ||
| // clear error before any crypto work. The authoritative guard on the | ||
| // serialized cookie runs below — this one just avoids confusing users. | ||
| if (options.state !== undefined) { | ||
| const stateBytes = new TextEncoder().encode(options.state).byteLength; | ||
| if (stateBytes > PKCE_MAX_STATE_BYTES) { | ||
| throw new PKCEPayloadTooLargeError(`Custom OAuth state is ${stateBytes} bytes, exceeds supported limit of ${PKCE_MAX_STATE_BYTES} bytes. ` + | ||
| `The sealed state is stored as the wos-auth-verifier cookie; oversized values would be silently dropped by the browser.`); | ||
| } | ||
| } | ||
| const pkce = await client.pkce.generate(); | ||
| const nonce = crypto.randomUUID(); | ||
| const sealedState = await sealState(encryption, config.cookiePassword, { | ||
| nonce, | ||
| codeVerifier: pkce.codeVerifier, | ||
| returnPathname: options.returnPathname, | ||
| customState: options.state, | ||
| // Persisted so handleCallback's verifier-delete cookie uses the same | ||
| // computed attributes as the original setCookie — notably `secure`, | ||
| // which depends on the redirect URI's protocol. | ||
| redirectUri: options.redirectUri, | ||
| }); | ||
| // Authoritative guard: measure the actual Set-Cookie header the adapter | ||
| // will emit. Catches cases the input-only check can't, like oversized | ||
| // returnPathname combined with near-max state, or an unusually long | ||
| // cookieDomain attribute. | ||
| const cookieOptions = getPKCECookieOptions(config, redirectUri); | ||
| const serialized = serializeCookie(PKCE_COOKIE_NAME, sealedState, cookieOptions); | ||
| const cookieBytes = new TextEncoder().encode(serialized).byteLength; | ||
| if (cookieBytes > PKCE_MAX_COOKIE_BYTES) { | ||
| throw new PKCEPayloadTooLargeError(`Sealed PKCE verifier cookie is ${cookieBytes} bytes, exceeds supported limit of ${PKCE_MAX_COOKIE_BYTES} bytes. ` + | ||
| `Reduce the size of options.state, options.returnPathname, or options.redirectUri.`); | ||
| } | ||
| const url = client.userManagement.getAuthorizationUrl({ | ||
| provider: 'authkit', | ||
| clientId: config.clientId, | ||
| redirectUri, | ||
| screenHint: options.screenHint, | ||
| organizationId: options.organizationId, | ||
| loginHint: options.loginHint, | ||
| prompt: options.prompt, | ||
| state: sealedState, | ||
| codeChallenge: pkce.codeChallenge, | ||
| codeChallengeMethod: pkce.codeChallengeMethod, | ||
| }); | ||
| return { | ||
| url, | ||
| sealedState, | ||
| cookieOptions, | ||
| }; | ||
| } | ||
| //# sourceMappingURL=generateAuthorizationUrl.js.map |
| {"version":3,"file":"generateAuthorizationUrl.js","sourceRoot":"","sources":["../../../src/core/pkce/generateAuthorizationUrl.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAMhE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAEzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAe1C;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,MAK9C;IACC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IACvD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC,WAAW,CAAC;IAE9D,yEAAyE;IACzE,qEAAqE;IACrE,uEAAuE;IACvE,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,UAAU,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC;QACtE,IAAI,UAAU,GAAG,oBAAoB,EAAE,CAAC;YACtC,MAAM,IAAI,wBAAwB,CAChC,yBAAyB,UAAU,sCAAsC,oBAAoB,UAAU;gBACrG,wHAAwH,CAC3H,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC1C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IAElC,MAAM,WAAW,GAAG,MAAM,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,cAAc,EAAE;QACrE,KAAK;QACL,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,WAAW,EAAE,OAAO,CAAC,KAAK;QAC1B,qEAAqE;QACrE,oEAAoE;QACpE,gDAAgD;QAChD,WAAW,EAAE,OAAO,CAAC,WAAW;KACjC,CAAC,CAAC;IAEH,wEAAwE;IACxE,sEAAsE;IACtE,oEAAoE;IACpE,0BAA0B;IAC1B,MAAM,aAAa,GAAG,oBAAoB,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,eAAe,CAChC,gBAAgB,EAChB,WAAW,EACX,aAAa,CACd,CAAC;IACF,MAAM,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC;IACpE,IAAI,WAAW,GAAG,qBAAqB,EAAE,CAAC;QACxC,MAAM,IAAI,wBAAwB,CAChC,kCAAkC,WAAW,sCAAsC,qBAAqB,UAAU;YAChH,mFAAmF,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC;QACpD,QAAQ,EAAE,SAAS;QACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,WAAW;QACX,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,WAAW;QAClB,aAAa,EAAE,IAAI,CAAC,aAAa;QACjC,mBAAmB,EAAE,IAAI,CAAC,mBAAmB;KAC9C,CAAC,CAAC;IAEH,OAAO;QACL,GAAG;QACH,WAAW;QACX,aAAa;KACd,CAAC;AACJ,CAAC"} |
| import * as v from 'valibot'; | ||
| import type { SessionEncryption } from '../session/types.js'; | ||
| /** | ||
| * Runtime schema for the sealed PKCE state blob. | ||
| * | ||
| * Validated at unseal time as defense-in-depth against any future code path | ||
| * accidentally sealing the wrong shape. Shape mismatch is treated as an | ||
| * integrity failure (same category as a tampered blob). | ||
| * | ||
| * `issuedAt` (ms since epoch) lets `unsealState` enforce the TTL on its own | ||
| * rather than trusting the encryption adapter's `ttl` handling. Custom | ||
| * `SessionEncryption` implementations may silently ignore the `ttl` field — | ||
| * the payload-level age check closes that gap. | ||
| */ | ||
| export declare const StateSchema: v.ObjectSchema<{ | ||
| readonly nonce: v.StringSchema<undefined>; | ||
| readonly codeVerifier: v.StringSchema<undefined>; | ||
| readonly issuedAt: v.NumberSchema<undefined>; | ||
| readonly returnPathname: v.OptionalSchema<v.StringSchema<undefined>, undefined>; | ||
| readonly customState: v.OptionalSchema<v.StringSchema<undefined>, undefined>; | ||
| /** | ||
| * The per-request `redirectUri` override resolved at sign-in time, if the | ||
| * caller supplied one. Persisted so `handleCallback` can recompute the | ||
| * exact cookie options used at `setCookie` time — notably `secure`, which | ||
| * is inferred from the redirect URI's protocol. Omitted when the caller | ||
| * relied on the configured default. | ||
| */ | ||
| readonly redirectUri: v.OptionalSchema<v.StringSchema<undefined>, undefined>; | ||
| }, undefined>; | ||
| export type PKCEState = v.InferOutput<typeof StateSchema>; | ||
| export type PKCEStateInput = Omit<PKCEState, 'issuedAt'>; | ||
| /** | ||
| * Seal a PKCE state object for use as both the OAuth `state` query param | ||
| * and the `wos-auth-verifier` cookie value. Seal embeds a 600s TTL and | ||
| * stamps `issuedAt` so the unseal path can enforce age independently. | ||
| */ | ||
| export declare function sealState(encryption: SessionEncryption, password: string, state: PKCEStateInput): Promise<string>; | ||
| /** | ||
| * Unseal a PKCE state blob, enforcing TTL and shape. | ||
| * | ||
| * Age is verified twice: | ||
| * 1. The encryption adapter's `ttl` check (iron-webcrypto enforces this by | ||
| * default — custom adapters MAY ignore it). | ||
| * 2. A payload-level `issuedAt` comparison against `Date.now()`. This is the | ||
| * authoritative check — it runs regardless of how the adapter treats | ||
| * `ttl`. | ||
| * | ||
| * Any failure — expired TTL, tamper, wrong password, or schema mismatch — | ||
| * is wrapped as `SessionEncryptionError`. Callers differentiate via the | ||
| * `cause` chain, not the message string. | ||
| */ | ||
| export declare function unsealState(encryption: SessionEncryption, password: string, sealed: string): Promise<PKCEState>; | ||
| //# sourceMappingURL=state.d.ts.map |
| {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../../src/core/pkce/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAE7B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAgB7D;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW;;;;;;IAMtB;;;;;;OAMG;;aAEH,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,WAAW,CAAC,OAAO,WAAW,CAAC,CAAC;AAE1D,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAEzD;;;;GAIG;AACH,wBAAsB,SAAS,CAC7B,UAAU,EAAE,iBAAiB,EAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAC/B,UAAU,EAAE,iBAAiB,EAC7B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,SAAS,CAAC,CAgCpB"} |
| import * as v from 'valibot'; | ||
| import { SessionEncryptionError } from '../errors.js'; | ||
| import { PKCE_COOKIE_MAX_AGE } from './cookieOptions.js'; | ||
| /** | ||
| * Clock-skew tolerance (ms) for the payload-level `issuedAt` check. | ||
| * | ||
| * Applied asymmetrically, only to the negative (future-`issuedAt`) side: | ||
| * allows a sign-in issued on node A to be verified on node B whose clock | ||
| * is up to 60s behind without failing a legitimate callback. The upper | ||
| * bound (expiry) is strict — `PKCE_COOKIE_MAX_AGE` with no grace — so | ||
| * this payload check is always at least as strict as iron-webcrypto's | ||
| * symmetric 60s skew on `ttl`, making it the authoritative expiry guard | ||
| * regardless of how a custom `SessionEncryption` adapter treats `ttl`. | ||
| */ | ||
| const PKCE_CLOCK_SKEW_MS = 60_000; | ||
| /** | ||
| * Runtime schema for the sealed PKCE state blob. | ||
| * | ||
| * Validated at unseal time as defense-in-depth against any future code path | ||
| * accidentally sealing the wrong shape. Shape mismatch is treated as an | ||
| * integrity failure (same category as a tampered blob). | ||
| * | ||
| * `issuedAt` (ms since epoch) lets `unsealState` enforce the TTL on its own | ||
| * rather than trusting the encryption adapter's `ttl` handling. Custom | ||
| * `SessionEncryption` implementations may silently ignore the `ttl` field — | ||
| * the payload-level age check closes that gap. | ||
| */ | ||
| export const StateSchema = v.object({ | ||
| nonce: v.string(), | ||
| codeVerifier: v.string(), | ||
| issuedAt: v.number(), | ||
| returnPathname: v.optional(v.string()), | ||
| customState: v.optional(v.string()), | ||
| /** | ||
| * The per-request `redirectUri` override resolved at sign-in time, if the | ||
| * caller supplied one. Persisted so `handleCallback` can recompute the | ||
| * exact cookie options used at `setCookie` time — notably `secure`, which | ||
| * is inferred from the redirect URI's protocol. Omitted when the caller | ||
| * relied on the configured default. | ||
| */ | ||
| redirectUri: v.optional(v.string()), | ||
| }); | ||
| /** | ||
| * Seal a PKCE state object for use as both the OAuth `state` query param | ||
| * and the `wos-auth-verifier` cookie value. Seal embeds a 600s TTL and | ||
| * stamps `issuedAt` so the unseal path can enforce age independently. | ||
| */ | ||
| export async function sealState(encryption, password, state) { | ||
| return encryption.sealData({ ...state, issuedAt: Date.now() }, { | ||
| password, | ||
| ttl: PKCE_COOKIE_MAX_AGE, | ||
| }); | ||
| } | ||
| /** | ||
| * Unseal a PKCE state blob, enforcing TTL and shape. | ||
| * | ||
| * Age is verified twice: | ||
| * 1. The encryption adapter's `ttl` check (iron-webcrypto enforces this by | ||
| * default — custom adapters MAY ignore it). | ||
| * 2. A payload-level `issuedAt` comparison against `Date.now()`. This is the | ||
| * authoritative check — it runs regardless of how the adapter treats | ||
| * `ttl`. | ||
| * | ||
| * Any failure — expired TTL, tamper, wrong password, or schema mismatch — | ||
| * is wrapped as `SessionEncryptionError`. Callers differentiate via the | ||
| * `cause` chain, not the message string. | ||
| */ | ||
| export async function unsealState(encryption, password, sealed) { | ||
| let raw; | ||
| try { | ||
| raw = await encryption.unsealData(sealed, { | ||
| password, | ||
| ttl: PKCE_COOKIE_MAX_AGE, | ||
| }); | ||
| } | ||
| catch (cause) { | ||
| throw new SessionEncryptionError('Failed to unseal PKCE state', cause); | ||
| } | ||
| const result = v.safeParse(StateSchema, raw); | ||
| if (!result.success) { | ||
| throw new SessionEncryptionError('Malformed PKCE state payload', result.issues); | ||
| } | ||
| const ageMs = Date.now() - result.output.issuedAt; | ||
| if (ageMs < -PKCE_CLOCK_SKEW_MS || ageMs > PKCE_COOKIE_MAX_AGE * 1000) { | ||
| // Attach age context so production logs can diagnose clock-skew issues | ||
| // via the `cause` chain without leaking the sealed blob itself. | ||
| throw new SessionEncryptionError('PKCE state expired', new Error(`age=${ageMs}ms, maxAge=${PKCE_COOKIE_MAX_AGE * 1000}ms, skew=±${PKCE_CLOCK_SKEW_MS}ms`)); | ||
| } | ||
| return result.output; | ||
| } | ||
| //# sourceMappingURL=state.js.map |
| {"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/core/pkce/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;IACpB,cAAc,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACtC,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IACnC;;;;;;OAMG;IACH,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;CACpC,CAAC,CAAC;AAMH;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,UAA6B,EAC7B,QAAgB,EAChB,KAAqB;IAErB,OAAO,UAAU,CAAC,QAAQ,CACxB,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,EAClC;QACE,QAAQ;QACR,GAAG,EAAE,mBAAmB;KACzB,CACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,UAA6B,EAC7B,QAAgB,EAChB,MAAc;IAEd,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,UAAU,CAAC,UAAU,CAAU,MAAM,EAAE;YACjD,QAAQ;YACR,GAAG,EAAE,mBAAmB;SACzB,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,sBAAsB,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,sBAAsB,CAC9B,8BAA8B,EAC9B,MAAM,CAAC,MAAM,CACd,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;IAClD,IAAI,KAAK,GAAG,CAAC,kBAAkB,IAAI,KAAK,GAAG,mBAAmB,GAAG,IAAI,EAAE,CAAC;QACtE,uEAAuE;QACvE,gEAAgE;QAChE,MAAM,IAAI,sBAAsB,CAC9B,oBAAoB,EACpB,IAAI,KAAK,CACP,OAAO,KAAK,cAAc,mBAAmB,GAAG,IAAI,aAAa,kBAAkB,IAAI,CACxF,CACF,CAAC;IACJ,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC;AACvB,CAAC"} |
| import type { CookieOptions } from './types.js'; | ||
| /** | ||
| * Serialize a `Set-Cookie` header value from (name, value, options). | ||
| * | ||
| * Shared between `CookieSessionStorage` (writes the header on response) and | ||
| * the PKCE flow's pre-write size guard (must measure the exact bytes the | ||
| * browser will see, including attribute overhead, to detect oversized | ||
| * verifier cookies before they silently get dropped). | ||
| */ | ||
| export declare function serializeCookie(name: string, value: string, options: CookieOptions, { expired }?: { | ||
| expired?: boolean; | ||
| }): string; | ||
| //# sourceMappingURL=serializeCookie.d.ts.map |
| {"version":3,"file":"serializeCookie.d.ts","sourceRoot":"","sources":["../../../src/core/session/serializeCookie.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EACtB,EAAE,OAAO,EAAE,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,MAAM,CAiBR"} |
| /** | ||
| * Serialize a `Set-Cookie` header value from (name, value, options). | ||
| * | ||
| * Shared between `CookieSessionStorage` (writes the header on response) and | ||
| * the PKCE flow's pre-write size guard (must measure the exact bytes the | ||
| * browser will see, including attribute overhead, to detect oversized | ||
| * verifier cookies before they silently get dropped). | ||
| */ | ||
| export function serializeCookie(name, value, options, { expired } = {}) { | ||
| const parts = [`${name}=${encodeURIComponent(value)}`]; | ||
| if (options.path) | ||
| parts.push(`Path=${options.path}`); | ||
| if (options.domain) | ||
| parts.push(`Domain=${options.domain}`); | ||
| if (options.maxAge != null || expired) | ||
| parts.push(`Max-Age=${expired ? 0 : options.maxAge}`); | ||
| if (options.httpOnly) | ||
| parts.push('HttpOnly'); | ||
| if (options.secure) | ||
| parts.push('Secure'); | ||
| if (options.sameSite) { | ||
| const capitalized = options.sameSite.charAt(0).toUpperCase() + | ||
| options.sameSite.slice(1).toLowerCase(); | ||
| parts.push(`SameSite=${capitalized}`); | ||
| } | ||
| if (options.priority) | ||
| parts.push(`Priority=${options.priority}`); | ||
| if (options.partitioned) | ||
| parts.push('Partitioned'); | ||
| return parts.join('; '); | ||
| } | ||
| //# sourceMappingURL=serializeCookie.js.map |
| {"version":3,"file":"serializeCookie.js","sourceRoot":"","sources":["../../../src/core/session/serializeCookie.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAY,EACZ,KAAa,EACb,OAAsB,EACtB,EAAE,OAAO,KAA4B,EAAE;IAEvC,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACvD,IAAI,OAAO,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3D,IAAI,OAAO,CAAC,MAAM,IAAI,IAAI,IAAI,OAAO;QACnC,KAAK,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,IAAI,OAAO,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7C,IAAI,OAAO,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;QACrB,MAAM,WAAW,GACf,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;YACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC1C,KAAK,CAAC,IAAI,CAAC,YAAY,WAAW,EAAE,CAAC,CAAC;IACxC,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACjE,IAAI,OAAO,CAAC,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACnD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"} |
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import { getPKCECookieOptions } from './cookieOptions.js'; | ||
| const baseConfig: AuthKitConfig = { | ||
| clientId: 'client_test', | ||
| apiKey: 'sk_test', | ||
| redirectUri: 'https://app.example.com/callback', | ||
| cookiePassword: 'x'.repeat(48), | ||
| } as AuthKitConfig; | ||
| describe('getPKCECookieOptions', () => { | ||
| describe('sameSite behavior', () => { | ||
| it.each([ | ||
| ['strict', 'lax'], | ||
| ['lax', 'lax'], | ||
| ['none', 'none'], | ||
| ] as const)( | ||
| 'maps cookieSameSite=%s to sameSite=%s', | ||
| (configured, expected) => { | ||
| const config = { ...baseConfig, cookieSameSite: configured }; | ||
| const opts = getPKCECookieOptions(config); | ||
| expect(opts.sameSite).toBe(expected); | ||
| }, | ||
| ); | ||
| it('defaults to lax when cookieSameSite is undefined', () => { | ||
| const opts = getPKCECookieOptions(baseConfig); | ||
| expect(opts.sameSite).toBe('lax'); | ||
| }); | ||
| it('accepts mixed-case sameSite values', () => { | ||
| const config = { ...baseConfig, cookieSameSite: 'NONE' as any }; | ||
| const opts = getPKCECookieOptions(config); | ||
| expect(opts.sameSite).toBe('none'); | ||
| }); | ||
| }); | ||
| describe('secure flag', () => { | ||
| it('derives secure=true from https redirectUri', () => { | ||
| const opts = getPKCECookieOptions({ | ||
| ...baseConfig, | ||
| redirectUri: 'https://app.example.com/callback', | ||
| }); | ||
| expect(opts.secure).toBe(true); | ||
| }); | ||
| it('derives secure=false from http redirectUri', () => { | ||
| const opts = getPKCECookieOptions({ | ||
| ...baseConfig, | ||
| redirectUri: 'http://localhost:3000/callback', | ||
| }); | ||
| expect(opts.secure).toBe(false); | ||
| }); | ||
| it('prefers explicit redirectUri arg over config', () => { | ||
| const opts = getPKCECookieOptions( | ||
| { ...baseConfig, redirectUri: 'https://prod.example.com/callback' }, | ||
| 'http://localhost:3000/callback', | ||
| ); | ||
| expect(opts.secure).toBe(false); | ||
| }); | ||
| it('fail-closes to secure=true on invalid URL', () => { | ||
| const opts = getPKCECookieOptions({ | ||
| ...baseConfig, | ||
| redirectUri: 'not-a-valid-url', | ||
| }); | ||
| expect(opts.secure).toBe(true); | ||
| }); | ||
| it('forces secure=true when sameSite=none regardless of protocol', () => { | ||
| const opts = getPKCECookieOptions({ | ||
| ...baseConfig, | ||
| cookieSameSite: 'none', | ||
| redirectUri: 'http://localhost:3000/callback', | ||
| }); | ||
| expect(opts.secure).toBe(true); | ||
| }); | ||
| }); | ||
| describe('invariants', () => { | ||
| it('always sets httpOnly=true', () => { | ||
| const opts = getPKCECookieOptions(baseConfig); | ||
| expect(opts.httpOnly).toBe(true); | ||
| }); | ||
| it('always sets maxAge=600', () => { | ||
| const opts = getPKCECookieOptions(baseConfig); | ||
| expect(opts.maxAge).toBe(600); | ||
| }); | ||
| }); | ||
| describe('path', () => { | ||
| it.each([ | ||
| ['config callback path', 'https://app.example.com/callback'], | ||
| ['nested callback path', 'https://app.example.com/auth/v2/callback'], | ||
| ['host-only', 'https://app.example.com'], | ||
| ['invalid url', 'not-a-valid-url'], | ||
| ])( | ||
| "is always '/' regardless of redirectUri (%s)", | ||
| (_label, redirectUri) => { | ||
| const opts = getPKCECookieOptions({ ...baseConfig, redirectUri }); | ||
| expect(opts.path).toBe('/'); | ||
| }, | ||
| ); | ||
| it("is '/' even when per-call redirectUri override is supplied", () => { | ||
| const opts = getPKCECookieOptions( | ||
| baseConfig, | ||
| 'https://app.example.com/some/other/path', | ||
| ); | ||
| expect(opts.path).toBe('/'); | ||
| }); | ||
| }); | ||
| describe('domain passthrough', () => { | ||
| it('includes domain when configured', () => { | ||
| const opts = getPKCECookieOptions({ | ||
| ...baseConfig, | ||
| cookieDomain: '.example.com', | ||
| }); | ||
| expect(opts.domain).toBe('.example.com'); | ||
| }); | ||
| it('omits domain when not configured', () => { | ||
| const opts = getPKCECookieOptions(baseConfig); | ||
| expect(opts.domain).toBeUndefined(); | ||
| }); | ||
| }); | ||
| }); |
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import type { CookieOptions } from '../session/types.js'; | ||
| /** Name of the PKCE verifier cookie on the wire. */ | ||
| export const PKCE_COOKIE_NAME = 'wos-auth-verifier'; | ||
| /** | ||
| * PKCE verifier cookie lifetime (seconds). Matches the 10-minute convention | ||
| * used by Arctic, openid-client, Clerk, and Okta for short-lived OAuth state | ||
| * cookies — long enough to accommodate slow sign-ins (out-of-band 2FA, tab | ||
| * switching) and short enough to keep the replay window tight if the sealed | ||
| * blob leaks. | ||
| * | ||
| * The same value is used as both the `Max-Age` cookie attribute and the | ||
| * sealed payload's TTL, so both layers reject a stale blob. | ||
| */ | ||
| export const PKCE_COOKIE_MAX_AGE = 600; | ||
| /** | ||
| * Compute PKCE verifier cookie options from config + the resolved redirect URI. | ||
| * | ||
| * Behavior: | ||
| * - `sameSite: 'strict'` is downgraded to `'lax'` so the cookie survives the | ||
| * cross-site redirect back from WorkOS. | ||
| * - `sameSite: 'none'` is preserved (iframe/embed flows require it). | ||
| * - `secure` is forced to `true` when `sameSite === 'none'` (required by | ||
| * modern browsers). Otherwise it is inferred from the redirect URI's | ||
| * protocol, defaulting fail-closed to `true` on invalid/missing URLs. | ||
| * - `path` is always `/`. The cookie gets sent on every same-origin request | ||
| * during the 10-minute window, which is fine — it's HttpOnly and expires | ||
| * quickly. The tradeoff favors DX: path-scoped cookies are invisible in | ||
| * Chrome DevTools' Application panel from any page outside the scoped | ||
| * path, which makes PKCE wiring look broken even when it works. | ||
| * | ||
| * Internal helper — not exported from the package. Callers get cookie options | ||
| * indirectly via `AuthService.createSignIn` / `clearPendingVerifier`. | ||
| */ | ||
| export function getPKCECookieOptions( | ||
| config: AuthKitConfig, | ||
| redirectUri?: string, | ||
| ): CookieOptions { | ||
| const configuredSameSite = (config.cookieSameSite ?? 'lax').toLowerCase(); | ||
| const sameSite: 'lax' | 'none' = | ||
| configuredSameSite === 'none' ? 'none' : 'lax'; | ||
| const urlString = redirectUri ?? config.redirectUri; | ||
| let secure = true; | ||
| if (urlString && sameSite !== 'none') { | ||
| try { | ||
| secure = new URL(urlString).protocol === 'https:'; | ||
| } catch { | ||
| // Fail-closed: secure stays true. | ||
| } | ||
| } | ||
| return { | ||
| path: '/', | ||
| httpOnly: true, | ||
| secure, | ||
| sameSite, | ||
| maxAge: PKCE_COOKIE_MAX_AGE, | ||
| ...(config.cookieDomain ? { domain: config.cookieDomain } : {}), | ||
| }; | ||
| } |
| import type { WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import { PKCEPayloadTooLargeError } from '../errors.js'; | ||
| import { serializeCookie } from '../session/serializeCookie.js'; | ||
| import type { | ||
| CookieOptions, | ||
| GetAuthorizationUrlOptions, | ||
| SessionEncryption, | ||
| } from '../session/types.js'; | ||
| import { getPKCECookieOptions, PKCE_COOKIE_NAME } from './cookieOptions.js'; | ||
| import { sealState } from './state.js'; | ||
| /** | ||
| * Maximum UTF-8 byte length for caller-supplied `options.state`. Enforced | ||
| * as an early, user-facing check so large custom state fails with a clear | ||
| * error before any crypto work — ahead of the authoritative sealed-cookie | ||
| * length guard below. | ||
| */ | ||
| export const PKCE_MAX_STATE_BYTES = 2048; | ||
| /** | ||
| * Maximum serialized `Set-Cookie` header length (bytes) for the verifier. | ||
| * Browsers start dropping cookies past ~4096 bytes per RFC 6265; 3800 | ||
| * leaves headroom for proxies that cap lower and for the handful of | ||
| * cookie attributes whose size isn't known until serialization time | ||
| * (notably `Domain=` when `cookieDomain` is set). | ||
| */ | ||
| export const PKCE_MAX_COOKIE_BYTES = 3800; | ||
| /** | ||
| * Internal authorization-URL generation result. | ||
| * | ||
| * Not exported: AuthService consumes `sealedState` + `cookieOptions` to write | ||
| * the verifier cookie via `storage.setCookie`, then returns only `{ url, response?, headers? }` | ||
| * to callers. | ||
| */ | ||
| export interface GeneratedAuthorizationUrl { | ||
| url: string; | ||
| sealedState: string; | ||
| cookieOptions: CookieOptions; | ||
| } | ||
| /** | ||
| * Generate a WorkOS authorization URL bound to a PKCE verifier. | ||
| * | ||
| * Returns the URL, the sealed state blob (used as both the OAuth `state` | ||
| * query param AND the cookie value — identical string; the callback does a | ||
| * byte-compare before decrypting), and the cookie options the verifier | ||
| * cookie should be written with. | ||
| */ | ||
| export async function generateAuthorizationUrl(params: { | ||
| client: WorkOS; | ||
| config: AuthKitConfig; | ||
| encryption: SessionEncryption; | ||
| options: GetAuthorizationUrlOptions; | ||
| }): Promise<GeneratedAuthorizationUrl> { | ||
| const { client, config, encryption, options } = params; | ||
| const redirectUri = options.redirectUri ?? config.redirectUri; | ||
| // Early bound on caller-supplied `state` so oversized input fails with a | ||
| // clear error before any crypto work. The authoritative guard on the | ||
| // serialized cookie runs below — this one just avoids confusing users. | ||
| if (options.state !== undefined) { | ||
| const stateBytes = new TextEncoder().encode(options.state).byteLength; | ||
| if (stateBytes > PKCE_MAX_STATE_BYTES) { | ||
| throw new PKCEPayloadTooLargeError( | ||
| `Custom OAuth state is ${stateBytes} bytes, exceeds supported limit of ${PKCE_MAX_STATE_BYTES} bytes. ` + | ||
| `The sealed state is stored as the wos-auth-verifier cookie; oversized values would be silently dropped by the browser.`, | ||
| ); | ||
| } | ||
| } | ||
| const pkce = await client.pkce.generate(); | ||
| const nonce = crypto.randomUUID(); | ||
| const sealedState = await sealState(encryption, config.cookiePassword, { | ||
| nonce, | ||
| codeVerifier: pkce.codeVerifier, | ||
| returnPathname: options.returnPathname, | ||
| customState: options.state, | ||
| // Persisted so handleCallback's verifier-delete cookie uses the same | ||
| // computed attributes as the original setCookie — notably `secure`, | ||
| // which depends on the redirect URI's protocol. | ||
| redirectUri: options.redirectUri, | ||
| }); | ||
| // Authoritative guard: measure the actual Set-Cookie header the adapter | ||
| // will emit. Catches cases the input-only check can't, like oversized | ||
| // returnPathname combined with near-max state, or an unusually long | ||
| // cookieDomain attribute. | ||
| const cookieOptions = getPKCECookieOptions(config, redirectUri); | ||
| const serialized = serializeCookie( | ||
| PKCE_COOKIE_NAME, | ||
| sealedState, | ||
| cookieOptions, | ||
| ); | ||
| const cookieBytes = new TextEncoder().encode(serialized).byteLength; | ||
| if (cookieBytes > PKCE_MAX_COOKIE_BYTES) { | ||
| throw new PKCEPayloadTooLargeError( | ||
| `Sealed PKCE verifier cookie is ${cookieBytes} bytes, exceeds supported limit of ${PKCE_MAX_COOKIE_BYTES} bytes. ` + | ||
| `Reduce the size of options.state, options.returnPathname, or options.redirectUri.`, | ||
| ); | ||
| } | ||
| const url = client.userManagement.getAuthorizationUrl({ | ||
| provider: 'authkit', | ||
| clientId: config.clientId, | ||
| redirectUri, | ||
| screenHint: options.screenHint, | ||
| organizationId: options.organizationId, | ||
| loginHint: options.loginHint, | ||
| prompt: options.prompt, | ||
| state: sealedState, | ||
| codeChallenge: pkce.codeChallenge, | ||
| codeChallengeMethod: pkce.codeChallengeMethod, | ||
| }); | ||
| return { | ||
| url, | ||
| sealedState, | ||
| cookieOptions, | ||
| }; | ||
| } |
| import { AuthKitCore } from '../AuthKitCore.js'; | ||
| import sessionEncryption from '../encryption/ironWebcryptoEncryption.js'; | ||
| import { | ||
| OAuthStateMismatchError, | ||
| PKCECookieMissingError, | ||
| PKCEPayloadTooLargeError, | ||
| } from '../errors.js'; | ||
| import { | ||
| generateAuthorizationUrl, | ||
| PKCE_MAX_COOKIE_BYTES, | ||
| PKCE_MAX_STATE_BYTES, | ||
| } from './generateAuthorizationUrl.js'; | ||
| const config = { | ||
| clientId: 'test-client-id', | ||
| apiKey: 'test-api-key', | ||
| redirectUri: 'https://app.example.com/callback', | ||
| cookiePassword: 'this-is-a-test-password-that-is-32-characters-long!', | ||
| } as const; | ||
| const mockClient = { | ||
| pkce: { | ||
| generate: async () => ({ | ||
| codeVerifier: 'verifier-' + crypto.randomUUID(), | ||
| codeChallenge: 'challenge-' + crypto.randomUUID(), | ||
| codeChallengeMethod: 'S256', | ||
| }), | ||
| }, | ||
| userManagement: { | ||
| getAuthorizationUrl: (opts: any) => { | ||
| const params = new URLSearchParams({ | ||
| client_id: opts.clientId, | ||
| redirect_uri: opts.redirectUri, | ||
| state: opts.state ?? '', | ||
| code_challenge: opts.codeChallenge ?? '', | ||
| code_challenge_method: opts.codeChallengeMethod ?? '', | ||
| }); | ||
| return `https://api.workos.com/sso/authorize?${params.toString()}`; | ||
| }, | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| }, | ||
| } as const; | ||
| function makeCore() { | ||
| return new AuthKitCore(config as any, mockClient as any, sessionEncryption); | ||
| } | ||
| function generate( | ||
| options: Parameters<typeof generateAuthorizationUrl>[0]['options'] = {}, | ||
| ) { | ||
| return generateAuthorizationUrl({ | ||
| client: mockClient as any, | ||
| config: config as any, | ||
| encryption: sessionEncryption, | ||
| options, | ||
| }); | ||
| } | ||
| // Tamper, missing cookie, missing state, TTL, and single-byte custom-state | ||
| // round-trip are covered in state.spec.ts and AuthKitCore.spec.ts. These | ||
| // tests exercise the integration seam (generateAuthorizationUrl → | ||
| // verifyCallbackState) for behaviors that only emerge when both modules | ||
| // run together. | ||
| describe('PKCE end-to-end round-trip', () => { | ||
| it('verifyCallbackState recovers the sealed state after generateAuthorizationUrl', async () => { | ||
| const core = makeCore(); | ||
| const { sealedState } = await generate({ | ||
| returnPathname: '/dashboard', | ||
| state: 'user-opaque', | ||
| }); | ||
| const result = await core.verifyCallbackState({ | ||
| stateFromUrl: sealedState, | ||
| cookieValue: sealedState, | ||
| }); | ||
| expect(result.returnPathname).toBe('/dashboard'); | ||
| expect(result.customState).toBe('user-opaque'); | ||
| expect(typeof result.codeVerifier).toBe('string'); | ||
| expect(typeof result.nonce).toBe('string'); | ||
| }); | ||
| it('empty-string cookieValue is treated as missing (falsy check)', async () => { | ||
| const core = makeCore(); | ||
| const { sealedState } = await generate(); | ||
| await expect( | ||
| core.verifyCallbackState({ stateFromUrl: sealedState, cookieValue: '' }), | ||
| ).rejects.toThrow(PKCECookieMissingError); | ||
| }); | ||
| it('concurrent sign-ins produce distinct sealedStates (cross-flow rejection)', async () => { | ||
| const core = makeCore(); | ||
| const a = await generate(); | ||
| const b = await generate(); | ||
| expect(a.sealedState).not.toBe(b.sealedState); | ||
| await expect( | ||
| core.verifyCallbackState({ | ||
| stateFromUrl: a.sealedState, | ||
| cookieValue: b.sealedState, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| }); | ||
| }); | ||
| describe('PKCE payload size guards', () => { | ||
| it(`accepts custom state at the ${PKCE_MAX_STATE_BYTES}-byte supported limit`, async () => { | ||
| const state = 'a'.repeat(PKCE_MAX_STATE_BYTES); | ||
| await expect(generate({ state })).resolves.toMatchObject({ | ||
| sealedState: expect.any(String), | ||
| }); | ||
| }); | ||
| it(`rejects custom state ${PKCE_MAX_STATE_BYTES + 1} bytes or larger before sealing`, async () => { | ||
| const state = 'a'.repeat(PKCE_MAX_STATE_BYTES + 1); | ||
| await expect(generate({ state })).rejects.toThrow(PKCEPayloadTooLargeError); | ||
| }); | ||
| it('counts state size in UTF-8 bytes, not JS characters (multibyte input)', async () => { | ||
| // 2-byte UTF-8 char; 1025 chars = 2050 bytes > 2048. | ||
| const state = '\u00e9'.repeat(1025); | ||
| await expect(generate({ state })).rejects.toThrow(PKCEPayloadTooLargeError); | ||
| }); | ||
| it('rejects oversized serialized cookie from large returnPathname + near-limit state', async () => { | ||
| // Under the per-field state cap, but the sealed cookie still overflows | ||
| // once returnPathname is concatenated into the payload. | ||
| const state = 'a'.repeat(PKCE_MAX_STATE_BYTES); | ||
| const returnPathname = '/' + 'p'.repeat(2048); | ||
| await expect(generate({ state, returnPathname })).rejects.toThrow( | ||
| PKCEPayloadTooLargeError, | ||
| ); | ||
| }); | ||
| it('shrinks the effective budget when cookieDomain adds attribute bytes', async () => { | ||
| // cookieDomain=<long> is serialized into every Set-Cookie and counts | ||
| // against the browser's per-cookie budget. A state payload that fits | ||
| // without a Domain attribute can push the serialized header over the | ||
| // limit when Domain is present. | ||
| const longDomain = 'sub.' + 'd'.repeat(200) + '.example.com'; | ||
| const configWithDomain = { ...config, cookieDomain: longDomain } as any; | ||
| const state = 'a'.repeat(PKCE_MAX_STATE_BYTES); | ||
| await expect( | ||
| generateAuthorizationUrl({ | ||
| client: mockClient as any, | ||
| config: configWithDomain, | ||
| encryption: sessionEncryption, | ||
| options: { state, returnPathname: '/' + 'p'.repeat(700) }, | ||
| }), | ||
| ).rejects.toThrow(PKCEPayloadTooLargeError); | ||
| }); | ||
| it(`error message names PKCE_MAX_COOKIE_BYTES (${PKCE_MAX_COOKIE_BYTES})`, async () => { | ||
| const state = 'a'.repeat(PKCE_MAX_STATE_BYTES); | ||
| const returnPathname = '/' + 'p'.repeat(2048); | ||
| try { | ||
| await generate({ state, returnPathname }); | ||
| throw new Error('expected throw'); | ||
| } catch (err) { | ||
| expect(err).toBeInstanceOf(PKCEPayloadTooLargeError); | ||
| expect((err as Error).message).toContain(String(PKCE_MAX_COOKIE_BYTES)); | ||
| } | ||
| }); | ||
| }); |
| import sessionEncryption from '../encryption/ironWebcryptoEncryption.js'; | ||
| import { SessionEncryptionError } from '../errors.js'; | ||
| import { sealState, unsealState, type PKCEStateInput } from './state.js'; | ||
| const testPassword = 'this-is-a-test-password-that-is-32-characters-long!'; | ||
| const validState: PKCEStateInput = { | ||
| nonce: '8b7b1c32-7d8f-44f9-aa51-4c3a6c8fb8d9', | ||
| codeVerifier: 'verifier-12345678901234567890123456789012345678', | ||
| returnPathname: '/dashboard', | ||
| customState: 'user-custom', | ||
| }; | ||
| describe('PKCE state seal/unseal', () => { | ||
| it('round-trips a valid state', async () => { | ||
| const sealed = await sealState(sessionEncryption, testPassword, validState); | ||
| const unsealed = await unsealState(sessionEncryption, testPassword, sealed); | ||
| expect(unsealed).toMatchObject(validState); | ||
| expect(typeof unsealed.issuedAt).toBe('number'); | ||
| }); | ||
| it('round-trips without optional fields', async () => { | ||
| const minimal: PKCEStateInput = { | ||
| nonce: 'n', | ||
| codeVerifier: 'v', | ||
| }; | ||
| const sealed = await sealState(sessionEncryption, testPassword, minimal); | ||
| const unsealed = await unsealState(sessionEncryption, testPassword, sealed); | ||
| expect(unsealed).toMatchObject(minimal); | ||
| expect(typeof unsealed.issuedAt).toBe('number'); | ||
| }); | ||
| it('stamps issuedAt with Date.now() at seal time', async () => { | ||
| vi.useFakeTimers(); | ||
| try { | ||
| const fixedNow = new Date('2026-01-01T00:00:00.000Z').getTime(); | ||
| vi.setSystemTime(fixedNow); | ||
| const sealed = await sealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| validState, | ||
| ); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| sealed, | ||
| ); | ||
| expect(unsealed.issuedAt).toBe(fixedNow); | ||
| } finally { | ||
| vi.useRealTimers(); | ||
| } | ||
| }); | ||
| it.each<[string, string]>([ | ||
| ['dots', 'has.many.dots.in.it'], | ||
| ['JSON-like', '{"key":"value","nested":{"a":1}}'], | ||
| ['2KB payload', 'x'.repeat(2048)], | ||
| ])( | ||
| 'preserves %s customState byte-identically', | ||
| async (_label, customState) => { | ||
| const input: PKCEStateInput = { | ||
| nonce: 'n', | ||
| codeVerifier: 'v', | ||
| customState, | ||
| }; | ||
| const sealed = await sealState(sessionEncryption, testPassword, input); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| sealed, | ||
| ); | ||
| expect(unsealed.customState).toBe(customState); | ||
| }, | ||
| ); | ||
| it('throws SessionEncryptionError on tampered ciphertext', async () => { | ||
| const sealed = await sealState(sessionEncryption, testPassword, validState); | ||
| // Flip one char in the middle of the sealed string (avoiding the ~2 suffix). | ||
| const mid = Math.floor(sealed.length / 2); | ||
| const tampered = | ||
| sealed.slice(0, mid) + | ||
| (sealed[mid] === 'a' ? 'b' : 'a') + | ||
| sealed.slice(mid + 1); | ||
| await expect( | ||
| unsealState(sessionEncryption, testPassword, tampered), | ||
| ).rejects.toThrow(SessionEncryptionError); | ||
| }); | ||
| it('throws SessionEncryptionError with wrong password', async () => { | ||
| const sealed = await sealState(sessionEncryption, testPassword, validState); | ||
| const wrongPassword = | ||
| 'this-is-a-completely-different-password-that-is-long'; | ||
| await expect( | ||
| unsealState(sessionEncryption, wrongPassword, sealed), | ||
| ).rejects.toThrow(SessionEncryptionError); | ||
| }); | ||
| it('throws SessionEncryptionError on schema mismatch', async () => { | ||
| // Seal something that passes encryption but fails the StateSchema check. | ||
| const wrongShape = await sessionEncryption.sealData( | ||
| { foo: 'bar', baz: 42 }, | ||
| { password: testPassword, ttl: 600 }, | ||
| ); | ||
| await expect( | ||
| unsealState(sessionEncryption, testPassword, wrongShape), | ||
| ).rejects.toThrow(SessionEncryptionError); | ||
| await expect( | ||
| unsealState(sessionEncryption, testPassword, wrongShape), | ||
| ).rejects.toThrow(/Malformed PKCE state payload/); | ||
| }); | ||
| describe('TTL enforcement', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| }); | ||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
| it('succeeds well before expiry', async () => { | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await sealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| validState, | ||
| ); | ||
| vi.setSystemTime(new Date('2026-01-01T00:09:59.000Z')); // +599s | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| sealed, | ||
| ); | ||
| expect(unsealed).toMatchObject(validState); | ||
| }); | ||
| it('throws just past the 600s payload-level expiry', async () => { | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await sealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| validState, | ||
| ); | ||
| // Payload-level expiry is strict at 600s (no positive skew grace). | ||
| vi.setSystemTime(new Date('2026-01-01T00:10:01.000Z')); // +601s | ||
| await expect( | ||
| unsealState(sessionEncryption, testPassword, sealed), | ||
| ).rejects.toThrow(SessionEncryptionError); | ||
| }); | ||
| it('rejects payloads older than 600s even when the encryptor ignores ttl', async () => { | ||
| // Custom SessionEncryption that ignores the ttl option — simulates an | ||
| // adapter that would let a stale blob through. The payload-level | ||
| // issuedAt check in unsealState is the authoritative guard. | ||
| const ignoresTtl = { | ||
| sealData: (data: unknown, options: { password: string }) => | ||
| sessionEncryption.sealData(data, { password: options.password }), | ||
| unsealData: <T>(sealed: string, options: { password: string }) => | ||
| sessionEncryption.unsealData<T>(sealed, { | ||
| password: options.password, | ||
| }), | ||
| }; | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await sealState(ignoresTtl, testPassword, validState); | ||
| // 605s — inside iron's default 60s skew grace, outside the strict 600s | ||
| // payload-level check. | ||
| vi.setSystemTime(new Date('2026-01-01T00:10:05.000Z')); | ||
| await expect( | ||
| unsealState(ignoresTtl, testPassword, sealed), | ||
| ).rejects.toThrow(/PKCE state expired/); | ||
| }); | ||
| it('tolerates bounded negative skew (issuedAt up to 60s ahead of now)', async () => { | ||
| // Simulates sign-in on a node whose clock is ahead of the callback | ||
| // node by 30s — within the 60s tolerance. | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await sessionEncryption.sealData( | ||
| { ...validState, issuedAt: Date.now() + 30_000 }, | ||
| { password: testPassword, ttl: 600 }, | ||
| ); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| testPassword, | ||
| sealed, | ||
| ); | ||
| expect(unsealed).toMatchObject(validState); | ||
| }); | ||
| it('rejects payloads meaningfully in the future (issuedAt beyond skew)', async () => { | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await sessionEncryption.sealData( | ||
| { ...validState, issuedAt: Date.now() + 120_000 }, | ||
| { password: testPassword, ttl: 600 }, | ||
| ); | ||
| await expect( | ||
| unsealState(sessionEncryption, testPassword, sealed), | ||
| ).rejects.toThrow(/PKCE state expired/); | ||
| }); | ||
| }); | ||
| }); |
| import * as v from 'valibot'; | ||
| import { SessionEncryptionError } from '../errors.js'; | ||
| import type { SessionEncryption } from '../session/types.js'; | ||
| import { PKCE_COOKIE_MAX_AGE } from './cookieOptions.js'; | ||
| /** | ||
| * Clock-skew tolerance (ms) for the payload-level `issuedAt` check. | ||
| * | ||
| * Applied asymmetrically, only to the negative (future-`issuedAt`) side: | ||
| * allows a sign-in issued on node A to be verified on node B whose clock | ||
| * is up to 60s behind without failing a legitimate callback. The upper | ||
| * bound (expiry) is strict — `PKCE_COOKIE_MAX_AGE` with no grace — so | ||
| * this payload check is always at least as strict as iron-webcrypto's | ||
| * symmetric 60s skew on `ttl`, making it the authoritative expiry guard | ||
| * regardless of how a custom `SessionEncryption` adapter treats `ttl`. | ||
| */ | ||
| const PKCE_CLOCK_SKEW_MS = 60_000; | ||
| /** | ||
| * Runtime schema for the sealed PKCE state blob. | ||
| * | ||
| * Validated at unseal time as defense-in-depth against any future code path | ||
| * accidentally sealing the wrong shape. Shape mismatch is treated as an | ||
| * integrity failure (same category as a tampered blob). | ||
| * | ||
| * `issuedAt` (ms since epoch) lets `unsealState` enforce the TTL on its own | ||
| * rather than trusting the encryption adapter's `ttl` handling. Custom | ||
| * `SessionEncryption` implementations may silently ignore the `ttl` field — | ||
| * the payload-level age check closes that gap. | ||
| */ | ||
| export const StateSchema = v.object({ | ||
| nonce: v.string(), | ||
| codeVerifier: v.string(), | ||
| issuedAt: v.number(), | ||
| returnPathname: v.optional(v.string()), | ||
| customState: v.optional(v.string()), | ||
| /** | ||
| * The per-request `redirectUri` override resolved at sign-in time, if the | ||
| * caller supplied one. Persisted so `handleCallback` can recompute the | ||
| * exact cookie options used at `setCookie` time — notably `secure`, which | ||
| * is inferred from the redirect URI's protocol. Omitted when the caller | ||
| * relied on the configured default. | ||
| */ | ||
| redirectUri: v.optional(v.string()), | ||
| }); | ||
| export type PKCEState = v.InferOutput<typeof StateSchema>; | ||
| export type PKCEStateInput = Omit<PKCEState, 'issuedAt'>; | ||
| /** | ||
| * Seal a PKCE state object for use as both the OAuth `state` query param | ||
| * and the `wos-auth-verifier` cookie value. Seal embeds a 600s TTL and | ||
| * stamps `issuedAt` so the unseal path can enforce age independently. | ||
| */ | ||
| export async function sealState( | ||
| encryption: SessionEncryption, | ||
| password: string, | ||
| state: PKCEStateInput, | ||
| ): Promise<string> { | ||
| return encryption.sealData( | ||
| { ...state, issuedAt: Date.now() }, | ||
| { | ||
| password, | ||
| ttl: PKCE_COOKIE_MAX_AGE, | ||
| }, | ||
| ); | ||
| } | ||
| /** | ||
| * Unseal a PKCE state blob, enforcing TTL and shape. | ||
| * | ||
| * Age is verified twice: | ||
| * 1. The encryption adapter's `ttl` check (iron-webcrypto enforces this by | ||
| * default — custom adapters MAY ignore it). | ||
| * 2. A payload-level `issuedAt` comparison against `Date.now()`. This is the | ||
| * authoritative check — it runs regardless of how the adapter treats | ||
| * `ttl`. | ||
| * | ||
| * Any failure — expired TTL, tamper, wrong password, or schema mismatch — | ||
| * is wrapped as `SessionEncryptionError`. Callers differentiate via the | ||
| * `cause` chain, not the message string. | ||
| */ | ||
| export async function unsealState( | ||
| encryption: SessionEncryption, | ||
| password: string, | ||
| sealed: string, | ||
| ): Promise<PKCEState> { | ||
| let raw: unknown; | ||
| try { | ||
| raw = await encryption.unsealData<unknown>(sealed, { | ||
| password, | ||
| ttl: PKCE_COOKIE_MAX_AGE, | ||
| }); | ||
| } catch (cause) { | ||
| throw new SessionEncryptionError('Failed to unseal PKCE state', cause); | ||
| } | ||
| const result = v.safeParse(StateSchema, raw); | ||
| if (!result.success) { | ||
| throw new SessionEncryptionError( | ||
| 'Malformed PKCE state payload', | ||
| result.issues, | ||
| ); | ||
| } | ||
| const ageMs = Date.now() - result.output.issuedAt; | ||
| if (ageMs < -PKCE_CLOCK_SKEW_MS || ageMs > PKCE_COOKIE_MAX_AGE * 1000) { | ||
| // Attach age context so production logs can diagnose clock-skew issues | ||
| // via the `cause` chain without leaking the sealed blob itself. | ||
| throw new SessionEncryptionError( | ||
| 'PKCE state expired', | ||
| new Error( | ||
| `age=${ageMs}ms, maxAge=${PKCE_COOKIE_MAX_AGE * 1000}ms, skew=±${PKCE_CLOCK_SKEW_MS}ms`, | ||
| ), | ||
| ); | ||
| } | ||
| return result.output; | ||
| } |
| import type { CookieOptions } from './types.js'; | ||
| /** | ||
| * Serialize a `Set-Cookie` header value from (name, value, options). | ||
| * | ||
| * Shared between `CookieSessionStorage` (writes the header on response) and | ||
| * the PKCE flow's pre-write size guard (must measure the exact bytes the | ||
| * browser will see, including attribute overhead, to detect oversized | ||
| * verifier cookies before they silently get dropped). | ||
| */ | ||
| export function serializeCookie( | ||
| name: string, | ||
| value: string, | ||
| options: CookieOptions, | ||
| { expired }: { expired?: boolean } = {}, | ||
| ): string { | ||
| const parts = [`${name}=${encodeURIComponent(value)}`]; | ||
| if (options.path) parts.push(`Path=${options.path}`); | ||
| if (options.domain) parts.push(`Domain=${options.domain}`); | ||
| if (options.maxAge != null || expired) | ||
| parts.push(`Max-Age=${expired ? 0 : options.maxAge}`); | ||
| if (options.httpOnly) parts.push('HttpOnly'); | ||
| if (options.secure) parts.push('Secure'); | ||
| if (options.sameSite) { | ||
| const capitalized = | ||
| options.sameSite.charAt(0).toUpperCase() + | ||
| options.sameSite.slice(1).toLowerCase(); | ||
| parts.push(`SameSite=${capitalized}`); | ||
| } | ||
| if (options.priority) parts.push(`Priority=${options.priority}`); | ||
| if (options.partitioned) parts.push('Partitioned'); | ||
| return parts.join('; '); | ||
| } |
| import type { Impersonator, User, WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitConfig } from './config/types.js'; | ||
| import { type PKCEState } from './pkce/state.js'; | ||
| import type { BaseTokenClaims, CustomClaims, Session, SessionEncryption } from './session/types.js'; | ||
@@ -74,2 +75,21 @@ /** | ||
| /** | ||
| * Verify the OAuth callback state against the PKCE verifier cookie and | ||
| * return the unsealed state blob. | ||
| * | ||
| * Core owns encryption, so verification lives here rather than on | ||
| * AuthService. Byte-compares the query-string `state` against the cookie | ||
| * value BEFORE attempting to decrypt — smaller crypto attack surface and | ||
| * no decryption cost on obvious mismatches. | ||
| * | ||
| * @throws {OAuthStateMismatchError} state missing or does not match cookie | ||
| * @throws {PKCECookieMissingError} cookie not sent — typically a proxy or | ||
| * Set-Cookie propagation issue on the adapter's callback path | ||
| * @throws {SessionEncryptionError} seal tampered, wrong password, TTL expired, | ||
| * or schema mismatch on the unsealed payload | ||
| */ | ||
| verifyCallbackState(params: { | ||
| stateFromUrl: string | undefined; | ||
| cookieValue: string | undefined; | ||
| }): Promise<PKCEState>; | ||
| /** | ||
| * Refresh tokens using WorkOS API. | ||
@@ -76,0 +96,0 @@ * |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthKitCore.d.ts","sourceRoot":"","sources":["../../src/core/AuthKitCore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,OAAO,EACP,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,QAAQ,CAAS;gBAGvB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,iBAAiB;IAQ/B;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAI3B;IAEF;;;;;OAKG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlD;;;;;;OAMG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,OAAO;IAU5D;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAK1B;;;;;;OAMG;IACH,gBAAgB,CAAC,aAAa,GAAG,YAAY,EAC3C,KAAK,EAAE,MAAM,GACZ,eAAe,GAAG,aAAa;IAQlC;;;;;;OAMG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAYvD;;;;;;OAMG;IACG,cAAc,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhE;;;;;;;;OAQG;IACG,aAAa,CACjB,YAAY,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC;QACT,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,IAAI,CAAC;QACX,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;KACxC,CAAC;IAoBF;;;;;;;;;;;OAWG;IACG,kBAAkB,CAAC,aAAa,GAAG,YAAY,EACnD,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GACrD,OAAO,CAAC;QACT,KAAK,EAAE,OAAO,CAAC;QACf,SAAS,EAAE,OAAO,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC;KACzC,CAAC;CA8CH"} | ||
| {"version":3,"file":"AuthKitCore.d.ts","sourceRoot":"","sources":["../../src/core/AuthKitCore.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,OAAO,EAAE,KAAK,SAAS,EAAe,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EACZ,OAAO,EACP,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAoB;IACtC,OAAO,CAAC,QAAQ,CAAS;gBAGvB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,iBAAiB;IAQ/B;;OAEG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAI3B;IAEF;;;;;OAKG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASlD;;;;;;OAMG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,GAAE,MAAW,GAAG,OAAO;IAU5D;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAK1B;;;;;;OAMG;IACH,gBAAgB,CAAC,aAAa,GAAG,YAAY,EAC3C,KAAK,EAAE,MAAM,GACZ,eAAe,GAAG,aAAa;IAQlC;;;;;;OAMG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAYvD;;;;;;OAMG;IACG,cAAc,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAYhE;;;;;;;;;;;;;;OAcG;IACG,mBAAmB,CAAC,MAAM,EAAE;QAChC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;QACjC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;KACjC,GAAG,OAAO,CAAC,SAAS,CAAC;IA2BtB;;;;;;;;OAQG;IACG,aAAa,CACjB,YAAY,EAAE,MAAM,EACpB,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC;QACT,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,IAAI,CAAC;QACX,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;KACxC,CAAC;IAoBF;;;;;;;;;;;OAWG;IACG,kBAAkB,CAAC,aAAa,GAAG,YAAY,EACnD,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GACrD,OAAO,CAAC;QACT,KAAK,EAAE,OAAO,CAAC;QACf,SAAS,EAAE,OAAO,CAAC;QACnB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,eAAe,GAAG,aAAa,CAAC;KACzC,CAAC;CA8CH"} |
| import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; | ||
| import { once } from '../utils.js'; | ||
| import { SessionEncryptionError, TokenRefreshError } from './errors.js'; | ||
| import { constantTimeEqual, once } from '../utils.js'; | ||
| import { OAuthStateMismatchError, PKCECookieMissingError, SessionEncryptionError, TokenRefreshError, } from './errors.js'; | ||
| import { unsealState } from './pkce/state.js'; | ||
| /** | ||
@@ -83,4 +84,4 @@ * AuthKitCore provides pure business logic for authentication operations. | ||
| } | ||
| catch (error) { | ||
| throw new Error('Invalid token'); | ||
| catch (cause) { | ||
| throw new Error('Invalid token', { cause }); | ||
| } | ||
@@ -124,2 +125,33 @@ } | ||
| /** | ||
| * Verify the OAuth callback state against the PKCE verifier cookie and | ||
| * return the unsealed state blob. | ||
| * | ||
| * Core owns encryption, so verification lives here rather than on | ||
| * AuthService. Byte-compares the query-string `state` against the cookie | ||
| * value BEFORE attempting to decrypt — smaller crypto attack surface and | ||
| * no decryption cost on obvious mismatches. | ||
| * | ||
| * @throws {OAuthStateMismatchError} state missing or does not match cookie | ||
| * @throws {PKCECookieMissingError} cookie not sent — typically a proxy or | ||
| * Set-Cookie propagation issue on the adapter's callback path | ||
| * @throws {SessionEncryptionError} seal tampered, wrong password, TTL expired, | ||
| * or schema mismatch on the unsealed payload | ||
| */ | ||
| async verifyCallbackState(params) { | ||
| const { stateFromUrl, cookieValue } = params; | ||
| if (!stateFromUrl) { | ||
| throw new OAuthStateMismatchError('Missing state parameter from callback URL'); | ||
| } | ||
| if (!cookieValue) { | ||
| throw new PKCECookieMissingError('PKCE verifier cookie missing — cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.'); | ||
| } | ||
| const encoder = new TextEncoder(); | ||
| const urlBytes = encoder.encode(stateFromUrl); | ||
| const cookieBytes = encoder.encode(cookieValue); | ||
| if (!constantTimeEqual(urlBytes, cookieBytes)) { | ||
| throw new OAuthStateMismatchError('OAuth state mismatch'); | ||
| } | ||
| return unsealState(this.encryption, this.config.cookiePassword, cookieValue); | ||
| } | ||
| /** | ||
| * Refresh tokens using WorkOS API. | ||
@@ -126,0 +158,0 @@ * |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthKitCore.js","sourceRoot":"","sources":["../../src/core/AuthKitCore.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"AuthKitCore.js","sourceRoot":"","sources":["../../src/core/AuthKitCore.ts"],"names":[],"mappings":"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"} |
@@ -13,4 +13,5 @@ import type { SessionEncryption as SessionEncryptionInterface } from '../session/types.js'; | ||
| }): Promise<string>; | ||
| unsealData<T = unknown>(encryptedData: string, { password }: { | ||
| unsealData<T = unknown>(encryptedData: string, { password, ttl }: { | ||
| password: string; | ||
| ttl?: number | undefined; | ||
| }): Promise<T>; | ||
@@ -17,0 +18,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"ironWebcryptoEncryption.d.ts","sourceRoot":"","sources":["../../../src/core/encryption/ironWebcryptoEncryption.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,IAAI,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAE3F;;GAEG;AACH,qBAAa,iBAAkB,YAAW,0BAA0B;IAClE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAO;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAK;IAGzC,OAAO,CAAC,SAAS;IAaX,QAAQ,CACZ,IAAI,EAAE,OAAO,EACb,EAAE,QAAQ,EAAE,GAAO,EAAE,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAgCjE,UAAU,CAAC,CAAC,GAAG,OAAO,EAC1B,aAAa,EAAE,MAAM,EACrB,EAAE,QAAQ,EAAE,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,GACjC,OAAO,CAAC,CAAC,CAAC;CAoCd;AAED,QAAA,MAAM,uBAAuB,mBAA0B,CAAC;AAExD,eAAe,uBAAuB,CAAC"} | ||
| {"version":3,"file":"ironWebcryptoEncryption.d.ts","sourceRoot":"","sources":["../../../src/core/encryption/ironWebcryptoEncryption.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,IAAI,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAE3F;;GAEG;AACH,qBAAa,iBAAkB,YAAW,0BAA0B;IAClE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAO;IACxC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAK;IAGzC,OAAO,CAAC,SAAS;IAaX,QAAQ,CACZ,IAAI,EAAE,OAAO,EACb,EAAE,QAAQ,EAAE,GAAO,EAAE,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE;IAgCjE,UAAU,CAAC,CAAC,GAAG,OAAO,EAC1B,aAAa,EAAE,MAAM,EACrB,EAAE,QAAQ,EAAE,GAAO,EAAE,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,GACpE,OAAO,CAAC,CAAC,CAAC;CAoCd;AAED,QAAA,MAAM,uBAAuB,mBAA0B,CAAC;AAExD,eAAe,uBAAuB,CAAC"} |
@@ -43,3 +43,3 @@ import { seal as sealData, unseal as unsealData } from 'iron-webcrypto'; | ||
| // Decrypt data from iron-session with HMAC verification | ||
| async unsealData(encryptedData, { password }) { | ||
| async unsealData(encryptedData, { password, ttl = 0 }) { | ||
| // First, parse the seal to extract the version | ||
@@ -63,3 +63,3 @@ const { sealWithoutVersion, tokenVersion } = this.parseSeal(encryptedData); | ||
| }, | ||
| ttl: 0, | ||
| ttl: ttl * 1000, // Convert seconds to milliseconds. 0 = no TTL enforcement. | ||
| timestampSkewSec: 60, | ||
@@ -66,0 +66,0 @@ localtimeOffsetMsec: 0, |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"ironWebcryptoEncryption.js","sourceRoot":"","sources":["../../../src/core/encryption/ironWebcryptoEncryption.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGxE;;GAEG;AACH,MAAM,OAAO,iBAAiB;IACX,gBAAgB,GAAG,GAAG,CAAC;IACvB,mBAAmB,GAAG,CAAC,CAAC;IAEzC,oDAAoD;IAC5C,SAAS,CAAC,IAAY;QAI5B,MAAM,CAAC,kBAAkB,GAAG,EAAE,EAAE,oBAAoB,CAAC,GAAG,IAAI,CAAC,KAAK,CAChE,IAAI,CAAC,gBAAgB,CACtB,CAAC;QACF,MAAM,YAAY,GAChB,oBAAoB,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,oBAAoB,EAAE,EAAE,CAAC,CAAC;QAC3E,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,CAAC;IAC9C,CAAC;IAED,4DAA4D;IAC5D,KAAK,CAAC,QAAQ,CACZ,IAAa,EACb,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,EAAkD;QAErE,0CAA0C;QAC1C,MAAM,WAAW,GAAG;YAClB,EAAE,EAAE,GAAG;YACP,MAAM,EAAE,QAAQ;SACjB,CAAC;QAEF,sEAAsE;QACtE,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,WAAW,EAAE;YAC7C,UAAU,EAAE;gBACV,QAAQ,EAAE,GAAG;gBACb,SAAS,EAAE,aAAa;gBACxB,UAAU,EAAE,CAAC;gBACb,iBAAiB,EAAE,EAAE;aACtB;YACD,SAAS,EAAE;gBACT,QAAQ,EAAE,GAAG;gBACb,SAAS,EAAE,QAAQ;gBACnB,UAAU,EAAE,CAAC;gBACb,iBAAiB,EAAE,EAAE;aACtB;YACD,GAAG,EAAE,GAAG,GAAG,IAAI,EAAE,kCAAkC;YACnD,gBAAgB,EAAE,EAAE;YACpB,mBAAmB,EAAE,CAAC;SACvB,CAAC,CAAC;QAEH,2DAA2D;QAC3D,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC;IACtE,CAAC;IAED,wDAAwD;IACxD,KAAK,CAAC,UAAU,CACd,aAAqB,EACrB,EAAE,QAAQ,EAAwB;QAElC,+CAA+C;QAC/C,MAAM,EAAE,kBAAkB,EAAE,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC;QAE3E,qDAAqD;QACrD,MAAM,WAAW,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC;QAEpC,uCAAuC;QACvC,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,kBAAkB,EAAE,WAAW,EAAE;YAC7D,UAAU,EAAE;gBACV,QAAQ,EAAE,GAAG;gBACb,SAAS,EAAE,aAAa;gBACxB,UAAU,EAAE,CAAC;gBACb,iBAAiB,EAAE,EAAE;aACtB;YACD,SAAS,EAAE;gBACT,QAAQ,EAAE,GAAG;gBACb,SAAS,EAAE,QAAQ;gBACnB,UAAU,EAAE,CAAC;gBACb,iBAAiB,EAAE,EAAE;aACtB;YACD,GAAG,EAAE,CAAC;YACN,gBAAgB,EAAE,EAAE;YACpB,mBAAmB,EAAE,CAAC;SACvB,CAAC,CAAC;QAEH,oCAAoC;QACpC,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;YACvB,OAAO,IAAS,CAAC;QACnB,CAAC;aAAM,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;YACjC,4DAA4D;YAC5D,OAAO,EAAE,GAAI,IAAY,CAAC,UAAU,EAAO,CAAC;QAC9C,CAAC;QAED,OAAO,IAAS,CAAC;IACnB,CAAC;CACF;AAED,MAAM,uBAAuB,GAAG,IAAI,iBAAiB,EAAE,CAAC;AAExD,eAAe,uBAAuB,CAAC"} | ||
| {"version":3,"file":"ironWebcryptoEncryption.js","sourceRoot":"","sources":["../../../src/core/encryption/ironWebcryptoEncryption.ts"],"names":[],"mappings":"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"} |
@@ -11,2 +11,11 @@ export declare class AuthKitError extends Error { | ||
| } | ||
| export declare class OAuthStateMismatchError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown); | ||
| } | ||
| export declare class PKCECookieMissingError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown); | ||
| } | ||
| export declare class PKCEPayloadTooLargeError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown); | ||
| } | ||
| export declare class TokenRefreshError extends AuthKitError { | ||
@@ -13,0 +22,0 @@ readonly userId?: string; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CAMzE;AAED,qBAAa,sBAAuB,SAAQ,YAAY;gBAC1C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,iBAAkB,SAAQ,YAAY;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAG1B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;CAOpD"} | ||
| {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,YAAa,SAAQ,KAAK;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBAEf,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CAMzE;AAED,qBAAa,sBAAuB,SAAQ,YAAY;gBAC1C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,sBAAuB,SAAQ,YAAY;gBAC1C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,wBAAyB,SAAQ,YAAY;gBAC5C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAI7C;AAED,qBAAa,iBAAkB,SAAQ,YAAY;IACjD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;gBAG1B,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,OAAO,EACf,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE;CAOpD"} |
+18
-0
@@ -22,2 +22,20 @@ export class AuthKitError extends Error { | ||
| } | ||
| export class OAuthStateMismatchError extends AuthKitError { | ||
| constructor(message, cause) { | ||
| super(message, cause); | ||
| this.name = 'OAuthStateMismatchError'; | ||
| } | ||
| } | ||
| export class PKCECookieMissingError extends AuthKitError { | ||
| constructor(message, cause) { | ||
| super(message, cause); | ||
| this.name = 'PKCECookieMissingError'; | ||
| } | ||
| } | ||
| export class PKCEPayloadTooLargeError extends AuthKitError { | ||
| constructor(message, cause) { | ||
| super(message, cause); | ||
| this.name = 'PKCEPayloadTooLargeError'; | ||
| } | ||
| } | ||
| export class TokenRefreshError extends AuthKitError { | ||
@@ -24,0 +42,0 @@ userId; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,IAAI,CAAuB;IAE3B,YAAY,OAAe,EAAE,KAAe,EAAE,IAA0B;QACtE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,YAAY;IACtD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,YAAY;IACpD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,YAAY;IACxC,MAAM,CAAU;IAChB,SAAS,CAAU;IAE5B,YACE,OAAe,EACf,KAAe,EACf,OAAiD;QAEjD,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;QAC9B,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,CAAC;IACtC,CAAC;CACF"} | ||
| {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,YAAa,SAAQ,KAAK;IACrC,IAAI,CAAuB;IAE3B,YAAY,OAAe,EAAE,KAAe,EAAE,IAA0B;QACtE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,YAAY;IACtD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,oBAAqB,SAAQ,YAAY;IACpD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AAED,MAAM,OAAO,uBAAwB,SAAQ,YAAY;IACvD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,YAAY;IACtD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,wBAAyB,SAAQ,YAAY;IACxD,YAAY,OAAe,EAAE,KAAe;QAC1C,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,OAAO,iBAAkB,SAAQ,YAAY;IACxC,MAAM,CAAU;IAChB,SAAS,CAAU;IAE5B,YACE,OAAe,EACf,KAAe,EACf,OAAiD;QAEjD,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;QAC9B,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,CAAC;IACtC,CAAC;CACF"} |
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import type { HeadersBag, SessionStorage } from './types.js'; | ||
| export interface CookieOptions { | ||
| path?: string; | ||
| domain?: string; | ||
| maxAge?: number; | ||
| expires?: Date; | ||
| httpOnly?: boolean; | ||
| secure?: boolean; | ||
| sameSite?: 'lax' | 'strict' | 'none'; | ||
| priority?: 'low' | 'medium' | 'high'; | ||
| partitioned?: boolean; | ||
| } | ||
| import type { CookieOptions, HeadersBag, SessionStorage } from './types.js'; | ||
| export type { CookieOptions } from './types.js'; | ||
| export declare abstract class CookieSessionStorage<TRequest, TResponse> implements SessionStorage<TRequest, TResponse> { | ||
@@ -21,4 +11,24 @@ protected cookieName: string; | ||
| } | void>; | ||
| protected buildSetCookie(value: string, expired?: boolean): string; | ||
| abstract getSession(request: TRequest): Promise<string | null>; | ||
| protected serializeCookie(name: string, value: string, options: CookieOptions, flags?: { | ||
| expired?: boolean; | ||
| }): string; | ||
| /** | ||
| * Read a named cookie from the framework-specific request. | ||
| * | ||
| * Implementations MUST return the URL-decoded value — the inverse of | ||
| * `serializeCookie`'s `encodeURIComponent` on write. Returning the raw | ||
| * on-wire bytes will silently break byte-comparison against the original | ||
| * value (e.g. PKCE state verification) for any seal containing characters | ||
| * that `encodeURIComponent` escapes. | ||
| */ | ||
| abstract getCookie(request: TRequest, name: string): Promise<string | null>; | ||
| setCookie(response: TResponse | undefined, name: string, value: string, options: CookieOptions): Promise<{ | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }>; | ||
| clearCookie(response: TResponse | undefined, name: string, options: CookieOptions): Promise<{ | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }>; | ||
| getSession(request: TRequest): Promise<string | null>; | ||
| saveSession(response: TResponse | undefined, sessionData: string): Promise<{ | ||
@@ -28,3 +38,3 @@ response?: TResponse; | ||
| }>; | ||
| clearSession(response: TResponse): Promise<{ | ||
| clearSession(response: TResponse | undefined): Promise<{ | ||
| response?: TResponse; | ||
@@ -31,0 +41,0 @@ headers?: HeadersBag; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"CookieSessionStorage.d.ts","sourceRoot":"","sources":["../../../src/core/session/CookieSessionStorage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,8BAAsB,oBAAoB,CACxC,QAAQ,EACR,SAAS,CACT,YAAW,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9C,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;gBAEpC,MAAM,EAAE,aAAa;cA2BjB,YAAY,CAC1B,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,QAAQ,EAAE,UAAU,GACnB,OAAO,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;IAI1C,SAAS,CAAC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM;IAkBlE,QAAQ,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAExD,WAAW,CACf,QAAQ,EAAE,SAAS,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAMpD,YAAY,CAChB,QAAQ,EAAE,SAAS,GAClB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;CAK3D"} | ||
| {"version":3,"file":"CookieSessionStorage.d.ts","sourceRoot":"","sources":["../../../src/core/session/CookieSessionStorage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5E,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,8BAAsB,oBAAoB,CACxC,QAAQ,EACR,SAAS,CACT,YAAW,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC9C,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;gBAEpC,MAAM,EAAE,aAAa;cA8BjB,YAAY,CAC1B,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,QAAQ,EAAE,UAAU,GACnB,OAAO,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,GAAG,IAAI,CAAC;IAI1C,SAAS,CAAC,eAAe,CACvB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,EACtB,KAAK,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAChC,MAAM;IAIT;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAErE,SAAS,CACb,QAAQ,EAAE,SAAS,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAMpD,WAAW,CACf,QAAQ,EAAE,SAAS,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAM1D,UAAU,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAIrD,WAAW,CACT,QAAQ,EAAE,SAAS,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAS1D,YAAY,CACV,QAAQ,EAAE,SAAS,GAAG,SAAS,GAC9B,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;CAG3D"} |
@@ -0,1 +1,2 @@ | ||
| import { serializeCookie } from './serializeCookie.js'; | ||
| export class CookieSessionStorage { | ||
@@ -5,3 +6,6 @@ cookieName; | ||
| constructor(config) { | ||
| this.cookieName = config.cookieName ?? 'wos_session'; | ||
| // Matches the canonical default in ConfigurationProvider. This fallback | ||
| // only fires when a caller instantiates the class with a config that | ||
| // hasn't been resolved through the provider. | ||
| this.cookieName = config.cookieName ?? 'wos-session'; | ||
| const sameSite = config.cookieSameSite ?? 'lax'; | ||
@@ -32,36 +36,25 @@ // Infer secure flag from redirectUri protocol | ||
| } | ||
| buildSetCookie(value, expired) { | ||
| const a = [`${this.cookieName}=${encodeURIComponent(value)}`]; | ||
| const o = this.cookieOptions; | ||
| if (o.path) | ||
| a.push(`Path=${o.path}`); | ||
| if (o.domain) | ||
| a.push(`Domain=${o.domain}`); | ||
| if (o.maxAge || expired) | ||
| a.push(`Max-Age=${expired ? 0 : o.maxAge}`); | ||
| if (o.httpOnly) | ||
| a.push('HttpOnly'); | ||
| if (o.secure) | ||
| a.push('Secure'); | ||
| if (o.sameSite) { | ||
| const capitalizedSameSite = o.sameSite.charAt(0).toUpperCase() + o.sameSite.slice(1).toLowerCase(); | ||
| a.push(`SameSite=${capitalizedSameSite}`); | ||
| } | ||
| if (o.priority) | ||
| a.push(`Priority=${o.priority}`); | ||
| if (o.partitioned) | ||
| a.push('Partitioned'); | ||
| return a.join('; '); | ||
| serializeCookie(name, value, options, flags = {}) { | ||
| return serializeCookie(name, value, options, flags); | ||
| } | ||
| async saveSession(response, sessionData) { | ||
| const header = this.buildSetCookie(sessionData); | ||
| async setCookie(response, name, value, options) { | ||
| const header = this.serializeCookie(name, value, options); | ||
| const mutated = await this.applyHeaders(response, { 'Set-Cookie': header }); | ||
| return mutated ?? { headers: { 'Set-Cookie': header } }; | ||
| } | ||
| async clearSession(response) { | ||
| const header = this.buildSetCookie('', true); | ||
| async clearCookie(response, name, options) { | ||
| const header = this.serializeCookie(name, '', options, { expired: true }); | ||
| const mutated = await this.applyHeaders(response, { 'Set-Cookie': header }); | ||
| return mutated ?? { headers: { 'Set-Cookie': header } }; | ||
| } | ||
| getSession(request) { | ||
| return this.getCookie(request, this.cookieName); | ||
| } | ||
| saveSession(response, sessionData) { | ||
| return this.setCookie(response, this.cookieName, sessionData, this.cookieOptions); | ||
| } | ||
| clearSession(response) { | ||
| return this.clearCookie(response, this.cookieName, this.cookieOptions); | ||
| } | ||
| } | ||
| //# sourceMappingURL=CookieSessionStorage.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"CookieSessionStorage.js","sourceRoot":"","sources":["../../../src/core/session/CookieSessionStorage.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"CookieSessionStorage.js","sourceRoot":"","sources":["../../../src/core/session/CookieSessionStorage.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAKvD,MAAM,OAAgB,oBAAoB;IAI9B,UAAU,CAAS;IACV,aAAa,CAAgB;IAEhD,YAAY,MAAqB;QAC/B,wEAAwE;QACxE,qEAAqE;QACrE,6CAA6C;QAC7C,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,IAAI,aAAa,CAAC;QAErD,MAAM,QAAQ,GAAG,MAAM,CAAC,cAAc,IAAI,KAAK,CAAC;QAEhD,8CAA8C;QAC9C,6DAA6D;QAC7D,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;gBACxC,MAAM,GAAG,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC;YACrC,CAAC;YAAC,MAAM,CAAC;gBACP,iDAAiD;YACnD,CAAC;QACH,CAAC;QAED,IAAI,CAAC,aAAa,GAAG;YACnB,IAAI,EAAE,GAAG;YACT,QAAQ,EAAE,IAAI;YACd,QAAQ;YACR,MAAM;YACN,MAAM,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,WAAW;YAC9D,MAAM,EAAE,MAAM,CAAC,YAAY;SAC5B,CAAC;IACJ,CAAC;IAES,KAAK,CAAC,YAAY,CAC1B,SAAgC,EAChC,QAAoB;QAEpB,+EAA+E;IACjF,CAAC;IAES,eAAe,CACvB,IAAY,EACZ,KAAa,EACb,OAAsB,EACtB,QAA+B,EAAE;QAEjC,OAAO,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;IAaD,KAAK,CAAC,SAAS,CACb,QAA+B,EAC/B,IAAY,EACZ,KAAa,EACb,OAAsB;QAEtB,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,OAAO,OAAO,IAAI,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,WAAW,CACf,QAA+B,EAC/B,IAAY,EACZ,OAAsB;QAEtB,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5E,OAAO,OAAO,IAAI,EAAE,OAAO,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,UAAU,CAAC,OAAiB;QAC1B,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAClD,CAAC;IAED,WAAW,CACT,QAA+B,EAC/B,WAAmB;QAEnB,OAAO,IAAI,CAAC,SAAS,CACnB,QAAQ,EACR,IAAI,CAAC,UAAU,EACf,WAAW,EACX,IAAI,CAAC,aAAa,CACnB,CAAC;IACJ,CAAC;IAED,YAAY,CACV,QAA+B;QAE/B,OAAO,IAAI,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;IACzE,CAAC;CACF"} |
@@ -24,4 +24,4 @@ import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; | ||
| } | ||
| catch (error) { | ||
| throw new Error('Invalid token'); | ||
| catch (cause) { | ||
| throw new Error('Invalid token', { cause }); | ||
| } | ||
@@ -28,0 +28,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"TokenManager.js","sourceRoot":"","sources":["../../../src/core/session/TokenManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAItC,MAAM,OAAO,YAAY;IACf,QAAQ,CAAS;IACjB,MAAM,CAAS;IAEvB,YAAY,QAAgB,EAAE,MAAc;QAC1C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAEgB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CACxC,kBAAkB,CAChB,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAC9D,CACF,CAAC;IAEF,KAAK,CAAC,WAAW,CAAC,KAAa;QAC7B,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,gBAAgB,CACd,KAAa;QAEb,IAAI,CAAC;YACH,OAAO,SAAS,CAAkC,KAAK,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,eAAe,CAAC,KAAa,EAAE,aAAsB;QACnD,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,IAAI,EAAE,CAAC,CAAC,sBAAsB;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,0BAA0B;QAC7E,OAAO,UAAU,GAAG,WAAW,IAAI,MAAM,CAAC;IAC5C,CAAC;IAED,kBAAkB,CAAC,KAAa;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC5C,OAAO,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC;IAC5B,CAAC;CACF;AAED,eAAe,YAAY,CAAC"} | ||
| {"version":3,"file":"TokenManager.js","sourceRoot":"","sources":["../../../src/core/session/TokenManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAItC,MAAM,OAAO,YAAY;IACf,QAAQ,CAAS;IACjB,MAAM,CAAS;IAEvB,YAAY,QAAgB,EAAE,MAAc;QAC1C,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAEgB,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CACxC,kBAAkB,CAChB,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAC9D,CACF,CAAC;IAEF,KAAK,CAAC,WAAW,CAAC,KAAa;QAC7B,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YAC5C,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,gBAAgB,CACd,KAAa;QAEb,IAAI,CAAC;YACH,OAAO,SAAS,CAAkC,KAAK,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,eAAe,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,eAAe,CAAC,KAAa,EAAE,aAAsB;QACnD,MAAM,UAAU,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC;QAClD,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC;QACf,CAAC;QAED,MAAM,MAAM,GAAG,aAAa,IAAI,EAAE,CAAC,CAAC,sBAAsB;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,0BAA0B;QAC7E,OAAO,UAAU,GAAG,WAAW,IAAI,MAAM,CAAC;IAC5C,CAAC;IAED,kBAAkB,CAAC,KAAa;QAC9B,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAC5C,OAAO,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC;IAC5B,CAAC;CACF;AAED,eAAe,YAAY,CAAC"} |
@@ -68,4 +68,46 @@ import type { Impersonator, User } from '@workos-inc/node'; | ||
| } | ||
| /** | ||
| * Map of HTTP response headers. | ||
| * | ||
| * `Set-Cookie` MUST be represented as `string[]` when multiple values exist | ||
| * — adapters must append each entry as its own header, never comma-join. | ||
| * A comma-joined `Set-Cookie` string is not a valid single HTTP header. | ||
| */ | ||
| export type HeadersBag = Record<string, string | string[]>; | ||
| export interface SessionStorage<TRequest, TResponse, TOptions = unknown> { | ||
| /** | ||
| * Read a named cookie from a request. | ||
| * | ||
| * MUST return the URL-decoded value (the inverse of `setCookie`'s | ||
| * `encodeURIComponent` on write). Returning raw on-wire bytes will break | ||
| * byte-comparison against the original value — notably PKCE state | ||
| * verification — for any seal format that contains characters | ||
| * `encodeURIComponent` escapes (`+`, `=`, etc.). | ||
| * | ||
| * @param request Framework-specific request object. | ||
| * @param name Cookie name. | ||
| * @returns The URL-decoded cookie value or null if absent. | ||
| */ | ||
| getCookie(request: TRequest, name: string): Promise<string | null>; | ||
| /** | ||
| * Write a named cookie to a response. | ||
| * @param response Framework-specific response object (or undefined to emit headers only). | ||
| * @param name Cookie name. | ||
| * @param value Cookie value (will be URL-encoded). | ||
| * @param options Per-call cookie options. | ||
| */ | ||
| setCookie(response: TResponse | undefined, name: string, value: string, options: CookieOptions): Promise<{ | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }>; | ||
| /** | ||
| * Clear a named cookie by emitting a Set-Cookie with Max-Age=0. | ||
| * @param response Framework-specific response object (or undefined to emit headers only). | ||
| * @param name Cookie name. | ||
| * @param options Cookie options (must match those used at set time, especially `path`). | ||
| */ | ||
| clearCookie(response: TResponse | undefined, name: string, options: CookieOptions): Promise<{ | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }>; | ||
| getSession(request: TRequest): Promise<string | null>; | ||
@@ -99,4 +141,18 @@ /** | ||
| password: string; | ||
| ttl?: number | undefined; | ||
| }) => Promise<T>; | ||
| } | ||
| /** | ||
| * Result shape returned by `createAuthorization` / `createSignIn` / `createSignUp`. | ||
| * | ||
| * The verifier cookie is written internally via `SessionStorage.setCookie` — callers | ||
| * only need to redirect the browser to `url` and apply any returned `headers`/`response`. | ||
| */ | ||
| export interface GetAuthorizationUrlResult { | ||
| url: string; | ||
| } | ||
| export type CreateAuthorizationResult<TResponse> = GetAuthorizationUrlResult & { | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }; | ||
| export interface CookieOptions { | ||
@@ -122,7 +178,20 @@ path?: string; | ||
| prompt?: 'login' | 'none' | 'consent' | 'select_account'; | ||
| /** Custom state to pass through the OAuth flow. Returned in handleCallback. */ | ||
| /** | ||
| * Custom state to pass through the OAuth flow. Returned in handleCallback. | ||
| * | ||
| * Bounded at 2048 UTF-8 bytes. The value is sealed alongside the PKCE | ||
| * verifier and stored in the `wos-auth-verifier` cookie; oversized values | ||
| * would push the cookie past the per-cookie browser limit (~4 KB) and | ||
| * would be silently dropped. Values over the supported limit throw | ||
| * `PKCEPayloadTooLargeError` at sign-in time. | ||
| * | ||
| * Note: total sealed payload size — not just `state` — drives the hard | ||
| * failure. Combining a large `returnPathname`, `redirectUri`, or | ||
| * `cookieDomain` with near-limit `state` can still overflow. | ||
| */ | ||
| state?: string; | ||
| } | ||
| /** | ||
| * Options for getAuthorizationUrl, including screenHint | ||
| * Options for `createAuthorization` / `createSignIn` / `createSignUp`, | ||
| * including the `screenHint` selector used by the sign-in/sign-up variants. | ||
| */ | ||
@@ -129,0 +198,0 @@ export interface GetAuthorizationUrlOptions extends AuthUrlOptions { |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/session/types.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/session/types.ts"],"names":[],"mappings":"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"} |
+1
-1
@@ -31,3 +31,3 @@ /** | ||
| export { getWorkOS } from './core/client/workos.js'; | ||
| export { AuthKitError, SessionEncryptionError, TokenValidationError, TokenRefreshError, } from './core/errors.js'; | ||
| export { AuthKitError, OAuthStateMismatchError, PKCECookieMissingError, PKCEPayloadTooLargeError, SessionEncryptionError, TokenValidationError, TokenRefreshError, } from './core/errors.js'; | ||
| export * from './core/session/types.js'; | ||
@@ -34,0 +34,0 @@ export * from './core/config/types.js'; |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAKzD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAKhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAK9E,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAK5F,OAAO,EACL,SAAS,EACT,SAAS,EACT,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAK/E,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAKpD,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAK1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,YAAY,EACV,IAAI,EACJ,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,sBAAsB,GACvB,MAAM,kBAAkB,CAAC"} | ||
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAKzD,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAKhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAK9E,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAK5F,OAAO,EACL,SAAS,EACT,SAAS,EACT,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAK/E,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAKpD,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAK1B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,YAAY,EACV,IAAI,EACJ,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,sBAAsB,GACvB,MAAM,kBAAkB,CAAC"} |
+1
-1
@@ -52,3 +52,3 @@ /** | ||
| // ============================================ | ||
| export { AuthKitError, SessionEncryptionError, TokenValidationError, TokenRefreshError, } from './core/errors.js'; | ||
| export { AuthKitError, OAuthStateMismatchError, PKCECookieMissingError, PKCEPayloadTooLargeError, SessionEncryptionError, TokenValidationError, TokenRefreshError, } from './core/errors.js'; | ||
| // ============================================ | ||
@@ -55,0 +55,0 @@ // Type Exports |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,+CAA+C;AAC/C,aAAa;AACb,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,+CAA+C;AAC/C,6BAA6B;AAC7B,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,+CAA+C;AAC/C,kBAAkB;AAClB,+CAA+C;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAE9E,+CAA+C;AAC/C,sBAAsB;AACtB,+CAA+C;AAC/C,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAE5F,+CAA+C;AAC/C,gBAAgB;AAChB,+CAA+C;AAC/C,OAAO,EACL,SAAS,EACT,SAAS,EACT,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAE/E,+CAA+C;AAC/C,iBAAiB;AACjB,+CAA+C;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,+CAA+C;AAC/C,SAAS;AACT,+CAA+C;AAC/C,OAAO,EACL,YAAY,EACZ,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,+CAA+C;AAC/C,eAAe;AACf,+CAA+C;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC"} | ||
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,+CAA+C;AAC/C,aAAa;AACb,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzD,+CAA+C;AAC/C,6BAA6B;AAC7B,+CAA+C;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,+CAA+C;AAC/C,kBAAkB;AAClB,+CAA+C;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAE9E,+CAA+C;AAC/C,sBAAsB;AACtB,+CAA+C;AAC/C,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,8CAA8C,CAAC;AAE5F,+CAA+C;AAC/C,gBAAgB;AAChB,+CAA+C;AAC/C,OAAO,EACL,SAAS,EACT,SAAS,EACT,wBAAwB,EACxB,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAE/E,+CAA+C;AAC/C,iBAAiB;AACjB,+CAA+C;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,+CAA+C;AAC/C,SAAS;AACT,+CAA+C;AAC/C,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,iBAAiB,GAClB,MAAM,kBAAkB,CAAC;AAE1B,+CAA+C;AAC/C,eAAe;AACf,+CAA+C;AAC/C,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC"} |
| import type { WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitCore } from '../core/AuthKitCore.js'; | ||
| import type { AuthKitConfig } from '../core/config/types.js'; | ||
| import type { AuthResult, GetAuthorizationUrlOptions, Session } from '../core/session/types.js'; | ||
| import { type GeneratedAuthorizationUrl } from '../core/pkce/generateAuthorizationUrl.js'; | ||
| import type { AuthResult, GetAuthorizationUrlOptions, Session, SessionEncryption } from '../core/session/types.js'; | ||
| /** | ||
@@ -21,3 +22,4 @@ * AuthOperations provides high-level authentication operations. | ||
| private config; | ||
| constructor(core: AuthKitCore, client: WorkOS, config: AuthKitConfig); | ||
| private encryption; | ||
| constructor(core: AuthKitCore, client: WorkOS, config: AuthKitConfig, encryption: SessionEncryption); | ||
| /** | ||
@@ -65,21 +67,21 @@ * Get the WorkOS logout URL. | ||
| /** | ||
| * Get authorization URL for WorkOS authentication. | ||
| * Create a PKCE-bound WorkOS authorization URL. | ||
| * | ||
| * State encoding format: `{internal}.{userState}` where internal is URL-safe | ||
| * base64 encoded JSON containing returnPathname. This allows customers to | ||
| * pass their own state through the OAuth flow. | ||
| * Returns the URL, the sealed state blob (to be used as both the cookie | ||
| * value and — already present in the URL — the OAuth `state` param), and | ||
| * the cookie options the verifier cookie should be written with. | ||
| * AuthService dispatches the write via `SessionStorage.setCookie`. | ||
| * | ||
| * @param options - Authorization URL options (returnPathname, screenHint, state, etc.) | ||
| * @returns The authorization URL | ||
| * @param options - returnPathname, screenHint, custom state, redirectUri, etc. | ||
| */ | ||
| getAuthorizationUrl(options?: GetAuthorizationUrlOptions): Promise<string>; | ||
| createAuthorization(options?: GetAuthorizationUrlOptions): Promise<GeneratedAuthorizationUrl>; | ||
| /** | ||
| * Convenience method: Get sign-in URL. | ||
| * Convenience method: Create sign-in authorization URL. | ||
| */ | ||
| getSignInUrl(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<string>; | ||
| createSignIn(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<GeneratedAuthorizationUrl>; | ||
| /** | ||
| * Convenience method: Get sign-up URL. | ||
| * Convenience method: Create sign-up authorization URL. | ||
| */ | ||
| getSignUpUrl(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<string>; | ||
| createSignUp(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<GeneratedAuthorizationUrl>; | ||
| } | ||
| //# sourceMappingURL=AuthOperations.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthOperations.d.ts","sourceRoot":"","sources":["../../src/operations/AuthOperations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EACV,UAAU,EACV,0BAA0B,EAC1B,OAAO,EACR,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAgB;gBAElB,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa;IAMpE;;;;;;;;;OASG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM;IAOxE;;;;;;;;;OASG;IACG,kBAAkB,CACtB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;QACT,IAAI,EAAE,UAAU,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IAKF;;;;;;;;;OASG;IACG,cAAc,CAClB,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;QACT,IAAI,EAAE,UAAU,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IA8BF;;;;;;;;;OASG;IACG,mBAAmB,CACvB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,MAAM,CAAC;IA2BlB;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM,GAC3D,OAAO,CAAC,MAAM,CAAC;IAOlB;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM,GAC3D,OAAO,CAAC,MAAM,CAAC;CAMnB"} | ||
| {"version":3,"file":"AuthOperations.d.ts","sourceRoot":"","sources":["../../src/operations/AuthOperations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EACL,KAAK,yBAAyB,EAE/B,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EACV,UAAU,EACV,0BAA0B,EAC1B,OAAO,EACP,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;GAWG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAc;IAC1B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,UAAU,CAAoB;gBAGpC,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,iBAAiB;IAQ/B;;;;;;;;;OASG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM;IAOxE;;;;;;;;;OASG;IACG,kBAAkB,CACtB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;QACT,IAAI,EAAE,UAAU,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IAKF;;;;;;;;;OASG;IACG,cAAc,CAClB,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,MAAM,GACtB,OAAO,CAAC;QACT,IAAI,EAAE,UAAU,CAAC;QACjB,gBAAgB,EAAE,MAAM,CAAC;KAC1B,CAAC;IA8BF;;;;;;;;;OASG;IACG,mBAAmB,CACvB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC;IASrC;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM,GAC3D,OAAO,CAAC,yBAAyB,CAAC;IAOrC;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM,GAC3D,OAAO,CAAC,yBAAyB,CAAC;CAMtC"} |
@@ -0,1 +1,2 @@ | ||
| import { generateAuthorizationUrl, } from '../core/pkce/generateAuthorizationUrl.js'; | ||
| /** | ||
@@ -17,6 +18,8 @@ * AuthOperations provides high-level authentication operations. | ||
| config; | ||
| constructor(core, client, config) { | ||
| encryption; | ||
| constructor(core, client, config, encryption) { | ||
| this.core = core; | ||
| this.client = client; | ||
| this.config = config; | ||
| this.encryption = encryption; | ||
| } | ||
@@ -87,39 +90,24 @@ /** | ||
| /** | ||
| * Get authorization URL for WorkOS authentication. | ||
| * Create a PKCE-bound WorkOS authorization URL. | ||
| * | ||
| * State encoding format: `{internal}.{userState}` where internal is URL-safe | ||
| * base64 encoded JSON containing returnPathname. This allows customers to | ||
| * pass their own state through the OAuth flow. | ||
| * Returns the URL, the sealed state blob (to be used as both the cookie | ||
| * value and — already present in the URL — the OAuth `state` param), and | ||
| * the cookie options the verifier cookie should be written with. | ||
| * AuthService dispatches the write via `SessionStorage.setCookie`. | ||
| * | ||
| * @param options - Authorization URL options (returnPathname, screenHint, state, etc.) | ||
| * @returns The authorization URL | ||
| * @param options - returnPathname, screenHint, custom state, redirectUri, etc. | ||
| */ | ||
| async getAuthorizationUrl(options = {}) { | ||
| // Build the combined state parameter (matches authkit-nextjs format) | ||
| const internalState = options.returnPathname | ||
| ? btoa(JSON.stringify({ returnPathname: options.returnPathname })) | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| : null; | ||
| // If both internal and custom state, combine as internal.custom | ||
| // Otherwise use whichever is provided | ||
| const state = internalState && options.state | ||
| ? `${internalState}.${options.state}` | ||
| : internalState || options.state || undefined; | ||
| return this.client.userManagement.getAuthorizationUrl({ | ||
| provider: 'authkit', | ||
| redirectUri: options.redirectUri ?? this.config.redirectUri, | ||
| screenHint: options.screenHint, | ||
| organizationId: options.organizationId, | ||
| loginHint: options.loginHint, | ||
| prompt: options.prompt, | ||
| clientId: this.config.clientId, | ||
| state, | ||
| async createAuthorization(options = {}) { | ||
| return generateAuthorizationUrl({ | ||
| client: this.client, | ||
| config: this.config, | ||
| encryption: this.encryption, | ||
| options, | ||
| }); | ||
| } | ||
| /** | ||
| * Convenience method: Get sign-in URL. | ||
| * Convenience method: Create sign-in authorization URL. | ||
| */ | ||
| async getSignInUrl(options = {}) { | ||
| return this.getAuthorizationUrl({ | ||
| async createSignIn(options = {}) { | ||
| return this.createAuthorization({ | ||
| ...options, | ||
@@ -130,6 +118,6 @@ screenHint: 'sign-in', | ||
| /** | ||
| * Convenience method: Get sign-up URL. | ||
| * Convenience method: Create sign-up authorization URL. | ||
| */ | ||
| async getSignUpUrl(options = {}) { | ||
| return this.getAuthorizationUrl({ | ||
| async createSignUp(options = {}) { | ||
| return this.createAuthorization({ | ||
| ...options, | ||
@@ -136,0 +124,0 @@ screenHint: 'sign-up', |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthOperations.js","sourceRoot":"","sources":["../../src/operations/AuthOperations.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,cAAc;IACjB,IAAI,CAAc;IAClB,MAAM,CAAS;IACf,MAAM,CAAgB;IAE9B,YAAY,IAAiB,EAAE,MAAc,EAAE,MAAqB;QAClE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED;;;;;;;;;OASG;IACH,YAAY,CAAC,SAAiB,EAAE,OAA+B;QAC7D,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,YAAY,CAAC;YAC7C,SAAS;YACT,QAAQ,EAAE,OAAO,EAAE,QAAQ;SAC5B,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,kBAAkB,CACtB,OAAgB,EAChB,cAAsB;QAKtB,wDAAwD;QACxD,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACtD,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,cAAc,CAClB,OAAgB,EAChB,cAAuB;QAKvB,6DAA6D;QAC7D,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,kBAAkB,CACxE,OAAO,EACP,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,CAChC,CAAC;QAEF,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAEpE,MAAM,IAAI,GAAe;YACvB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,SAAS,EAAE,MAAM,CAAC,GAAG;YACrB,YAAY,EAAE,UAAU,CAAC,YAAY;YACrC,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,YAAY,EAAE,UAAU,CAAC,YAAY;YACrC,MAAM;YACN,cAAc,EAAE,MAAM,CAAC,MAAM;YAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,YAAY,EAAE,MAAM,CAAC,aAAa;SACnC,CAAC;QAEF,OAAO;YACL,IAAI;YACJ,gBAAgB;SACjB,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,mBAAmB,CACvB,UAAsC,EAAE;QAExC,qEAAqE;QACrE,MAAM,aAAa,GAAG,OAAO,CAAC,cAAc;YAC1C,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;iBAC7D,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;iBACnB,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;YACxB,CAAC,CAAC,IAAI,CAAC;QAET,gEAAgE;QAChE,sCAAsC;QACtC,MAAM,KAAK,GACT,aAAa,IAAI,OAAO,CAAC,KAAK;YAC5B,CAAC,CAAC,GAAG,aAAa,IAAI,OAAO,CAAC,KAAK,EAAE;YACrC,CAAC,CAAC,aAAa,IAAI,OAAO,CAAC,KAAK,IAAI,SAAS,CAAC;QAElD,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,mBAAmB,CAAC;YACpD,QAAQ,EAAE,SAAS;YACnB,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC,WAAW;YAC3D,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,cAAc,EAAE,OAAO,CAAC,cAAc;YACtC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;YAC9B,KAAK;SACN,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC;YAC9B,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC;YAC9B,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;CACF"} | ||
| {"version":3,"file":"AuthOperations.js","sourceRoot":"","sources":["../../src/operations/AuthOperations.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,wBAAwB,GACzB,MAAM,0CAA0C,CAAC;AAQlD;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,cAAc;IACjB,IAAI,CAAc;IAClB,MAAM,CAAS;IACf,MAAM,CAAgB;IACtB,UAAU,CAAoB;IAEtC,YACE,IAAiB,EACjB,MAAc,EACd,MAAqB,EACrB,UAA6B;QAE7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED;;;;;;;;;OASG;IACH,YAAY,CAAC,SAAiB,EAAE,OAA+B;QAC7D,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,YAAY,CAAC;YAC7C,SAAS;YACT,QAAQ,EAAE,OAAO,EAAE,QAAQ;SAC5B,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,kBAAkB,CACtB,OAAgB,EAChB,cAAsB;QAKtB,wDAAwD;QACxD,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACtD,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,cAAc,CAClB,OAAgB,EAChB,cAAuB;QAKvB,6DAA6D;QAC7D,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,kBAAkB,CACxE,OAAO,EACP,EAAE,KAAK,EAAE,IAAI,EAAE,cAAc,EAAE,CAChC,CAAC;QAEF,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;QAEpE,MAAM,IAAI,GAAe;YACvB,IAAI,EAAE,UAAU,CAAC,IAAI;YACrB,SAAS,EAAE,MAAM,CAAC,GAAG;YACrB,YAAY,EAAE,UAAU,CAAC,YAAY;YACrC,WAAW,EAAE,UAAU,CAAC,WAAW;YACnC,YAAY,EAAE,UAAU,CAAC,YAAY;YACrC,MAAM;YACN,cAAc,EAAE,MAAM,CAAC,MAAM;YAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,YAAY,EAAE,MAAM,CAAC,aAAa;SACnC,CAAC;QAEF,OAAO;YACL,IAAI;YACJ,gBAAgB;SACjB,CAAC;IACJ,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,mBAAmB,CACvB,UAAsC,EAAE;QAExC,OAAO,wBAAwB,CAAC;YAC9B,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,OAAO;SACR,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC;YAC9B,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC;YAC9B,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;CACF"} |
| import type { WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitConfig } from '../core/config/types.js'; | ||
| import type { AuthResult, CustomClaims, GetAuthorizationUrlOptions, HeadersBag, Session, SessionEncryption, SessionStorage } from '../core/session/types.js'; | ||
| import type { AuthResult, CreateAuthorizationResult, CustomClaims, GetAuthorizationUrlOptions, HeadersBag, Session, SessionEncryption, SessionStorage } from '../core/session/types.js'; | ||
| /** | ||
@@ -15,3 +15,3 @@ * Framework-agnostic authentication service. | ||
| * - `handleCallback()` - Process OAuth callback | ||
| * - `signOut()`, `getSignInUrl()`, etc. - Delegate to AuthOperations | ||
| * - `signOut()`, `createSignIn()`, etc. - Delegate to AuthOperations | ||
| * | ||
@@ -102,14 +102,35 @@ * **Used by:** @workos/authkit-tanstack-react-start | ||
| /** | ||
| * Get authorization URL - delegates to AuthOperations. | ||
| * Create an authorization URL and write the PKCE verifier cookie. | ||
| * | ||
| * @param response - Framework-specific response object (may be undefined — | ||
| * adapters that mutate responses in-place will get back a mutated copy). | ||
| * @param options - screenHint, returnPathname, custom state, redirectUri, etc. | ||
| * @returns The URL to redirect the browser to, plus storage's | ||
| * `{ response?, headers? }` carrying the verifier `Set-Cookie`. | ||
| */ | ||
| getAuthorizationUrl(options?: GetAuthorizationUrlOptions): Promise<string>; | ||
| createAuthorization(response: TResponse | undefined, options?: GetAuthorizationUrlOptions): Promise<CreateAuthorizationResult<TResponse>>; | ||
| /** | ||
| * Convenience: Get sign-in URL. | ||
| * Convenience: Create sign-in URL and write the PKCE verifier cookie. | ||
| */ | ||
| getSignInUrl(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<string>; | ||
| createSignIn(response: TResponse | undefined, options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<CreateAuthorizationResult<TResponse>>; | ||
| /** | ||
| * Convenience: Get sign-up URL. | ||
| * Convenience: Create sign-up URL and write the PKCE verifier cookie. | ||
| */ | ||
| getSignUpUrl(options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<string>; | ||
| createSignUp(response: TResponse | undefined, options?: Omit<GetAuthorizationUrlOptions, 'screenHint'>): Promise<CreateAuthorizationResult<TResponse>>; | ||
| /** | ||
| * Emit a `Set-Cookie` header that clears the PKCE verifier cookie. | ||
| * | ||
| * Use on any exit path where a sign-in was started (verifier cookie | ||
| * written) but `handleCallback` will not run to clear it — OAuth error | ||
| * responses, missing `code`, early bail-outs. | ||
| * | ||
| * Pass `options.redirectUri` on requests that used a per-request | ||
| * `redirectUri` override at sign-in time, so the delete cookie's computed | ||
| * attributes (notably `secure`) match what was originally set. | ||
| */ | ||
| clearPendingVerifier(response: TResponse | undefined, options?: Pick<GetAuthorizationUrlOptions, 'redirectUri'>): Promise<{ | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }>; | ||
| /** | ||
| * Get the WorkOS client instance. | ||
@@ -121,12 +142,20 @@ * Useful for direct API calls not covered by AuthKit. | ||
| * Handle OAuth callback. | ||
| * This creates a new session after successful authentication. | ||
| * | ||
| * @param request - Framework-specific request (not currently used) | ||
| * @param response - Framework-specific response | ||
| * @param options - OAuth callback options (code, state) | ||
| * @returns Updated response, return pathname, and auth response | ||
| * Reads the verifier cookie via storage, verifies it against the URL | ||
| * `state`, exchanges the code, saves the session, and — on success — | ||
| * also emits a verifier-delete `Set-Cookie` so `HeadersBag['Set-Cookie']` | ||
| * carries both entries as a `string[]`. Adapters MUST append each | ||
| * `Set-Cookie` as its own header (never comma-join). | ||
| * | ||
| * Error-path cleanup: if any step after the cookie read throws | ||
| * (state mismatch, tampered seal, code-exchange failure, session-save | ||
| * failure), `handleCallback` attempts a best-effort `clearCookie` on the | ||
| * response before rethrowing so the verifier does not linger for its full | ||
| * 600s TTL. Response-mutating adapters get the delete `Set-Cookie` | ||
| * applied in place; headers-only adapters should still call | ||
| * `clearPendingVerifier` in their catch block to capture the headers bag. | ||
| */ | ||
| handleCallback(_request: TRequest, response: TResponse, options: { | ||
| handleCallback(request: TRequest, response: TResponse, options: { | ||
| code: string; | ||
| state?: string; | ||
| state: string | undefined; | ||
| }): Promise<{ | ||
@@ -139,3 +168,17 @@ response: TResponse | undefined; | ||
| }>; | ||
| /** | ||
| * Best-effort verifier cleanup on a `handleCallback` error path. | ||
| * | ||
| * Mutates the response in place for response-mutating adapters. | ||
| * Swallows storage errors — cleanup must never mask the original failure. | ||
| * The original error is always rethrown by the caller. | ||
| * | ||
| * `schemeAgnostic` (pre-unseal failures only): drops the `Secure` attribute | ||
| * on the delete when `sameSite === 'lax'` so the browser accepts the | ||
| * `Set-Cookie` over http:// dev callbacks. Cookie replacement matches on | ||
| * (name, domain, path), not `Secure`, so the tuple-matched original cookie | ||
| * is still cleared regardless of its original `Secure` flag. | ||
| */ | ||
| private bestEffortClearVerifier; | ||
| } | ||
| //# sourceMappingURL=AuthService.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthService.d.ts","sourceRoot":"","sources":["../../src/service/AuthService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,0BAA0B,EAC1B,UAAU,EACV,OAAO,EACP,iBAAiB,EACjB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;;;;;;;GAcG;AACH,qBAAa,WAAW,CAAC,QAAQ,EAAE,SAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAG9B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,EAC5C,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,iBAAiB;IAS/B;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,aAAa,GAAG,YAAY,EACzC,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC;QACT,IAAI,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC;QAChC,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;IAsCF;;;;;OAKG;IACG,UAAU,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAQ5D;;;;;;OAMG;IACG,WAAW,CACf,QAAQ,EAAE,SAAS,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAI1D;;;;;OAKG;IACG,YAAY,CAChB,QAAQ,EAAE,SAAS,GAClB,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,SAAS,CAAC;QAAC,OAAO,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAI1D;;;;;;;;;OASG;IACG,OAAO,CACX,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAC9B,OAAO,CAAC;QACT,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,SAAS,CAAC;QACrB,OAAO,CAAC,EAAE,UAAU,CAAC;KACtB,CAAC;IAMF;;OAEG;IACG,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM;;;;IAIjE;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,CAAC,EAAE,MAAM;;;;IAI9D;;OAEG;IACG,mBAAmB,CAAC,OAAO,GAAE,0BAA+B;IAIlE;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM;IAK9D;;OAEG;IACG,YAAY,CAChB,OAAO,GAAE,IAAI,CAAC,0BAA0B,EAAE,YAAY,CAAM;IAK9D;;;OAGG;IACH,SAAS,IAAI,MAAM;IAInB;;;;;;;;OAQG;IACG,cAAc,CAClB,QAAQ,EAAE,QAAQ,EAClB,QAAQ,EAAE,SAAS,EACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;;;;;;;CA8D5C"} | ||
| {"version":3,"file":"AuthService.d.ts","sourceRoot":"","sources":["../../src/service/AuthService.ts"],"names":[],"mappings":"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"} |
+157
-71
| import { AuthKitCore } from '../core/AuthKitCore.js'; | ||
| import { getPKCECookieOptions, PKCE_COOKIE_NAME, } from '../core/pkce/cookieOptions.js'; | ||
| import { AuthOperations } from '../operations/AuthOperations.js'; | ||
| /** | ||
| * Merge two `HeadersBag` values. `Set-Cookie` matching is case-insensitive; | ||
| * existing key casing is preserved. Multiple `Set-Cookie` values are | ||
| * concatenated into a `string[]`. Other keys are shallow-merged (second wins). | ||
| */ | ||
| function mergeHeaderBags(a, b) { | ||
| if (!a) | ||
| return b; | ||
| if (!b) | ||
| return a; | ||
| const merged = { ...a }; | ||
| let setCookieKey = Object.keys(merged).find(k => k.toLowerCase() === 'set-cookie'); | ||
| for (const [key, value] of Object.entries(b)) { | ||
| if (key.toLowerCase() !== 'set-cookie') { | ||
| merged[key] = value; | ||
| continue; | ||
| } | ||
| if (!setCookieKey) { | ||
| merged[key] = value; | ||
| setCookieKey = key; | ||
| continue; | ||
| } | ||
| const left = merged[setCookieKey]; | ||
| const leftArr = Array.isArray(left) ? left : [left]; | ||
| const rightArr = Array.isArray(value) ? value : [value]; | ||
| merged[setCookieKey] = [...leftArr, ...rightArr]; | ||
| } | ||
| return merged; | ||
| } | ||
| /** | ||
| * Framework-agnostic authentication service. | ||
@@ -14,3 +44,3 @@ * | ||
| * - `handleCallback()` - Process OAuth callback | ||
| * - `signOut()`, `getSignInUrl()`, etc. - Delegate to AuthOperations | ||
| * - `signOut()`, `createSignIn()`, etc. - Delegate to AuthOperations | ||
| * | ||
@@ -30,3 +60,3 @@ * **Used by:** @workos/authkit-tanstack-react-start | ||
| this.core = new AuthKitCore(config, client, encryption); | ||
| this.operations = new AuthOperations(this.core, client, config); | ||
| this.operations = new AuthOperations(this.core, client, config, encryption); | ||
| } | ||
@@ -135,20 +165,48 @@ /** | ||
| /** | ||
| * Get authorization URL - delegates to AuthOperations. | ||
| * Create an authorization URL and write the PKCE verifier cookie. | ||
| * | ||
| * @param response - Framework-specific response object (may be undefined — | ||
| * adapters that mutate responses in-place will get back a mutated copy). | ||
| * @param options - screenHint, returnPathname, custom state, redirectUri, etc. | ||
| * @returns The URL to redirect the browser to, plus storage's | ||
| * `{ response?, headers? }` carrying the verifier `Set-Cookie`. | ||
| */ | ||
| async getAuthorizationUrl(options = {}) { | ||
| return this.operations.getAuthorizationUrl(options); | ||
| async createAuthorization(response, options = {}) { | ||
| const { url, sealedState, cookieOptions } = await this.operations.createAuthorization(options); | ||
| const write = await this.storage.setCookie(response, PKCE_COOKIE_NAME, sealedState, cookieOptions); | ||
| return { url, ...write }; | ||
| } | ||
| /** | ||
| * Convenience: Get sign-in URL. | ||
| * Convenience: Create sign-in URL and write the PKCE verifier cookie. | ||
| */ | ||
| async getSignInUrl(options = {}) { | ||
| return this.operations.getSignInUrl(options); | ||
| async createSignIn(response, options = {}) { | ||
| return this.createAuthorization(response, { | ||
| ...options, | ||
| screenHint: 'sign-in', | ||
| }); | ||
| } | ||
| /** | ||
| * Convenience: Get sign-up URL. | ||
| * Convenience: Create sign-up URL and write the PKCE verifier cookie. | ||
| */ | ||
| async getSignUpUrl(options = {}) { | ||
| return this.operations.getSignUpUrl(options); | ||
| async createSignUp(response, options = {}) { | ||
| return this.createAuthorization(response, { | ||
| ...options, | ||
| screenHint: 'sign-up', | ||
| }); | ||
| } | ||
| /** | ||
| * Emit a `Set-Cookie` header that clears the PKCE verifier cookie. | ||
| * | ||
| * Use on any exit path where a sign-in was started (verifier cookie | ||
| * written) but `handleCallback` will not run to clear it — OAuth error | ||
| * responses, missing `code`, early bail-outs. | ||
| * | ||
| * Pass `options.redirectUri` on requests that used a per-request | ||
| * `redirectUri` override at sign-in time, so the delete cookie's computed | ||
| * attributes (notably `secure`) match what was originally set. | ||
| */ | ||
| async clearPendingVerifier(response, options) { | ||
| return this.storage.clearCookie(response, PKCE_COOKIE_NAME, getPKCECookieOptions(this.config, options?.redirectUri)); | ||
| } | ||
| /** | ||
| * Get the WorkOS client instance. | ||
@@ -162,67 +220,95 @@ * Useful for direct API calls not covered by AuthKit. | ||
| * Handle OAuth callback. | ||
| * This creates a new session after successful authentication. | ||
| * | ||
| * @param request - Framework-specific request (not currently used) | ||
| * @param response - Framework-specific response | ||
| * @param options - OAuth callback options (code, state) | ||
| * @returns Updated response, return pathname, and auth response | ||
| * Reads the verifier cookie via storage, verifies it against the URL | ||
| * `state`, exchanges the code, saves the session, and — on success — | ||
| * also emits a verifier-delete `Set-Cookie` so `HeadersBag['Set-Cookie']` | ||
| * carries both entries as a `string[]`. Adapters MUST append each | ||
| * `Set-Cookie` as its own header (never comma-join). | ||
| * | ||
| * Error-path cleanup: if any step after the cookie read throws | ||
| * (state mismatch, tampered seal, code-exchange failure, session-save | ||
| * failure), `handleCallback` attempts a best-effort `clearCookie` on the | ||
| * response before rethrowing so the verifier does not linger for its full | ||
| * 600s TTL. Response-mutating adapters get the delete `Set-Cookie` | ||
| * applied in place; headers-only adapters should still call | ||
| * `clearPendingVerifier` in their catch block to capture the headers bag. | ||
| */ | ||
| async handleCallback(_request, response, options) { | ||
| // Authenticate with WorkOS using the OAuth code | ||
| const authResponse = await this.client.userManagement.authenticateWithCode({ | ||
| code: options.code, | ||
| clientId: this.config.clientId, | ||
| }); | ||
| // Create and save the new session | ||
| const session = { | ||
| accessToken: authResponse.accessToken, | ||
| refreshToken: authResponse.refreshToken, | ||
| user: authResponse.user, | ||
| impersonator: authResponse.impersonator, | ||
| }; | ||
| const encryptedSession = await this.core.encryptSession(session); | ||
| const { response: updatedResponse, headers } = await this.saveSession(response, encryptedSession); | ||
| // Parse state: format is `{internal}.{userState}` or legacy `{base64JSON}` | ||
| let returnPathname = '/'; | ||
| let customState; | ||
| if (options.state) { | ||
| if (options.state.includes('.')) { | ||
| const [internal, ...rest] = options.state.split('.'); | ||
| customState = rest.join('.'); // Rejoin in case userState contains dots | ||
| try { | ||
| // Reverse URL-safe base64 encoding and decode | ||
| const decoded = (internal ?? '') | ||
| .replace(/-/g, '+') | ||
| .replace(/_/g, '/'); | ||
| const parsed = JSON.parse(atob(decoded)); | ||
| returnPathname = parsed.returnPathname || '/'; | ||
| } | ||
| catch { | ||
| // Malformed internal state, use default | ||
| } | ||
| } | ||
| else { | ||
| try { | ||
| const parsed = JSON.parse(atob(options.state)); | ||
| if (parsed.returnPathname) { | ||
| returnPathname = parsed.returnPathname; | ||
| } | ||
| else { | ||
| customState = options.state; | ||
| } | ||
| } | ||
| catch { | ||
| customState = options.state; | ||
| } | ||
| } | ||
| async handleCallback(request, response, options) { | ||
| const cookieValue = await this.storage.getCookie(request, PKCE_COOKIE_NAME); | ||
| let unsealed; | ||
| try { | ||
| unsealed = await this.core.verifyCallbackState({ | ||
| stateFromUrl: options.state, | ||
| cookieValue: cookieValue ?? undefined, | ||
| }); | ||
| } | ||
| return { | ||
| response: updatedResponse, | ||
| headers, | ||
| returnPathname, | ||
| state: customState, | ||
| authResponse, | ||
| }; | ||
| catch (err) { | ||
| // Pre-unseal failure — we don't know the per-request redirectUri | ||
| // override, so emit a scheme-agnostic delete: same (name, domain, | ||
| // path) tuple as the original set (what the browser uses to match | ||
| // for replacement), with `Secure` dropped in the `sameSite: 'lax'` | ||
| // case so the Set-Cookie is accepted over http:// callbacks too. | ||
| // The `sameSite: 'none'` case already forces Secure on both set and | ||
| // clear, so there's no scheme-mismatch risk there. | ||
| await this.bestEffortClearVerifier(response, undefined, { | ||
| schemeAgnostic: true, | ||
| }); | ||
| throw err; | ||
| } | ||
| const { codeVerifier, returnPathname, customState, redirectUri } = unsealed; | ||
| const clearOptions = getPKCECookieOptions(this.config, redirectUri); | ||
| try { | ||
| const authResponse = await this.client.userManagement.authenticateWithCode({ | ||
| code: options.code, | ||
| clientId: this.config.clientId, | ||
| codeVerifier, | ||
| }); | ||
| const session = { | ||
| accessToken: authResponse.accessToken, | ||
| refreshToken: authResponse.refreshToken, | ||
| user: authResponse.user, | ||
| impersonator: authResponse.impersonator, | ||
| }; | ||
| const encryptedSession = await this.core.encryptSession(session); | ||
| const save = await this.storage.saveSession(response, encryptedSession); | ||
| const clear = await this.storage.clearCookie(save.response ?? response, PKCE_COOKIE_NAME, clearOptions); | ||
| return { | ||
| response: clear.response ?? save.response, | ||
| headers: mergeHeaderBags(save.headers, clear.headers), | ||
| returnPathname: returnPathname ?? '/', | ||
| state: customState, | ||
| authResponse, | ||
| }; | ||
| } | ||
| catch (err) { | ||
| await this.bestEffortClearVerifier(response, redirectUri); | ||
| throw err; | ||
| } | ||
| } | ||
| /** | ||
| * Best-effort verifier cleanup on a `handleCallback` error path. | ||
| * | ||
| * Mutates the response in place for response-mutating adapters. | ||
| * Swallows storage errors — cleanup must never mask the original failure. | ||
| * The original error is always rethrown by the caller. | ||
| * | ||
| * `schemeAgnostic` (pre-unseal failures only): drops the `Secure` attribute | ||
| * on the delete when `sameSite === 'lax'` so the browser accepts the | ||
| * `Set-Cookie` over http:// dev callbacks. Cookie replacement matches on | ||
| * (name, domain, path), not `Secure`, so the tuple-matched original cookie | ||
| * is still cleared regardless of its original `Secure` flag. | ||
| */ | ||
| async bestEffortClearVerifier(response, redirectUri, { schemeAgnostic = false } = {}) { | ||
| const options = getPKCECookieOptions(this.config, redirectUri); | ||
| if (schemeAgnostic && options.sameSite === 'lax') { | ||
| options.secure = false; | ||
| } | ||
| try { | ||
| await this.storage.clearCookie(response, PKCE_COOKIE_NAME, options); | ||
| } | ||
| catch { | ||
| // Swallow: cleanup is opportunistic; callers get the original error. | ||
| } | ||
| } | ||
| } | ||
| //# sourceMappingURL=AuthService.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"AuthService.js","sourceRoot":"","sources":["../../src/service/AuthService.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"AuthService.js","sourceRoot":"","sources":["../../src/service/AuthService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAErD,OAAO,EACL,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAYjE;;;;GAIG;AACH,SAAS,eAAe,CACtB,CAAyB,EACzB,CAAyB;IAEzB,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,MAAM,MAAM,GAAe,EAAE,GAAG,CAAC,EAAE,CAAC;IACpC,IAAI,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CACzC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,YAAY,CACtC,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,IAAI,GAAG,CAAC,WAAW,EAAE,KAAK,YAAY,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;YACpB,YAAY,GAAG,GAAG,CAAC;YACnB,SAAS;QACX,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,CAAE,CAAC;QACnC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACxD,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,WAAW;IACL,IAAI,CAAc;IAClB,UAAU,CAAiB;IAC3B,OAAO,CAAsC;IAC7C,MAAM,CAAgB;IACtB,MAAM,CAAS;IAEhC,YACE,MAAqB,EACrB,OAA4C,EAC5C,MAAc,EACd,UAA6B;QAE7B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC,UAAU,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9E,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,QAAQ,CACZ,OAAiB;QAKjB,IAAI,CAAC;YACH,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAChE,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACtB,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;YAClC,CAAC;YAED,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAClC,MAAM,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAChC,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,CACjD,CAAC;YAEJ,MAAM,IAAI,GAA8B;gBACtC,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,MAAM;gBACN,YAAY,EAAE,OAAO,CAAC,YAAY;gBAClC,WAAW,EAAE,OAAO,CAAC,WAAW;gBAChC,SAAS,EAAE,MAAM,CAAC,GAAG;gBACrB,cAAc,EAAE,MAAM,CAAC,MAAM;gBAC7B,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,WAAW,EAAE,MAAM,CAAC,WAAW;gBAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;gBACjC,YAAY,EAAE,MAAM,CAAC,aAAa;aACnC,CAAC;YAEF,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,oBAAoB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;gBACrE,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;YACxC,CAAC;YAED,OAAO,EAAE,IAAI,EAAE,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QAClC,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,UAAU,CAAC,OAAiB;QAChC,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAChE,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACtB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;IACpD,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,WAAW,CACf,QAA+B,EAC/B,WAAmB;QAEnB,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACzD,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,YAAY,CAChB,QAAmB;QAEnB,OAAO,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,OAAO,CACX,SAAiB,EACjB,OAA+B;QAM/B,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;QACnE,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QAC/D,OAAO,EAAE,SAAS,EAAE,GAAG,WAAW,EAAE,CAAC;IACvC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,kBAAkB,CAAC,OAAgB,EAAE,cAAsB;QAC/D,OAAO,IAAI,CAAC,UAAU,CAAC,kBAAkB,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACrE,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,OAAgB,EAAE,cAAuB;QAC5D,OAAO,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,mBAAmB,CACvB,QAA+B,EAC/B,UAAsC,EAAE;QAExC,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,aAAa,EAAE,GACvC,MAAM,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACrD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CACxC,QAAQ,EACR,gBAAgB,EAChB,WAAW,EACX,aAAa,CACd,CAAC;QACF,OAAO,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,QAA+B,EAC/B,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE;YACxC,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAChB,QAA+B,EAC/B,UAA0D,EAAE;QAE5D,OAAO,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE;YACxC,GAAG,OAAO;YACV,UAAU,EAAE,SAAS;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,oBAAoB,CACxB,QAA+B,EAC/B,OAAyD;QAEzD,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,CAC7B,QAAQ,EACR,gBAAgB,EAChB,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,CACxD,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,SAAS;QACP,OAAO,IAAI,CAAC,MAAM,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,cAAc,CAClB,OAAiB,EACjB,QAAmB,EACnB,OAGC;QAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;QAE5E,IAAI,QAAQ,CAAC;QACb,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC;gBAC7C,YAAY,EAAE,OAAO,CAAC,KAAK;gBAC3B,WAAW,EAAE,WAAW,IAAI,SAAS;aACtC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,iEAAiE;YACjE,kEAAkE;YAClE,kEAAkE;YAClE,mEAAmE;YACnE,iEAAiE;YACjE,oEAAoE;YACpE,mDAAmD;YACnD,MAAM,IAAI,CAAC,uBAAuB,CAAC,QAAQ,EAAE,SAAS,EAAE;gBACtD,cAAc,EAAE,IAAI;aACrB,CAAC,CAAC;YACH,MAAM,GAAG,CAAC;QACZ,CAAC;QAED,MAAM,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,QAAQ,CAAC;QAC5E,MAAM,YAAY,GAAG,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAEpE,IAAI,CAAC;YACH,MAAM,YAAY,GAChB,MAAM,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,oBAAoB,CAAC;gBACpD,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ;gBAC9B,YAAY;aACb,CAAC,CAAC;YAEL,MAAM,OAAO,GAAY;gBACvB,WAAW,EAAE,YAAY,CAAC,WAAW;gBACrC,YAAY,EAAE,YAAY,CAAC,YAAY;gBACvC,IAAI,EAAE,YAAY,CAAC,IAAI;gBACvB,YAAY,EAAE,YAAY,CAAC,YAAY;aACxC,CAAC;YAEF,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;YACjE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;YACxE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAC1C,IAAI,CAAC,QAAQ,IAAI,QAAQ,EACzB,gBAAgB,EAChB,YAAY,CACb,CAAC;YAEF,OAAO;gBACL,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ;gBACzC,OAAO,EAAE,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC;gBACrD,cAAc,EAAE,cAAc,IAAI,GAAG;gBACrC,KAAK,EAAE,WAAW;gBAClB,YAAY;aACb,CAAC;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,uBAAuB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;YAC1D,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;OAYG;IACK,KAAK,CAAC,uBAAuB,CACnC,QAA+B,EAC/B,WAA+B,EAC/B,EAAE,cAAc,GAAG,KAAK,KAAmC,EAAE;QAE7D,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAC/D,IAAI,cAAc,IAAI,OAAO,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;YACjD,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC;QACzB,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;IACH,CAAC;CACF"} |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/service/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IAC9D,qBAAqB,EAAE,CACrB,MAAM,EAAE,aAAa,KAClB,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,CAAC;IAClD,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,iBAAiB,CAAC;CAClE,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAoCnC"} | ||
| {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/service/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE;IAC9D,qBAAqB,EAAE,CACrB,MAAM,EAAE,aAAa,KAClB,cAAc,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACzC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,CAAC;IAClD,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,iBAAiB,CAAC;CAClE,GAAG,WAAW,CAAC,QAAQ,EAAE,SAAS,CAAC,CAuCnC"} |
@@ -53,5 +53,6 @@ import { once } from '../utils.js'; | ||
| refreshSession: (session, organizationId) => getService().refreshSession(session, organizationId), | ||
| getAuthorizationUrl: opts => getService().getAuthorizationUrl(opts), | ||
| getSignInUrl: opts => getService().getSignInUrl(opts), | ||
| getSignUpUrl: opts => getService().getSignUpUrl(opts), | ||
| createAuthorization: (response, opts) => getService().createAuthorization(response, opts), | ||
| createSignIn: (response, opts) => getService().createSignIn(response, opts), | ||
| createSignUp: (response, opts) => getService().createSignUp(response, opts), | ||
| clearPendingVerifier: (response, opts) => getService().clearPendingVerifier(response, opts), | ||
| getWorkOS: () => getService().getWorkOS(), | ||
@@ -58,0 +59,0 @@ handleCallback: (request, response, opts) => getService().handleCallback(request, response, opts), |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/service/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,iBAAiB,MAAM,+CAA+C,CAAC;AAK9E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,iBAAiB,CAAsB,OAMtD;IACC,MAAM,EACJ,qBAAqB,EACrB,aAAa,GAAG,GAAG,EAAE,CAAC,SAAS,EAAE,EACjC,iBAAiB,GAAG,GAAG,EAAE,CAAC,iBAAiB,GAC5C,GAAG,OAAO,CAAC;IAEZ,0DAA0D;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;QAC3B,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC7C,OAAO,IAAI,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,sFAAsF;IACtF,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;QACnD,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QACvD,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,CACrC,UAAU,EAAE,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC;QACjD,YAAY,EAAE,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC;QAC7D,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC;QACnE,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAC9C,UAAU,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,cAAc,CAAC;QAC1D,cAAc,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAC1C,UAAU,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC;QACtD,mBAAmB,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,mBAAmB,CAAC,IAAI,CAAC;QACnE,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;QACrD,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;QACrD,SAAS,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE;QACzC,cAAc,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAC1C,UAAU,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC;KACnB,CAAC;AACxC,CAAC"} | ||
| {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/service/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,iBAAiB,MAAM,+CAA+C,CAAC;AAK9E,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,iBAAiB,CAAsB,OAMtD;IACC,MAAM,EACJ,qBAAqB,EACrB,aAAa,GAAG,GAAG,EAAE,CAAC,SAAS,EAAE,EACjC,iBAAiB,GAAG,GAAG,EAAE,CAAC,iBAAiB,GAC5C,GAAG,OAAO,CAAC;IAEZ,0DAA0D;IAC1D,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;QAC3B,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;QACrC,MAAM,UAAU,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC7C,OAAO,IAAI,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,sFAAsF;IACtF,OAAO;QACL,QAAQ,EAAE,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;QACnD,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QACvD,WAAW,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,CACrC,UAAU,EAAE,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC;QACjD,YAAY,EAAE,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC;QAC7D,OAAO,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC;QACnE,kBAAkB,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAC9C,UAAU,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,cAAc,CAAC;QAC1D,cAAc,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,EAAE,CAC1C,UAAU,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,CAAC;QACtD,mBAAmB,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CACtC,UAAU,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC;QAClD,YAAY,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC;QAC3E,YAAY,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC;QAC3E,oBAAoB,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,CACvC,UAAU,EAAE,CAAC,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC;QACnD,SAAS,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,SAAS,EAAE;QACzC,cAAc,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAC1C,UAAU,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC;KACnB,CAAC;AACxC,CAAC"} |
+16
-0
@@ -9,2 +9,18 @@ /** | ||
| export declare function once<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn): (...args: TArgs) => TReturn; | ||
| /** | ||
| * Constant-time byte-array equality. | ||
| * | ||
| * Used for comparing security-sensitive values (OAuth state, tokens, MACs) | ||
| * where early-return on mismatch would leak timing information about how | ||
| * many leading bytes matched. | ||
| * | ||
| * Runtime-portable: no `node:crypto`, no `Buffer` — works in Node, browsers, | ||
| * and edge runtimes. Callers produce `Uint8Array`s via `TextEncoder`. | ||
| * | ||
| * Contract: | ||
| * - Returns false immediately if lengths differ (length is not secret). | ||
| * - Once past the length gate, inspects EVERY byte regardless of mismatches. | ||
| * - No branches whose count depends on byte values. | ||
| */ | ||
| export declare function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean; | ||
| //# sourceMappingURL=utils.d.ts.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACnD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,GAC9B,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAW7B"} | ||
| {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,IAAI,CAAC,KAAK,SAAS,OAAO,EAAE,EAAE,OAAO,EACnD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,GAC9B,CAAC,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAW7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAKvE"} |
+23
-0
@@ -19,2 +19,25 @@ /** | ||
| } | ||
| /** | ||
| * Constant-time byte-array equality. | ||
| * | ||
| * Used for comparing security-sensitive values (OAuth state, tokens, MACs) | ||
| * where early-return on mismatch would leak timing information about how | ||
| * many leading bytes matched. | ||
| * | ||
| * Runtime-portable: no `node:crypto`, no `Buffer` — works in Node, browsers, | ||
| * and edge runtimes. Callers produce `Uint8Array`s via `TextEncoder`. | ||
| * | ||
| * Contract: | ||
| * - Returns false immediately if lengths differ (length is not secret). | ||
| * - Once past the length gate, inspects EVERY byte regardless of mismatches. | ||
| * - No branches whose count depends on byte values. | ||
| */ | ||
| export function constantTimeEqual(a, b) { | ||
| if (a.length !== b.length) | ||
| return false; | ||
| let diff = 0; | ||
| for (let i = 0; i < a.length; i++) | ||
| diff |= a[i] ^ b[i]; | ||
| return diff === 0; | ||
| } | ||
| //# sourceMappingURL=utils.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAClB,EAA+B;IAE/B,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAe,CAAC;IAEpB,OAAO,CAAC,GAAG,IAAW,EAAW,EAAE;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACrB,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC"} | ||
| {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,IAAI,CAClB,EAA+B;IAE/B,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,MAAe,CAAC;IAEpB,OAAO,CAAC,GAAG,IAAW,EAAW,EAAE;QACjC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACrB,MAAM,GAAG,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAa,EAAE,CAAa;IAC5D,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;IACzD,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC"} |
+23
-20
| { | ||
| "name": "@workos/authkit-session", | ||
| "version": "0.3.4", | ||
| "version": "0.4.0", | ||
| "description": "Framework-agnostic authentication library for WorkOS with pluggable storage adapters", | ||
| "keywords": [], | ||
| "license": "MIT", | ||
| "author": "WorkOS", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/workos/authkit-session.git" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "src", | ||
| "README.md" | ||
| ], | ||
| "type": "module", | ||
@@ -14,8 +26,7 @@ "main": "./dist/index.js", | ||
| }, | ||
| "keywords": [], | ||
| "author": "WorkOS", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/workos/authkit-session.git" | ||
| "dependencies": { | ||
| "@workos-inc/node": "^8.0.0", | ||
| "iron-webcrypto": "^2.0.0", | ||
| "jose": "^6.1.3", | ||
| "valibot": "^1.3.1" | ||
| }, | ||
@@ -25,19 +36,10 @@ "devDependencies": { | ||
| "@vitest/coverage-v8": "^4.0.17", | ||
| "prettier": "^3.7.4", | ||
| "oxfmt": "^0.42.0", | ||
| "oxlint": "^1.57.0", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.17" | ||
| }, | ||
| "dependencies": { | ||
| "@workos-inc/node": "8.0.0", | ||
| "iron-webcrypto": "^2.0.0", | ||
| "jose": "^6.1.3" | ||
| }, | ||
| "engines": { | ||
| "node": ">=20.0.0" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "src", | ||
| "README.md" | ||
| ], | ||
| "scripts": { | ||
@@ -48,4 +50,5 @@ "clean": "rm -rf dist", | ||
| "dev": "tsc -p tsconfig.build.json --watch", | ||
| "prettier": "prettier --check .", | ||
| "format": "prettier --write .", | ||
| "format": "oxfmt --write .", | ||
| "format:check": "oxfmt --check .", | ||
| "lint": "oxlint .", | ||
| "typecheck": "tsc --noEmit", | ||
@@ -52,0 +55,0 @@ "test": "vitest --run", |
+94
-27
@@ -52,6 +52,3 @@ # @workos/authkit-session | ||
| ```typescript | ||
| import { | ||
| CookieSessionStorage, | ||
| parseCookieHeader, | ||
| } from '@workos/authkit-session'; | ||
| import { CookieSessionStorage } from '@workos/authkit-session'; | ||
@@ -62,6 +59,10 @@ export class MyFrameworkStorage extends CookieSessionStorage< | ||
| > { | ||
| async getSession(request: Request): Promise<string | null> { | ||
| const cookieHeader = request.headers.get('cookie'); | ||
| if (!cookieHeader) return null; | ||
| return parseCookieHeader(cookieHeader)[this.cookieName] ?? null; | ||
| async getCookie(request: Request, name: string): Promise<string | null> { | ||
| const header = request.headers.get('cookie'); | ||
| if (!header) return null; | ||
| for (const part of header.split(';')) { | ||
| const [k, ...rest] = part.trim().split('='); | ||
| if (k === name) return decodeURIComponent(rest.join('=')); | ||
| } | ||
| return null; | ||
| } | ||
@@ -72,3 +73,3 @@ | ||
| response: Response | undefined, | ||
| headers: Record<string, string>, | ||
| headers: Record<string, string | string[]>, | ||
| ): Promise<{ response: Response }> { | ||
@@ -83,5 +84,9 @@ const newResponse = response | ||
| Object.entries(headers).forEach(([key, value]) => { | ||
| newResponse.headers.append(key, value); | ||
| }); | ||
| for (const [key, value] of Object.entries(headers)) { | ||
| if (Array.isArray(value)) { | ||
| for (const v of value) newResponse.headers.append(key, v); | ||
| } else { | ||
| newResponse.headers.append(key, value); | ||
| } | ||
| } | ||
@@ -93,3 +98,3 @@ return { response: newResponse }; | ||
| `CookieSessionStorage` provides `this.cookieName`, `this.cookieOptions`, `buildSetCookie()`, and implements `saveSession()`/`clearSession()` using your `applyHeaders()`. | ||
| `CookieSessionStorage` provides `this.cookieName`, `this.cookieOptions`, and generic `setCookie`/`clearCookie`/`serializeCookie` primitives. `getSession`/`saveSession`/`clearSession` are one-line wrappers — you only implement `getCookie`. | ||
@@ -125,3 +130,4 @@ ### 3. Create Service | ||
| ); | ||
| if (headers?.['Set-Cookie']) { | ||
| const setCookie = headers?.['Set-Cookie']; | ||
| if (setCookie) { | ||
| const newResponse = new Response(result.response.body, { | ||
@@ -132,3 +138,7 @@ status: result.response.status, | ||
| }); | ||
| newResponse.headers.set('Set-Cookie', headers['Set-Cookie']); | ||
| // Append each entry — never `.set()` with an array (comma-joined | ||
| // Set-Cookie is not a valid single HTTP header). | ||
| for (const v of Array.isArray(setCookie) ? setCookie : [setCookie]) { | ||
| newResponse.headers.append('Set-Cookie', v); | ||
| } | ||
| return { ...result, response: newResponse }; | ||
@@ -170,3 +180,3 @@ } | ||
| | `WORKOS_COOKIE_PASSWORD` | `cookiePassword` | 32+ char encryption key | | ||
| | `WORKOS_COOKIE_NAME` | `cookieName` | Cookie name (default: `wos_session`) | | ||
| | `WORKOS_COOKIE_NAME` | `cookieName` | Cookie name (default: `wos-session`) | | ||
| | `WORKOS_COOKIE_MAX_AGE` | `cookieMaxAge` | Cookie lifetime in seconds | | ||
@@ -191,12 +201,68 @@ | `WORKOS_COOKIE_DOMAIN` | `cookieDomain` | Cookie domain | | ||
| // WorkOS Operations | ||
| authService.signOut(sessionId, { returnTo }) // → { logoutUrl, clearCookieHeader } | ||
| authService.signOut(sessionId, { returnTo }) // → { logoutUrl, response?, headers? } | ||
| authService.refreshSession(session, organizationId?) | ||
| authService.switchOrganization(session, organizationId) | ||
| // URL Generation | ||
| authService.getAuthorizationUrl(options) | ||
| authService.getSignInUrl(options) | ||
| authService.getSignUpUrl(options) | ||
| // URL Generation — write verifier cookie, return { url, response?, headers? } | ||
| authService.createAuthorization(response, options) | ||
| authService.createSignIn(response, options) | ||
| authService.createSignUp(response, options) | ||
| // Error-path cleanup for the PKCE verifier cookie | ||
| // (response may be `undefined` for headers-only adapters) | ||
| authService.clearPendingVerifier(response, { redirectUri? }) | ||
| ``` | ||
| ### PKCE verifier cookie (`wos-auth-verifier`) | ||
| This library binds every OAuth sign-in to a PKCE code verifier, so a leaked | ||
| `state` value on its own cannot be used to complete a session hijack. | ||
| The verifier is sealed into a single blob that serves two roles: | ||
| 1. It is sent to WorkOS as the OAuth `state` query parameter. | ||
| 2. It is set as a short-lived HTTP-only cookie (`wos-auth-verifier`, 10 min). | ||
| The cookie is written and read through `SessionStorage`. Callers don't see | ||
| sealed blobs or cookie options: | ||
| ```typescript | ||
| // Sign in: library writes the verifier cookie via storage, returns the URL + headers | ||
| const { url, headers } = await authService.createSignIn(response, { | ||
| returnPathname: '/dashboard', | ||
| }); | ||
| return new Response(null, { | ||
| status: 302, | ||
| headers: { ...headers, Location: url }, | ||
| }); | ||
| // Callback: library reads the verifier via storage, byte-compares, then exchanges | ||
| await authService.handleCallback(request, response, { | ||
| code, | ||
| state, // from URL | ||
| }); | ||
| ``` | ||
| On success, `handleCallback` returns a `Set-Cookie` entry in `headers` as a | ||
| `string[]` with two values — the session cookie AND a clear for the verifier | ||
| cookie. Adapters must append each entry as its own `Set-Cookie` HTTP header | ||
| (never comma-join). The bag key is case-insensitive — `mergeHeaderBags` | ||
| preserves the adapter's casing — so look it up that way: | ||
| ```ts | ||
| const setCookie = | ||
| result.headers?.['Set-Cookie'] ?? result.headers?.['set-cookie']; | ||
| if (setCookie) { | ||
| for (const v of Array.isArray(setCookie) ? setCookie : [setCookie]) { | ||
| response.headers.append('Set-Cookie', v); | ||
| } | ||
| } | ||
| ``` | ||
| Mismatched state and cookie raise `OAuthStateMismatchError`. A missing cookie | ||
| (typical cause: Set-Cookie stripped by a proxy) raises | ||
| `PKCECookieMissingError`. On either error path — or any early bail-out before | ||
| `handleCallback` runs — call `authService.clearPendingVerifier(response)` to | ||
| emit a delete header. | ||
| ### Direct Access (Advanced) | ||
@@ -210,10 +276,11 @@ | ||
| AuthOperations, | ||
| getConfigurationProvider, | ||
| getWorkOS, | ||
| getConfigurationProvider, | ||
| sessionEncryption, | ||
| } from '@workos/authkit-session'; | ||
| const config = getConfigurationProvider(); | ||
| const client = getWorkOS(config.getConfig()); | ||
| const core = new AuthKitCore(config, client, encryption); | ||
| const operations = new AuthOperations(core, client, config); | ||
| const config = getConfigurationProvider().getConfig(); | ||
| const client = getWorkOS(); | ||
| const core = new AuthKitCore(config, client, sessionEncryption); | ||
| const operations = new AuthOperations(core, client, config, sessionEncryption); | ||
@@ -226,3 +293,3 @@ // Use core.validateAndRefresh(), core.encryptSession(), etc. | ||
| - **JWKS Caching**: Keys fetched on-demand, cached for process lifetime. `jose` handles key rotation automatically. | ||
| - **Token Expiry Buffer**: 60 seconds default. Tokens refresh before actual expiry. | ||
| - **Token Refresh**: `validateAndRefresh` refreshes when `verifyToken` fails (i.e. when the access token is expired or invalid). `isTokenExpiring(token, buffer)` is available as a separate helper for callers that want to proactively refresh before expiry. | ||
| - **Session Encryption**: AES-256-CBC + SHA-256 HMAC via `iron-webcrypto`. | ||
@@ -229,0 +296,0 @@ - **Lazy Initialization**: `createAuthService()` defers initialization until first use, allowing `configure()` to be called later. |
@@ -262,2 +262,111 @@ import { AuthKitCore } from './AuthKitCore.js'; | ||
| }); | ||
| describe('validateAndRefresh()', () => { | ||
| // Decodable JWT with sid + exp + org_id. Signature is garbage → verifyToken false. | ||
| const oldJwt = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInNpZCI6InNlc3Npb25fOTk5IiwiZXhwIjoxMDAwMDAwMDAwLCJvcmdfaWQiOiJvcmdfYWJjIn0.sig'; | ||
| const newJwt = | ||
| 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInNpZCI6InNlc3Npb25fbmV3IiwiZXhwIjozMDAwMDAwMDAwfQ.sig'; | ||
| function makeRefreshClient(capture?: { opts?: any }) { | ||
| return { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| authenticateWithRefreshToken: async (opts: any) => { | ||
| if (capture) capture.opts = opts; | ||
| return { | ||
| accessToken: newJwt, | ||
| refreshToken: 'new-rt', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }; | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| it('forces refresh when token invalid and returns new session', async () => { | ||
| const testCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| makeRefreshClient() as any, | ||
| mockEncryption as any, | ||
| ); | ||
| const session = { | ||
| accessToken: oldJwt, | ||
| refreshToken: 'rt', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }; | ||
| const result = await testCore.validateAndRefresh(session); | ||
| expect(result.refreshed).toBe(true); | ||
| expect(result.session.accessToken).toBe(newJwt); | ||
| }); | ||
| it('propagates explicit organizationId into refresh', async () => { | ||
| const capture: { opts?: any } = {}; | ||
| const testCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| makeRefreshClient(capture) as any, | ||
| mockEncryption as any, | ||
| ); | ||
| await testCore.validateAndRefresh( | ||
| { | ||
| accessToken: oldJwt, | ||
| refreshToken: 'rt', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }, | ||
| { organizationId: 'org_explicit' }, | ||
| ); | ||
| expect(capture.opts?.organizationId).toBe('org_explicit'); | ||
| }); | ||
| it('continues when access token is unparseable', async () => { | ||
| const testCore = new AuthKitCore( | ||
| mockConfig as any, | ||
| makeRefreshClient() as any, | ||
| mockEncryption as any, | ||
| ); | ||
| const session = { | ||
| accessToken: 'not-a-jwt', | ||
| refreshToken: 'rt', | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }; | ||
| const result = await testCore.validateAndRefresh(session); | ||
| expect(result.refreshed).toBe(true); | ||
| }); | ||
| }); | ||
| describe('verifyCallbackState()', () => { | ||
| it('throws OAuthStateMismatchError when stateFromUrl missing', async () => { | ||
| await expect( | ||
| core.verifyCallbackState({ | ||
| stateFromUrl: undefined, | ||
| cookieValue: 'x', | ||
| }), | ||
| ).rejects.toMatchObject({ name: 'OAuthStateMismatchError' }); | ||
| }); | ||
| it('throws PKCECookieMissingError when cookie missing', async () => { | ||
| await expect( | ||
| core.verifyCallbackState({ | ||
| stateFromUrl: 'x', | ||
| cookieValue: undefined, | ||
| }), | ||
| ).rejects.toMatchObject({ name: 'PKCECookieMissingError' }); | ||
| }); | ||
| it('throws OAuthStateMismatchError when values differ', async () => { | ||
| await expect( | ||
| core.verifyCallbackState({ stateFromUrl: 'a', cookieValue: 'b' }), | ||
| ).rejects.toMatchObject({ name: 'OAuthStateMismatchError' }); | ||
| }); | ||
| }); | ||
| }); |
| import type { Impersonator, User, WorkOS } from '@workos-inc/node'; | ||
| import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; | ||
| import { once } from '../utils.js'; | ||
| import { constantTimeEqual, once } from '../utils.js'; | ||
| import type { AuthKitConfig } from './config/types.js'; | ||
| import { SessionEncryptionError, TokenRefreshError } from './errors.js'; | ||
| import { | ||
| OAuthStateMismatchError, | ||
| PKCECookieMissingError, | ||
| SessionEncryptionError, | ||
| TokenRefreshError, | ||
| } from './errors.js'; | ||
| import { type PKCEState, unsealState } from './pkce/state.js'; | ||
| import type { | ||
@@ -107,4 +113,4 @@ BaseTokenClaims, | ||
| return decodeJwt<BaseTokenClaims & TCustomClaims>(token); | ||
| } catch (error) { | ||
| throw new Error('Invalid token'); | ||
| } catch (cause) { | ||
| throw new Error('Invalid token', { cause }); | ||
| } | ||
@@ -152,2 +158,47 @@ } | ||
| /** | ||
| * Verify the OAuth callback state against the PKCE verifier cookie and | ||
| * return the unsealed state blob. | ||
| * | ||
| * Core owns encryption, so verification lives here rather than on | ||
| * AuthService. Byte-compares the query-string `state` against the cookie | ||
| * value BEFORE attempting to decrypt — smaller crypto attack surface and | ||
| * no decryption cost on obvious mismatches. | ||
| * | ||
| * @throws {OAuthStateMismatchError} state missing or does not match cookie | ||
| * @throws {PKCECookieMissingError} cookie not sent — typically a proxy or | ||
| * Set-Cookie propagation issue on the adapter's callback path | ||
| * @throws {SessionEncryptionError} seal tampered, wrong password, TTL expired, | ||
| * or schema mismatch on the unsealed payload | ||
| */ | ||
| async verifyCallbackState(params: { | ||
| stateFromUrl: string | undefined; | ||
| cookieValue: string | undefined; | ||
| }): Promise<PKCEState> { | ||
| const { stateFromUrl, cookieValue } = params; | ||
| if (!stateFromUrl) { | ||
| throw new OAuthStateMismatchError( | ||
| 'Missing state parameter from callback URL', | ||
| ); | ||
| } | ||
| if (!cookieValue) { | ||
| throw new PKCECookieMissingError( | ||
| 'PKCE verifier cookie missing — cannot verify OAuth state. Ensure Set-Cookie headers are propagated on redirects.', | ||
| ); | ||
| } | ||
| const encoder = new TextEncoder(); | ||
| const urlBytes = encoder.encode(stateFromUrl); | ||
| const cookieBytes = encoder.encode(cookieValue); | ||
| if (!constantTimeEqual(urlBytes, cookieBytes)) { | ||
| throw new OAuthStateMismatchError('OAuth state mismatch'); | ||
| } | ||
| return unsealState( | ||
| this.encryption, | ||
| this.config.cookiePassword, | ||
| cookieValue, | ||
| ); | ||
| } | ||
| /** | ||
| * Refresh tokens using WorkOS API. | ||
@@ -154,0 +205,0 @@ * |
@@ -67,2 +67,64 @@ import { SessionEncryption } from './ironWebcryptoEncryption.js'; | ||
| }); | ||
| describe('TTL enforcement', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| }); | ||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
| it('unseals successfully before TTL expires', async () => { | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await encryption.sealData(testData, { | ||
| password: testPassword, | ||
| ttl: 600, | ||
| }); | ||
| // Advance less than TTL | ||
| vi.setSystemTime(new Date('2026-01-01T00:09:00.000Z')); // +540s | ||
| const unsealed = await encryption.unsealData(sealed, { | ||
| password: testPassword, | ||
| ttl: 600, | ||
| }); | ||
| expect(unsealed).toEqual(testData); | ||
| }); | ||
| it('throws when TTL has expired on unseal', async () => { | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await encryption.sealData(testData, { | ||
| password: testPassword, | ||
| ttl: 1, | ||
| }); | ||
| // Advance past TTL + skew (60s default) | ||
| vi.setSystemTime(new Date('2026-01-01T00:02:00.000Z')); | ||
| await expect( | ||
| encryption.unsealData(sealed, { | ||
| password: testPassword, | ||
| ttl: 1, | ||
| }), | ||
| ).rejects.toThrow(); | ||
| }); | ||
| it('preserves session-cookie flow: seal without TTL, unseal without TTL', async () => { | ||
| // This is the invariant AuthKitCore.encryptSession/decryptSession relies on. | ||
| vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z')); | ||
| const sealed = await encryption.sealData(testData, { | ||
| password: testPassword, | ||
| // no ttl — matches AuthKitCore.encryptSession | ||
| }); | ||
| // Advance time substantially — session cookies must still unseal. | ||
| vi.setSystemTime(new Date('2027-01-01T00:00:00.000Z')); | ||
| const unsealed = await encryption.unsealData(sealed, { | ||
| password: testPassword, | ||
| // no ttl — matches AuthKitCore.decryptSession | ||
| }); | ||
| expect(unsealed).toEqual(testData); | ||
| }); | ||
| }); | ||
| }); |
@@ -61,3 +61,3 @@ import { seal as sealData, unseal as unsealData } from 'iron-webcrypto'; | ||
| encryptedData: string, | ||
| { password }: { password: string }, | ||
| { password, ttl = 0 }: { password: string; ttl?: number | undefined }, | ||
| ): Promise<T> { | ||
@@ -84,3 +84,3 @@ // First, parse the seal to extract the version | ||
| }, | ||
| ttl: 0, | ||
| ttl: ttl * 1000, // Convert seconds to milliseconds. 0 = no TTL enforcement. | ||
| timestampSkewSec: 60, | ||
@@ -87,0 +87,0 @@ localtimeOffsetMsec: 0, |
+31
-140
| import { | ||
| AuthKitError, | ||
| OAuthStateMismatchError, | ||
| PKCECookieMissingError, | ||
| PKCEPayloadTooLargeError, | ||
| SessionEncryptionError, | ||
@@ -9,91 +12,17 @@ TokenValidationError, | ||
| describe('AuthKitError', () => { | ||
| it('creates error with message', () => { | ||
| const error = new AuthKitError('Test error'); | ||
| it('carries message, cause, and arbitrary data', () => { | ||
| const cause = new Error('Original'); | ||
| const data = { userId: '123' }; | ||
| const error = new AuthKitError('Test', cause, data); | ||
| expect(error.message).toBe('Test error'); | ||
| expect(error.message).toBe('Test'); | ||
| expect(error.name).toBe('AuthKitError'); | ||
| expect(error).toBeInstanceOf(Error); | ||
| }); | ||
| it('creates error with cause', () => { | ||
| const originalError = new Error('Original error'); | ||
| const error = new AuthKitError('Test error', originalError); | ||
| expect(error.cause).toBe(originalError); | ||
| }); | ||
| it('creates error with data', () => { | ||
| const data = { userId: '123', action: 'login' }; | ||
| const error = new AuthKitError('Test error', undefined, data); | ||
| expect(error.cause).toBe(cause); | ||
| expect(error.data).toEqual(data); | ||
| }); | ||
| it('creates error with cause and data', () => { | ||
| const originalError = new Error('Original error'); | ||
| const data = { userId: '123' }; | ||
| const error = new AuthKitError('Test error', originalError, data); | ||
| expect(error.cause).toBe(originalError); | ||
| expect(error.data).toEqual(data); | ||
| }); | ||
| }); | ||
| describe('SessionEncryptionError', () => { | ||
| it('creates error with correct name', () => { | ||
| const error = new SessionEncryptionError('Encryption failed'); | ||
| expect(error.name).toBe('SessionEncryptionError'); | ||
| expect(error.message).toBe('Encryption failed'); | ||
| expect(error).toBeInstanceOf(AuthKitError); | ||
| expect(error).toBeInstanceOf(Error); | ||
| }); | ||
| it('creates error with cause', () => { | ||
| const originalError = new Error('Crypto error'); | ||
| const error = new SessionEncryptionError( | ||
| 'Encryption failed', | ||
| originalError, | ||
| ); | ||
| expect(error.cause).toBe(originalError); | ||
| }); | ||
| }); | ||
| describe('TokenValidationError', () => { | ||
| it('creates error with correct name', () => { | ||
| const error = new TokenValidationError('Token invalid'); | ||
| expect(error.name).toBe('TokenValidationError'); | ||
| expect(error.message).toBe('Token invalid'); | ||
| expect(error).toBeInstanceOf(AuthKitError); | ||
| expect(error).toBeInstanceOf(Error); | ||
| }); | ||
| it('creates error with cause', () => { | ||
| const originalError = new Error('JWT malformed'); | ||
| const error = new TokenValidationError('Token invalid', originalError); | ||
| expect(error.cause).toBe(originalError); | ||
| }); | ||
| }); | ||
| describe('TokenRefreshError', () => { | ||
| it('creates error with correct name', () => { | ||
| const error = new TokenRefreshError('Refresh failed'); | ||
| expect(error.name).toBe('TokenRefreshError'); | ||
| expect(error.message).toBe('Refresh failed'); | ||
| expect(error).toBeInstanceOf(AuthKitError); | ||
| expect(error).toBeInstanceOf(Error); | ||
| }); | ||
| it('creates error with cause', () => { | ||
| const originalError = new Error('Network error'); | ||
| const error = new TokenRefreshError('Refresh failed', originalError); | ||
| expect(error.cause).toBe(originalError); | ||
| }); | ||
| it('creates error with userId and sessionId', () => { | ||
| describe('TokenRefreshError context', () => { | ||
| it('captures optional userId and sessionId', () => { | ||
| const error = new TokenRefreshError('Refresh failed', undefined, { | ||
@@ -108,33 +37,3 @@ userId: 'user_123', | ||
| it('creates error with cause and context', () => { | ||
| const originalError = new Error('Network error'); | ||
| const error = new TokenRefreshError('Refresh failed', originalError, { | ||
| userId: 'user_123', | ||
| sessionId: 'session_456', | ||
| }); | ||
| expect(error.cause).toBe(originalError); | ||
| expect(error.userId).toBe('user_123'); | ||
| expect(error.sessionId).toBe('session_456'); | ||
| }); | ||
| it('creates error with partial context (userId only)', () => { | ||
| const error = new TokenRefreshError('Refresh failed', undefined, { | ||
| userId: 'user_123', | ||
| }); | ||
| expect(error.userId).toBe('user_123'); | ||
| expect(error.sessionId).toBeUndefined(); | ||
| }); | ||
| it('creates error with partial context (sessionId only)', () => { | ||
| const error = new TokenRefreshError('Refresh failed', undefined, { | ||
| sessionId: 'session_456', | ||
| }); | ||
| expect(error.userId).toBeUndefined(); | ||
| expect(error.sessionId).toBe('session_456'); | ||
| }); | ||
| it('has undefined properties when no context provided', () => { | ||
| it('leaves context fields undefined when omitted', () => { | ||
| const error = new TokenRefreshError('Refresh failed'); | ||
@@ -147,34 +46,26 @@ | ||
| describe('error inheritance', () => { | ||
| it('maintains proper inheritance chain', () => { | ||
| const sessionError = new SessionEncryptionError('test'); | ||
| const tokenError = new TokenValidationError('test'); | ||
| const refreshError = new TokenRefreshError('test'); | ||
| describe('error subclasses', () => { | ||
| const cases: Array<[string, new (msg: string) => AuthKitError]> = [ | ||
| ['SessionEncryptionError', SessionEncryptionError], | ||
| ['TokenValidationError', TokenValidationError], | ||
| ['TokenRefreshError', TokenRefreshError], | ||
| ['OAuthStateMismatchError', OAuthStateMismatchError], | ||
| ['PKCECookieMissingError', PKCECookieMissingError], | ||
| ['PKCEPayloadTooLargeError', PKCEPayloadTooLargeError], | ||
| ]; | ||
| expect(sessionError).toBeInstanceOf(SessionEncryptionError); | ||
| expect(sessionError).toBeInstanceOf(AuthKitError); | ||
| expect(sessionError).toBeInstanceOf(Error); | ||
| it.each(cases)( | ||
| '%s preserves name, message, and AuthKitError inheritance', | ||
| (name, Ctor) => { | ||
| const error = new Ctor('msg'); | ||
| expect(tokenError).toBeInstanceOf(TokenValidationError); | ||
| expect(tokenError).toBeInstanceOf(AuthKitError); | ||
| expect(tokenError).toBeInstanceOf(Error); | ||
| expect(refreshError).toBeInstanceOf(TokenRefreshError); | ||
| expect(refreshError).toBeInstanceOf(AuthKitError); | ||
| expect(refreshError).toBeInstanceOf(Error); | ||
| }); | ||
| it('can be caught as AuthKitError', () => { | ||
| const errors = [ | ||
| new SessionEncryptionError('test'), | ||
| new TokenValidationError('test'), | ||
| new TokenRefreshError('test'), | ||
| ]; | ||
| errors.forEach(error => { | ||
| expect(error.name).toBe(name); | ||
| expect(error.message).toBe('msg'); | ||
| expect(error).toBeInstanceOf(Ctor); | ||
| expect(error).toBeInstanceOf(AuthKitError); | ||
| expect(() => { | ||
| throw error; | ||
| }).toThrow(AuthKitError); | ||
| }); | ||
| }); | ||
| }, | ||
| ); | ||
| }); |
+21
-0
@@ -26,2 +26,23 @@ export class AuthKitError extends Error { | ||
| export class OAuthStateMismatchError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown) { | ||
| super(message, cause); | ||
| this.name = 'OAuthStateMismatchError'; | ||
| } | ||
| } | ||
| export class PKCECookieMissingError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown) { | ||
| super(message, cause); | ||
| this.name = 'PKCECookieMissingError'; | ||
| } | ||
| } | ||
| export class PKCEPayloadTooLargeError extends AuthKitError { | ||
| constructor(message: string, cause?: unknown) { | ||
| super(message, cause); | ||
| this.name = 'PKCEPayloadTooLargeError'; | ||
| } | ||
| } | ||
| export class TokenRefreshError extends AuthKitError { | ||
@@ -28,0 +49,0 @@ readonly userId?: string; |
@@ -22,3 +22,3 @@ import { CookieSessionStorage } from './CookieSessionStorage.js'; | ||
| class TestCookieSessionStorage extends CookieSessionStorage<string, string> { | ||
| async getSession(): Promise<string | null> { | ||
| async getCookie(_request: string, _name: string): Promise<string | null> { | ||
| return null; | ||
@@ -114,3 +114,3 @@ } | ||
| const session = await storage.getSession(); | ||
| const session = await storage.getSession('request'); | ||
| expect(session).toBeNull(); | ||
@@ -128,3 +128,3 @@ | ||
| describe('buildSetCookie', () => { | ||
| describe('serializeCookie', () => { | ||
| it('capitalizes SameSite values for Safari compatibility', async () => { | ||
@@ -145,2 +145,78 @@ const testCases = [ | ||
| }); | ||
| describe('generic cookie primitives', () => { | ||
| it('setCookie emits a Set-Cookie with the passed name, value, and options', async () => { | ||
| const storage = new TestCookieSessionStorage(createMockConfig()); | ||
| const result = await storage.setCookie(undefined, 'foo', 'bar', { | ||
| path: '/x', | ||
| maxAge: 60, | ||
| httpOnly: true, | ||
| secure: true, | ||
| sameSite: 'lax', | ||
| }); | ||
| const header = result.headers?.['Set-Cookie']; | ||
| expect(header).toContain('foo=bar'); | ||
| expect(header).toContain('Path=/x'); | ||
| expect(header).toContain('Max-Age=60'); | ||
| expect(header).toContain('HttpOnly'); | ||
| expect(header).toContain('Secure'); | ||
| expect(header).toContain('SameSite=Lax'); | ||
| }); | ||
| it('clearCookie emits Max-Age=0 with empty value', async () => { | ||
| const storage = new TestCookieSessionStorage(createMockConfig()); | ||
| const result = await storage.clearCookie(undefined, 'foo', { | ||
| path: '/x', | ||
| }); | ||
| const header = result.headers?.['Set-Cookie'] as string; | ||
| expect(header).toContain('foo=;'); | ||
| expect(header).toContain('Max-Age=0'); | ||
| }); | ||
| it('getCookie is abstract — subclass must implement it', async () => { | ||
| const storage = new TestCookieSessionStorage(createMockConfig()); | ||
| const value = await storage.getCookie('request', 'anything'); | ||
| expect(value).toBeNull(); | ||
| }); | ||
| it('getSession wraps getCookie with the configured cookie name', async () => { | ||
| class CapturingStorage extends CookieSessionStorage<string, string> { | ||
| captured?: { name: string }; | ||
| async getCookie(_req: string, name: string): Promise<string | null> { | ||
| this.captured = { name }; | ||
| return 'stored-value'; | ||
| } | ||
| } | ||
| const storage = new CapturingStorage( | ||
| createMockConfig({ cookieName: 'my-session' }), | ||
| ); | ||
| const value = await storage.getSession('request'); | ||
| expect(storage.captured).toEqual({ name: 'my-session' }); | ||
| expect(value).toBe('stored-value'); | ||
| }); | ||
| it('saveSession and setCookie produce identical headers for the session cookie', async () => { | ||
| const config = createMockConfig(); | ||
| const storage = new TestCookieSessionStorage(config); | ||
| const saved = await storage.saveSession(undefined, 'data'); | ||
| const explicit = await storage.setCookie( | ||
| undefined, | ||
| storage['cookieName'], | ||
| 'data', | ||
| storage['cookieOptions'], | ||
| ); | ||
| expect(saved.headers?.['Set-Cookie']).toBe( | ||
| explicit.headers?.['Set-Cookie'], | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
| import type { AuthKitConfig } from '../config/types.js'; | ||
| import type { HeadersBag, SessionStorage } from './types.js'; | ||
| import { serializeCookie } from './serializeCookie.js'; | ||
| import type { CookieOptions, HeadersBag, SessionStorage } from './types.js'; | ||
| export interface CookieOptions { | ||
| path?: string; | ||
| domain?: string; | ||
| maxAge?: number; | ||
| expires?: Date; | ||
| httpOnly?: boolean; | ||
| secure?: boolean; | ||
| sameSite?: 'lax' | 'strict' | 'none'; | ||
| priority?: 'low' | 'medium' | 'high'; | ||
| partitioned?: boolean; | ||
| } | ||
| export type { CookieOptions } from './types.js'; | ||
@@ -24,3 +15,6 @@ export abstract class CookieSessionStorage< | ||
| constructor(config: AuthKitConfig) { | ||
| this.cookieName = config.cookieName ?? 'wos_session'; | ||
| // Matches the canonical default in ConfigurationProvider. This fallback | ||
| // only fires when a caller instantiates the class with a config that | ||
| // hasn't been resolved through the provider. | ||
| this.cookieName = config.cookieName ?? 'wos-session'; | ||
@@ -58,27 +52,29 @@ const sameSite = config.cookieSameSite ?? 'lax'; | ||
| protected buildSetCookie(value: string, expired?: boolean): string { | ||
| const a = [`${this.cookieName}=${encodeURIComponent(value)}`]; | ||
| const o = this.cookieOptions; | ||
| if (o.path) a.push(`Path=${o.path}`); | ||
| if (o.domain) a.push(`Domain=${o.domain}`); | ||
| if (o.maxAge || expired) a.push(`Max-Age=${expired ? 0 : o.maxAge}`); | ||
| if (o.httpOnly) a.push('HttpOnly'); | ||
| if (o.secure) a.push('Secure'); | ||
| if (o.sameSite) { | ||
| const capitalizedSameSite = | ||
| o.sameSite.charAt(0).toUpperCase() + o.sameSite.slice(1).toLowerCase(); | ||
| a.push(`SameSite=${capitalizedSameSite}`); | ||
| } | ||
| if (o.priority) a.push(`Priority=${o.priority}`); | ||
| if (o.partitioned) a.push('Partitioned'); | ||
| return a.join('; '); | ||
| protected serializeCookie( | ||
| name: string, | ||
| value: string, | ||
| options: CookieOptions, | ||
| flags: { expired?: boolean } = {}, | ||
| ): string { | ||
| return serializeCookie(name, value, options, flags); | ||
| } | ||
| abstract getSession(request: TRequest): Promise<string | null>; | ||
| /** | ||
| * Read a named cookie from the framework-specific request. | ||
| * | ||
| * Implementations MUST return the URL-decoded value — the inverse of | ||
| * `serializeCookie`'s `encodeURIComponent` on write. Returning the raw | ||
| * on-wire bytes will silently break byte-comparison against the original | ||
| * value (e.g. PKCE state verification) for any seal containing characters | ||
| * that `encodeURIComponent` escapes. | ||
| */ | ||
| abstract getCookie(request: TRequest, name: string): Promise<string | null>; | ||
| async saveSession( | ||
| async setCookie( | ||
| response: TResponse | undefined, | ||
| sessionData: string, | ||
| name: string, | ||
| value: string, | ||
| options: CookieOptions, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }> { | ||
| const header = this.buildSetCookie(sessionData); | ||
| const header = this.serializeCookie(name, value, options); | ||
| const mutated = await this.applyHeaders(response, { 'Set-Cookie': header }); | ||
@@ -88,9 +84,33 @@ return mutated ?? { headers: { 'Set-Cookie': header } }; | ||
| async clearSession( | ||
| response: TResponse, | ||
| async clearCookie( | ||
| response: TResponse | undefined, | ||
| name: string, | ||
| options: CookieOptions, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }> { | ||
| const header = this.buildSetCookie('', true); | ||
| const header = this.serializeCookie(name, '', options, { expired: true }); | ||
| const mutated = await this.applyHeaders(response, { 'Set-Cookie': header }); | ||
| return mutated ?? { headers: { 'Set-Cookie': header } }; | ||
| } | ||
| getSession(request: TRequest): Promise<string | null> { | ||
| return this.getCookie(request, this.cookieName); | ||
| } | ||
| saveSession( | ||
| response: TResponse | undefined, | ||
| sessionData: string, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }> { | ||
| return this.setCookie( | ||
| response, | ||
| this.cookieName, | ||
| sessionData, | ||
| this.cookieOptions, | ||
| ); | ||
| } | ||
| clearSession( | ||
| response: TResponse | undefined, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }> { | ||
| return this.clearCookie(response, this.cookieName, this.cookieOptions); | ||
| } | ||
| } |
@@ -35,4 +35,4 @@ import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose'; | ||
| return decodeJwt<BaseTokenClaims & TCustomClaims>(token); | ||
| } catch (error) { | ||
| throw new Error('Invalid token'); | ||
| } catch (cause) { | ||
| throw new Error('Invalid token', { cause }); | ||
| } | ||
@@ -39,0 +39,0 @@ } |
@@ -75,5 +75,53 @@ import type { Impersonator, User } from '@workos-inc/node'; | ||
| /** | ||
| * Map of HTTP response headers. | ||
| * | ||
| * `Set-Cookie` MUST be represented as `string[]` when multiple values exist | ||
| * — adapters must append each entry as its own header, never comma-join. | ||
| * A comma-joined `Set-Cookie` string is not a valid single HTTP header. | ||
| */ | ||
| export type HeadersBag = Record<string, string | string[]>; | ||
| export interface SessionStorage<TRequest, TResponse, TOptions = unknown> { | ||
| /** | ||
| * Read a named cookie from a request. | ||
| * | ||
| * MUST return the URL-decoded value (the inverse of `setCookie`'s | ||
| * `encodeURIComponent` on write). Returning raw on-wire bytes will break | ||
| * byte-comparison against the original value — notably PKCE state | ||
| * verification — for any seal format that contains characters | ||
| * `encodeURIComponent` escapes (`+`, `=`, etc.). | ||
| * | ||
| * @param request Framework-specific request object. | ||
| * @param name Cookie name. | ||
| * @returns The URL-decoded cookie value or null if absent. | ||
| */ | ||
| getCookie(request: TRequest, name: string): Promise<string | null>; | ||
| /** | ||
| * Write a named cookie to a response. | ||
| * @param response Framework-specific response object (or undefined to emit headers only). | ||
| * @param name Cookie name. | ||
| * @param value Cookie value (will be URL-encoded). | ||
| * @param options Per-call cookie options. | ||
| */ | ||
| setCookie( | ||
| response: TResponse | undefined, | ||
| name: string, | ||
| value: string, | ||
| options: CookieOptions, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }>; | ||
| /** | ||
| * Clear a named cookie by emitting a Set-Cookie with Max-Age=0. | ||
| * @param response Framework-specific response object (or undefined to emit headers only). | ||
| * @param name Cookie name. | ||
| * @param options Cookie options (must match those used at set time, especially `path`). | ||
| */ | ||
| clearCookie( | ||
| response: TResponse | undefined, | ||
| name: string, | ||
| options: CookieOptions, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }>; | ||
| /* | ||
@@ -121,2 +169,3 @@ * Extract session data from a request object | ||
| password: string; | ||
| ttl?: number | undefined; | ||
| }, | ||
@@ -126,2 +175,17 @@ ) => Promise<T>; | ||
| /** | ||
| * Result shape returned by `createAuthorization` / `createSignIn` / `createSignUp`. | ||
| * | ||
| * The verifier cookie is written internally via `SessionStorage.setCookie` — callers | ||
| * only need to redirect the browser to `url` and apply any returned `headers`/`response`. | ||
| */ | ||
| export interface GetAuthorizationUrlResult { | ||
| url: string; | ||
| } | ||
| export type CreateAuthorizationResult<TResponse> = GetAuthorizationUrlResult & { | ||
| response?: TResponse; | ||
| headers?: HeadersBag; | ||
| }; | ||
| export interface CookieOptions { | ||
@@ -148,3 +212,15 @@ path?: string; | ||
| prompt?: 'login' | 'none' | 'consent' | 'select_account'; | ||
| /** Custom state to pass through the OAuth flow. Returned in handleCallback. */ | ||
| /** | ||
| * Custom state to pass through the OAuth flow. Returned in handleCallback. | ||
| * | ||
| * Bounded at 2048 UTF-8 bytes. The value is sealed alongside the PKCE | ||
| * verifier and stored in the `wos-auth-verifier` cookie; oversized values | ||
| * would push the cookie past the per-cookie browser limit (~4 KB) and | ||
| * would be silently dropped. Values over the supported limit throw | ||
| * `PKCEPayloadTooLargeError` at sign-in time. | ||
| * | ||
| * Note: total sealed payload size — not just `state` — drives the hard | ||
| * failure. Combining a large `returnPathname`, `redirectUri`, or | ||
| * `cookieDomain` with near-limit `state` can still overflow. | ||
| */ | ||
| state?: string; | ||
@@ -154,3 +230,4 @@ } | ||
| /** | ||
| * Options for getAuthorizationUrl, including screenHint | ||
| * Options for `createAuthorization` / `createSignIn` / `createSignUp`, | ||
| * including the `screenHint` selector used by the sign-in/sign-up variants. | ||
| */ | ||
@@ -157,0 +234,0 @@ export interface GetAuthorizationUrlOptions extends AuthUrlOptions { |
+3
-0
@@ -66,2 +66,5 @@ /** | ||
| AuthKitError, | ||
| OAuthStateMismatchError, | ||
| PKCECookieMissingError, | ||
| PKCEPayloadTooLargeError, | ||
| SessionEncryptionError, | ||
@@ -68,0 +71,0 @@ TokenValidationError, |
@@ -0,1 +1,3 @@ | ||
| import sessionEncryption from '../core/encryption/ironWebcryptoEncryption.js'; | ||
| import { unsealState } from '../core/pkce/state.js'; | ||
| import { AuthOperations } from './AuthOperations.js'; | ||
@@ -7,3 +9,4 @@ | ||
| cookieName: 'wos-session', | ||
| }; | ||
| cookiePassword: 'this-is-a-test-password-that-is-32-characters-long!', | ||
| } as const; | ||
@@ -46,19 +49,41 @@ const mockUser = { | ||
| const mockClient = { | ||
| userManagement: { | ||
| getLogoutUrl: ({ sessionId, returnTo }: any) => | ||
| `https://api.workos.com/sso/logout?session_id=${sessionId}&return_to=${returnTo || ''}`, | ||
| getAuthorizationUrl: ({ clientId, redirectUri, screenHint, state }: any) => | ||
| `https://api.workos.com/sso/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&screen_hint=${screenHint || ''}&state=${state || ''}`, | ||
| }, | ||
| }; | ||
| function makeAuthUrlClient(capture?: { last?: Record<string, unknown> }) { | ||
| return { | ||
| pkce: { | ||
| generate: async () => ({ | ||
| codeVerifier: 'generated-verifier-1234567890abcdef', | ||
| codeChallenge: 'generated-challenge', | ||
| codeChallengeMethod: 'S256', | ||
| }), | ||
| }, | ||
| userManagement: { | ||
| getLogoutUrl: ({ sessionId, returnTo }: any) => | ||
| `https://api.workos.com/sso/logout?session_id=${sessionId}&return_to=${returnTo || ''}`, | ||
| getAuthorizationUrl: (opts: any) => { | ||
| if (capture) capture.last = opts; | ||
| const params = new URLSearchParams({ | ||
| client_id: opts.clientId, | ||
| redirect_uri: opts.redirectUri, | ||
| state: opts.state ?? '', | ||
| screen_hint: opts.screenHint ?? '', | ||
| code_challenge: opts.codeChallenge ?? '', | ||
| code_challenge_method: opts.codeChallengeMethod ?? '', | ||
| }); | ||
| return `https://api.workos.com/sso/authorize?${params.toString()}`; | ||
| }, | ||
| }, | ||
| }; | ||
| } | ||
| describe('AuthOperations', () => { | ||
| let operations: AuthOperations; | ||
| let capture: { last?: Record<string, unknown> }; | ||
| beforeEach(() => { | ||
| capture = {}; | ||
| operations = new AuthOperations( | ||
| mockCore as any, | ||
| mockClient as any, | ||
| makeAuthUrlClient(capture) as any, | ||
| mockConfig as any, | ||
| sessionEncryption, | ||
| ); | ||
@@ -153,4 +178,5 @@ }); | ||
| coreWithSpy as any, | ||
| mockClient as any, | ||
| makeAuthUrlClient() as any, | ||
| mockConfig as any, | ||
| sessionEncryption, | ||
| ); | ||
@@ -199,4 +225,5 @@ | ||
| coreWithSpy as any, | ||
| mockClient as any, | ||
| makeAuthUrlClient() as any, | ||
| mockConfig as any, | ||
| sessionEncryption, | ||
| ); | ||
@@ -220,85 +247,103 @@ | ||
| describe('getAuthorizationUrl()', () => { | ||
| it('returns WorkOS authorization URL', async () => { | ||
| const result = await operations.getAuthorizationUrl(); | ||
| describe('createAuthorization()', () => { | ||
| it('returns url + sealedState + cookieOptions for storage dispatch', async () => { | ||
| const result = await operations.createAuthorization(); | ||
| expect(result).toContain('client_id=test-client-id'); | ||
| expect(result).toContain('redirect_uri'); | ||
| expect(typeof result.url).toBe('string'); | ||
| expect(typeof result.sealedState).toBe('string'); | ||
| expect(result.sealedState.length).toBeGreaterThan(0); | ||
| expect(result.cookieOptions.maxAge).toBe(600); | ||
| expect(result.cookieOptions.path).toBe('/'); | ||
| expect(result.url).toContain('client_id=test-client-id'); | ||
| }); | ||
| it('encodes returnPathname in URL-safe base64 state', async () => { | ||
| const result = await operations.getAuthorizationUrl({ | ||
| it('passes the sealedState as the URL state param (identical string)', async () => { | ||
| const result = await operations.createAuthorization(); | ||
| const urlState = decodeURIComponent( | ||
| new URL(result.url).searchParams.get('state') ?? '', | ||
| ); | ||
| expect(urlState).toBe(result.sealedState); | ||
| }); | ||
| it('includes codeChallenge + codeChallengeMethod in WorkOS URL', async () => { | ||
| await operations.createAuthorization(); | ||
| expect(capture.last?.codeChallenge).toBe('generated-challenge'); | ||
| expect(capture.last?.codeChallengeMethod).toBe('S256'); | ||
| }); | ||
| it('seals returnPathname into the state blob', async () => { | ||
| const result = await operations.createAuthorization({ | ||
| returnPathname: '/dashboard', | ||
| }); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| mockConfig.cookiePassword, | ||
| result.sealedState, | ||
| ); | ||
| expect(result).toContain('state='); | ||
| const stateMatch = result.match(/state=([^&]+)/); | ||
| expect(stateMatch).toBeTruthy(); | ||
| // Decode URL-safe base64: reverse - to +, _ to / | ||
| const urlSafeState = stateMatch![1]!; | ||
| const standardBase64 = urlSafeState.replace(/-/g, '+').replace(/_/g, '/'); | ||
| const decoded = JSON.parse(atob(standardBase64)); | ||
| expect(decoded.returnPathname).toBe('/dashboard'); | ||
| expect(unsealed.returnPathname).toBe('/dashboard'); | ||
| expect(unsealed.codeVerifier).toBe('generated-verifier-1234567890abcdef'); | ||
| }); | ||
| it('combines internal state with custom user state', async () => { | ||
| const result = await operations.getAuthorizationUrl({ | ||
| returnPathname: '/profile', | ||
| it('seals customState into the state blob', async () => { | ||
| const result = await operations.createAuthorization({ | ||
| state: 'my-custom-state', | ||
| }); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| mockConfig.cookiePassword, | ||
| result.sealedState, | ||
| ); | ||
| expect(result).toContain('state='); | ||
| const stateMatch = result.match(/state=([^&]+)/); | ||
| expect(stateMatch).toBeTruthy(); | ||
| // State should be in format: internal.userState | ||
| const fullState = stateMatch![1]!; | ||
| expect(fullState).toContain('.'); | ||
| const [internal, userState] = fullState.split('.'); | ||
| expect(userState).toBe('my-custom-state'); | ||
| // Decode internal part | ||
| const standardBase64 = internal!.replace(/-/g, '+').replace(/_/g, '/'); | ||
| const decoded = JSON.parse(atob(standardBase64)); | ||
| expect(decoded.returnPathname).toBe('/profile'); | ||
| expect(unsealed.customState).toBe('my-custom-state'); | ||
| }); | ||
| it('passes custom state as-is when no returnPathname', async () => { | ||
| const result = await operations.getAuthorizationUrl({ | ||
| state: 'only-user-state', | ||
| it('seals both returnPathname and customState together', async () => { | ||
| const result = await operations.createAuthorization({ | ||
| returnPathname: '/profile', | ||
| state: 'custom', | ||
| }); | ||
| const unsealed = await unsealState( | ||
| sessionEncryption, | ||
| mockConfig.cookiePassword, | ||
| result.sealedState, | ||
| ); | ||
| expect(result).toContain('state='); | ||
| const stateMatch = result.match(/state=([^&]+)/); | ||
| expect(stateMatch).toBeTruthy(); | ||
| // State should be passed through directly without internal wrapper | ||
| expect(stateMatch![1]).toBe('only-user-state'); | ||
| expect(unsealed.returnPathname).toBe('/profile'); | ||
| expect(unsealed.customState).toBe('custom'); | ||
| }); | ||
| it('includes screenHint when provided', async () => { | ||
| const result = await operations.getAuthorizationUrl({ | ||
| const result = await operations.createAuthorization({ | ||
| screenHint: 'sign-up', | ||
| }); | ||
| expect(result).toContain('screen_hint=sign-up'); | ||
| expect(result.url).toContain('screen_hint=sign-up'); | ||
| }); | ||
| it('generates a unique nonce per call (concurrent last-flow-wins)', async () => { | ||
| const a = await operations.createAuthorization(); | ||
| const b = await operations.createAuthorization(); | ||
| expect(a.sealedState).not.toBe(b.sealedState); | ||
| }); | ||
| }); | ||
| describe('getSignInUrl()', () => { | ||
| describe('createSignIn()', () => { | ||
| it('returns authorization URL with sign-in hint', async () => { | ||
| const result = await operations.getSignInUrl(); | ||
| const result = await operations.createSignIn(); | ||
| expect(result).toContain('screen_hint=sign-in'); | ||
| expect(result.url).toContain('screen_hint=sign-in'); | ||
| }); | ||
| }); | ||
| describe('getSignUpUrl()', () => { | ||
| describe('createSignUp()', () => { | ||
| it('returns authorization URL with sign-up hint', async () => { | ||
| const result = await operations.getSignUpUrl(); | ||
| const result = await operations.createSignUp(); | ||
| expect(result).toContain('screen_hint=sign-up'); | ||
| expect(result.url).toContain('screen_hint=sign-up'); | ||
| }); | ||
| }); | ||
| }); |
| import type { WorkOS } from '@workos-inc/node'; | ||
| import type { AuthKitCore } from '../core/AuthKitCore.js'; | ||
| import type { AuthKitConfig } from '../core/config/types.js'; | ||
| import { | ||
| type GeneratedAuthorizationUrl, | ||
| generateAuthorizationUrl, | ||
| } from '../core/pkce/generateAuthorizationUrl.js'; | ||
| import type { | ||
@@ -8,2 +12,3 @@ AuthResult, | ||
| Session, | ||
| SessionEncryption, | ||
| } from '../core/session/types.js'; | ||
@@ -27,7 +32,14 @@ | ||
| private config: AuthKitConfig; | ||
| private encryption: SessionEncryption; | ||
| constructor(core: AuthKitCore, client: WorkOS, config: AuthKitConfig) { | ||
| constructor( | ||
| core: AuthKitCore, | ||
| client: WorkOS, | ||
| config: AuthKitConfig, | ||
| encryption: SessionEncryption, | ||
| ) { | ||
| this.core = core; | ||
| this.client = client; | ||
| this.config = config; | ||
| this.encryption = encryption; | ||
| } | ||
@@ -120,37 +132,19 @@ | ||
| /** | ||
| * Get authorization URL for WorkOS authentication. | ||
| * Create a PKCE-bound WorkOS authorization URL. | ||
| * | ||
| * State encoding format: `{internal}.{userState}` where internal is URL-safe | ||
| * base64 encoded JSON containing returnPathname. This allows customers to | ||
| * pass their own state through the OAuth flow. | ||
| * Returns the URL, the sealed state blob (to be used as both the cookie | ||
| * value and — already present in the URL — the OAuth `state` param), and | ||
| * the cookie options the verifier cookie should be written with. | ||
| * AuthService dispatches the write via `SessionStorage.setCookie`. | ||
| * | ||
| * @param options - Authorization URL options (returnPathname, screenHint, state, etc.) | ||
| * @returns The authorization URL | ||
| * @param options - returnPathname, screenHint, custom state, redirectUri, etc. | ||
| */ | ||
| async getAuthorizationUrl( | ||
| async createAuthorization( | ||
| options: GetAuthorizationUrlOptions = {}, | ||
| ): Promise<string> { | ||
| // Build the combined state parameter (matches authkit-nextjs format) | ||
| const internalState = options.returnPathname | ||
| ? btoa(JSON.stringify({ returnPathname: options.returnPathname })) | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| : null; | ||
| // If both internal and custom state, combine as internal.custom | ||
| // Otherwise use whichever is provided | ||
| const state = | ||
| internalState && options.state | ||
| ? `${internalState}.${options.state}` | ||
| : internalState || options.state || undefined; | ||
| return this.client.userManagement.getAuthorizationUrl({ | ||
| provider: 'authkit', | ||
| redirectUri: options.redirectUri ?? this.config.redirectUri, | ||
| screenHint: options.screenHint, | ||
| organizationId: options.organizationId, | ||
| loginHint: options.loginHint, | ||
| prompt: options.prompt, | ||
| clientId: this.config.clientId, | ||
| state, | ||
| ): Promise<GeneratedAuthorizationUrl> { | ||
| return generateAuthorizationUrl({ | ||
| client: this.client, | ||
| config: this.config, | ||
| encryption: this.encryption, | ||
| options, | ||
| }); | ||
@@ -160,8 +154,8 @@ } | ||
| /** | ||
| * Convenience method: Get sign-in URL. | ||
| * Convenience method: Create sign-in authorization URL. | ||
| */ | ||
| async getSignInUrl( | ||
| async createSignIn( | ||
| options: Omit<GetAuthorizationUrlOptions, 'screenHint'> = {}, | ||
| ): Promise<string> { | ||
| return this.getAuthorizationUrl({ | ||
| ): Promise<GeneratedAuthorizationUrl> { | ||
| return this.createAuthorization({ | ||
| ...options, | ||
@@ -173,8 +167,8 @@ screenHint: 'sign-in', | ||
| /** | ||
| * Convenience method: Get sign-up URL. | ||
| * Convenience method: Create sign-up authorization URL. | ||
| */ | ||
| async getSignUpUrl( | ||
| async createSignUp( | ||
| options: Omit<GetAuthorizationUrlOptions, 'screenHint'> = {}, | ||
| ): Promise<string> { | ||
| return this.getAuthorizationUrl({ | ||
| ): Promise<GeneratedAuthorizationUrl> { | ||
| return this.createAuthorization({ | ||
| ...options, | ||
@@ -181,0 +175,0 @@ screenHint: 'sign-up', |
@@ -0,1 +1,6 @@ | ||
| import sessionEncryption from '../core/encryption/ironWebcryptoEncryption.js'; | ||
| import { | ||
| OAuthStateMismatchError, | ||
| PKCECookieMissingError, | ||
| } from '../core/errors.js'; | ||
| import { AuthService } from './AuthService.js'; | ||
@@ -6,4 +11,4 @@ | ||
| apiKey: 'test-api-key', | ||
| redirectUri: 'http://localhost:3000/callback', | ||
| cookiePassword: 'test-password-that-is-32-chars-long!!', | ||
| redirectUri: 'https://app.example.com/callback', | ||
| cookiePassword: 'this-is-a-test-password-that-is-32-characters-long!', | ||
| cookieName: 'wos-session', | ||
@@ -27,28 +32,83 @@ }; | ||
| const mockStorage = { | ||
| getSession: async () => 'encrypted-session-data', | ||
| saveSession: async () => ({ response: 'updated-response' }), | ||
| clearSession: async () => ({ | ||
| response: 'cleared-response', | ||
| headers: { 'Set-Cookie': 'wos-session=; Path=/; Max-Age=0' }, | ||
| }), | ||
| }; | ||
| const mockClient = { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| getAuthorizationUrl: ({ screenHint }: any) => | ||
| `https://api.workos.com/sso/authorize?screen_hint=${screenHint || ''}`, | ||
| authenticateWithCode: async ({ code }: any) => ({ | ||
| accessToken: `access-${code}`, | ||
| refreshToken: `refresh-${code}`, | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| /** | ||
| * Storage mock that remembers the last value passed to setCookie so | ||
| * handleCallback's getCookie read sees what sign-in wrote. Exposes the most | ||
| * recent options per cookie name for path/scope assertions. | ||
| */ | ||
| function makeStorage(initialSession: string | null = 'encrypted-session-data') { | ||
| const cookies = new Map<string, string>(); | ||
| const lastSetOptions = new Map<string, any>(); | ||
| const lastClearOptions = new Map<string, any>(); | ||
| return { | ||
| cookies, | ||
| lastSetOptions, | ||
| lastClearOptions, | ||
| getSession: async () => initialSession, | ||
| getCookie: async (_req: any, name: string) => cookies.get(name) ?? null, | ||
| setCookie: async (_res: any, name: string, value: string, options: any) => { | ||
| cookies.set(name, value); | ||
| lastSetOptions.set(name, options); | ||
| return { | ||
| headers: { | ||
| 'Set-Cookie': `${name}=${value}; Path=${options.path}; Max-Age=${options.maxAge}`, | ||
| }, | ||
| }; | ||
| }, | ||
| clearCookie: async (_res: any, name: string, options: any) => { | ||
| lastClearOptions.set(name, options); | ||
| cookies.delete(name); | ||
| return { | ||
| headers: { | ||
| 'Set-Cookie': `${name}=; Path=${options.path}; Max-Age=0`, | ||
| }, | ||
| }; | ||
| }, | ||
| saveSession: async () => ({ | ||
| response: 'updated-response', | ||
| headers: { 'Set-Cookie': 'wos-session=encrypted; Path=/; Max-Age=3600' }, | ||
| }), | ||
| getLogoutUrl: ({ sessionId }: any) => | ||
| `https://api.workos.com/sso/logout?session_id=${sessionId}`, | ||
| }, | ||
| }; | ||
| clearSession: async () => ({ | ||
| response: 'cleared-response', | ||
| headers: { 'Set-Cookie': 'wos-session=; Path=/; Max-Age=0' }, | ||
| }), | ||
| }; | ||
| } | ||
| const mockEncryption = { | ||
| const testVerifier = 'test-verifier-abcdefghijklmnopqrstuvwxyz1234567890'; | ||
| function makeClient(capture?: { authCall?: Record<string, unknown> }) { | ||
| return { | ||
| userManagement: { | ||
| getJwksUrl: () => 'https://api.workos.com/sso/jwks/test-client-id', | ||
| getAuthorizationUrl: (opts: any) => { | ||
| const params = new URLSearchParams({ | ||
| state: opts.state ?? '', | ||
| screen_hint: opts.screenHint ?? '', | ||
| }); | ||
| return `https://api.workos.com/sso/authorize?${params.toString()}`; | ||
| }, | ||
| authenticateWithCode: async (opts: any) => { | ||
| if (capture) capture.authCall = opts; | ||
| return { | ||
| accessToken: `access-${opts.code}`, | ||
| refreshToken: `refresh-${opts.code}`, | ||
| user: mockUser, | ||
| impersonator: undefined, | ||
| }; | ||
| }, | ||
| getLogoutUrl: ({ sessionId }: any) => | ||
| `https://api.workos.com/sso/logout?session_id=${sessionId}`, | ||
| }, | ||
| pkce: { | ||
| generate: async () => ({ | ||
| codeVerifier: testVerifier, | ||
| codeChallenge: 'test-challenge', | ||
| codeChallengeMethod: 'S256', | ||
| }), | ||
| }, | ||
| }; | ||
| } | ||
| // For withAuth / getSession tests — returns a decrypted Session-shaped blob. | ||
| const mockEncryptionSessionShape = { | ||
| sealData: async () => 'encrypted-session-data', | ||
@@ -65,9 +125,11 @@ unsealData: async () => ({ | ||
| let service: AuthService<any, any>; | ||
| let storage: ReturnType<typeof makeStorage>; | ||
| beforeEach(() => { | ||
| storage = makeStorage(); | ||
| service = new AuthService( | ||
| mockConfig as any, | ||
| mockStorage as any, | ||
| mockClient as any, | ||
| mockEncryption as any, | ||
| storage as any, | ||
| makeClient() as any, | ||
| mockEncryptionSessionShape as any, | ||
| ); | ||
@@ -84,11 +146,8 @@ }); | ||
| it('returns null user when no session exists', async () => { | ||
| const emptyStorage = { | ||
| ...mockStorage, | ||
| getSession: async () => null, | ||
| }; | ||
| const emptyStorage = makeStorage(null); | ||
| const testService = new AuthService( | ||
| mockConfig as any, | ||
| emptyStorage as any, | ||
| mockClient as any, | ||
| mockEncryption as any, | ||
| makeClient() as any, | ||
| mockEncryptionSessionShape as any, | ||
| ); | ||
@@ -111,4 +170,4 @@ | ||
| mockConfig as any, | ||
| mockStorage as any, | ||
| mockClient as any, | ||
| storage as any, | ||
| makeClient() as any, | ||
| failingEncryption as any, | ||
@@ -132,11 +191,8 @@ ); | ||
| it('returns null when no session exists', async () => { | ||
| const emptyStorage = { | ||
| ...mockStorage, | ||
| getSession: async () => null, | ||
| }; | ||
| const emptyStorage = makeStorage(null); | ||
| const testService = new AuthService( | ||
| mockConfig as any, | ||
| emptyStorage as any, | ||
| mockClient as any, | ||
| mockEncryption as any, | ||
| makeClient() as any, | ||
| mockEncryptionSessionShape as any, | ||
| ); | ||
@@ -176,26 +232,89 @@ | ||
| describe('getAuthorizationUrl()', () => { | ||
| it('delegates to operations', async () => { | ||
| const result = await service.getAuthorizationUrl(); | ||
| describe('createAuthorization()', () => { | ||
| it('returns url and writes the verifier cookie via storage.setCookie', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| expect(result).toContain('authorize'); | ||
| const result = await realService.createAuthorization('res'); | ||
| expect(result.url).toContain('authorize'); | ||
| expect(realStorage.cookies.get('wos-auth-verifier')).toBeTruthy(); | ||
| expect(result.headers?.['Set-Cookie']).toContain('wos-auth-verifier='); | ||
| expect(realStorage.lastSetOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| }); | ||
| describe('getSignInUrl()', () => { | ||
| it('returns sign-in URL', async () => { | ||
| const result = await service.getSignInUrl(); | ||
| describe('createSignIn()', () => { | ||
| it('returns sign-in URL and writes the verifier cookie', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| expect(result).toContain('screen_hint=sign-in'); | ||
| const result = await realService.createSignIn('res'); | ||
| expect(result.url).toContain('screen_hint=sign-in'); | ||
| expect(realStorage.cookies.get('wos-auth-verifier')).toBeTruthy(); | ||
| }); | ||
| }); | ||
| describe('getSignUpUrl()', () => { | ||
| it('returns sign-up URL', async () => { | ||
| const result = await service.getSignUpUrl(); | ||
| describe('createSignUp()', () => { | ||
| it('returns sign-up URL and writes the verifier cookie', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| expect(result).toContain('screen_hint=sign-up'); | ||
| const result = await realService.createSignUp('res'); | ||
| expect(result.url).toContain('screen_hint=sign-up'); | ||
| expect(realStorage.cookies.get('wos-auth-verifier')).toBeTruthy(); | ||
| }); | ||
| }); | ||
| describe('clearPendingVerifier()', () => { | ||
| it('emits a delete cookie with Path=/', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.clearPendingVerifier('res'); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('accepts undefined response for headers-only adapters', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| const result = await realService.clearPendingVerifier(undefined); | ||
| expect(result.headers?.['Set-Cookie']).toContain('wos-auth-verifier='); | ||
| }); | ||
| }); | ||
| describe('getWorkOS()', () => { | ||
@@ -205,3 +324,4 @@ it('returns WorkOS client', () => { | ||
| expect(result).toBe(mockClient); | ||
| expect(result).toBeDefined(); | ||
| expect(typeof (result as any).userManagement.getJwksUrl).toBe('function'); | ||
| }); | ||
@@ -211,66 +331,463 @@ }); | ||
| describe('handleCallback()', () => { | ||
| it('authenticates and creates session', async () => { | ||
| const result = await service.handleCallback('request', 'response', { | ||
| code: 'auth-code-123', | ||
| it('round-trips through createSignIn → handleCallback', async () => { | ||
| const capture: { authCall?: Record<string, unknown> } = {}; | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient(capture) as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res', { | ||
| returnPathname: '/dashboard', | ||
| state: 'my.custom.state', | ||
| }); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| expect(result.authResponse.accessToken).toBe('access-auth-code-123'); | ||
| expect(result.returnPathname).toBe('/'); | ||
| expect(result.response).toBe('updated-response'); | ||
| const result = await realService.handleCallback('req', 'res', { | ||
| code: 'auth-code-xyz', | ||
| state: sealedState, | ||
| }); | ||
| expect(result.authResponse.accessToken).toBe('access-auth-code-xyz'); | ||
| expect(result.returnPathname).toBe('/dashboard'); | ||
| expect(result.state).toBe('my.custom.state'); | ||
| expect(capture.authCall?.codeVerifier).toBe(testVerifier); | ||
| }); | ||
| it('decodes returnPathname from state', async () => { | ||
| const state = btoa(JSON.stringify({ returnPathname: '/dashboard' })); | ||
| it('returns both session and verifier-delete Set-Cookie as a string[]', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| const result = await service.handleCallback('request', 'response', { | ||
| code: 'auth-code-123', | ||
| state, | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| const result = await realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }); | ||
| expect(result.returnPathname).toBe('/dashboard'); | ||
| const setCookie = result.headers?.['Set-Cookie']; | ||
| expect(Array.isArray(setCookie)).toBe(true); | ||
| expect(setCookie).toHaveLength(2); | ||
| expect( | ||
| (setCookie as string[]).some(c => c.startsWith('wos-session=')), | ||
| ).toBe(true); | ||
| expect( | ||
| (setCookie as string[]).some(c => c.startsWith('wos-auth-verifier=')), | ||
| ).toBe(true); | ||
| expect( | ||
| (setCookie as string[]).find(c => c.startsWith('wos-auth-verifier=')), | ||
| ).toContain('Max-Age=0'); | ||
| }); | ||
| it('treats invalid state as custom state', async () => { | ||
| const result = await service.handleCallback('request', 'response', { | ||
| code: 'auth-code-123', | ||
| state: 'invalid-state', | ||
| it('merges lowercase set-cookie headers into an array (case-insensitive)', async () => { | ||
| // Regression: adapters that normalize through Headers objects emit | ||
| // lowercase `set-cookie`. | ||
| const realStorage = makeStorage(); | ||
| const lowerStorage = { | ||
| ...realStorage, | ||
| setCookie: async ( | ||
| _res: any, | ||
| name: string, | ||
| value: string, | ||
| options: any, | ||
| ) => { | ||
| realStorage.cookies.set(name, value); | ||
| realStorage.lastSetOptions.set(name, options); | ||
| return { | ||
| headers: { | ||
| 'set-cookie': `${name}=${value}; Path=${options.path}; Max-Age=${options.maxAge}`, | ||
| }, | ||
| }; | ||
| }, | ||
| clearCookie: async (_res: any, name: string, options: any) => { | ||
| realStorage.lastClearOptions.set(name, options); | ||
| realStorage.cookies.delete(name); | ||
| return { | ||
| headers: { | ||
| 'set-cookie': `${name}=; Path=${options.path}; Max-Age=0`, | ||
| }, | ||
| }; | ||
| }, | ||
| saveSession: async () => ({ | ||
| response: 'updated-response', | ||
| headers: { | ||
| 'set-cookie': 'wos-session=encrypted; Path=/; Max-Age=3600', | ||
| }, | ||
| }), | ||
| }; | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| lowerStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| const result = await realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }); | ||
| const setCookie = result.headers?.['set-cookie']; | ||
| expect(Array.isArray(setCookie)).toBe(true); | ||
| expect(setCookie).toHaveLength(2); | ||
| expect( | ||
| (setCookie as string[]).some(c => c.startsWith('wos-session=')), | ||
| ).toBe(true); | ||
| expect( | ||
| (setCookie as string[]).some(c => c.startsWith('wos-auth-verifier=')), | ||
| ).toBe(true); | ||
| }); | ||
| it('emits the verifier-delete cookie with Path=/', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| await realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('throws OAuthStateMismatchError when storage cookie differs from url state', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| // Tamper the stored cookie after sign-in | ||
| realStorage.cookies.set( | ||
| 'wos-auth-verifier', | ||
| sealedState.slice(0, -2) + 'XX', | ||
| ); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| }); | ||
| it('throws PKCECookieMissingError when storage has no verifier cookie', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| realStorage.cookies.delete('wos-auth-verifier'); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(PKCECookieMissingError); | ||
| }); | ||
| it('throws OAuthStateMismatchError when state is undefined', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: undefined, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| }); | ||
| it('defaults returnPathname to "/" when the sealed state omits it', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| const result = await realService.handleCallback('req', 'res', { | ||
| code: 'c', | ||
| state: sealedState, | ||
| }); | ||
| expect(result.returnPathname).toBe('/'); | ||
| expect(result.state).toBe('invalid-state'); | ||
| }); | ||
| it('parses new format with internal.userState', async () => { | ||
| // Create URL-safe base64 internal state | ||
| const internal = btoa(JSON.stringify({ returnPathname: '/profile' })) | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| .replace(/=+$/, ''); | ||
| const state = `${internal}.my-custom-state`; | ||
| it('best-effort clears the verifier cookie on OAuthStateMismatchError', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| const result = await service.handleCallback('request', 'response', { | ||
| code: 'auth-code-123', | ||
| state, | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| realStorage.cookies.set( | ||
| 'wos-auth-verifier', | ||
| sealedState.slice(0, -2) + 'XX', | ||
| ); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('emits a scheme-agnostic (secure=false, sameSite=lax) delete on pre-unseal failure', async () => { | ||
| // Covers the end-to-end case that motivates the schemeAgnostic flag: | ||
| // sign-in used an http:// redirectUri override (secure=false), then | ||
| // the callback hits a pre-unseal error (state mismatch) — we don't | ||
| // know the override at that point, so the fallback delete must drop | ||
| // Secure so the browser accepts the Set-Cookie over http://. | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| { ...mockConfig, cookieSameSite: 'lax' } as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res', { | ||
| redirectUri: 'http://localhost:3000/callback', | ||
| }); | ||
| // Confirm the original set was secure=false (the scenario we're | ||
| // proving we can still clean up). | ||
| expect(realStorage.lastSetOptions.get('wos-auth-verifier')?.secure).toBe( | ||
| false, | ||
| ); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| realStorage.cookies.set( | ||
| 'wos-auth-verifier', | ||
| sealedState.slice(0, -2) + 'XX', | ||
| ); | ||
| expect(result.returnPathname).toBe('/profile'); | ||
| expect(result.state).toBe('my-custom-state'); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| const clearOpts = realStorage.lastClearOptions.get('wos-auth-verifier'); | ||
| expect(clearOpts?.secure).toBe(false); | ||
| expect(clearOpts?.sameSite).toBe('lax'); | ||
| expect(clearOpts?.path).toBe('/'); | ||
| }); | ||
| it('handles user state with dots', async () => { | ||
| const internal = btoa(JSON.stringify({ returnPathname: '/dashboard' })) | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| .replace(/=+$/, ''); | ||
| const state = `${internal}.user.state.with.dots`; | ||
| it('keeps Secure on scheme-agnostic delete when sameSite=none (Secure is required)', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| { ...mockConfig, cookieSameSite: 'none' } as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| const result = await service.handleCallback('request', 'response', { | ||
| code: 'auth-code-123', | ||
| state, | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| realStorage.cookies.set( | ||
| 'wos-auth-verifier', | ||
| sealedState.slice(0, -2) + 'XX', | ||
| ); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(OAuthStateMismatchError); | ||
| const clearOpts = realStorage.lastClearOptions.get('wos-auth-verifier'); | ||
| expect(clearOpts?.secure).toBe(true); | ||
| expect(clearOpts?.sameSite).toBe('none'); | ||
| }); | ||
| it('best-effort clears the verifier cookie on PKCECookieMissingError', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| realStorage.cookies.delete('wos-auth-verifier'); | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow(PKCECookieMissingError); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('best-effort clears the verifier cookie when authenticateWithCode throws', async () => { | ||
| const realStorage = makeStorage(); | ||
| const throwingClient = makeClient(); | ||
| throwingClient.userManagement.authenticateWithCode = async () => { | ||
| throw new Error('WorkOS exchange failed'); | ||
| }; | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| throwingClient as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow('WorkOS exchange failed'); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('best-effort clears the verifier cookie when saveSession throws', async () => { | ||
| const realStorage = makeStorage(); | ||
| realStorage.saveSession = async () => { | ||
| throw new Error('storage write failed'); | ||
| }; | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow('storage write failed'); | ||
| expect(realStorage.lastClearOptions.get('wos-auth-verifier')?.path).toBe( | ||
| '/', | ||
| ); | ||
| }); | ||
| it('swallows clearCookie errors so the original failure propagates', async () => { | ||
| const realStorage = makeStorage(); | ||
| const throwingClient = makeClient(); | ||
| throwingClient.userManagement.authenticateWithCode = async () => { | ||
| throw new Error('original exchange failure'); | ||
| }; | ||
| realStorage.clearCookie = async () => { | ||
| throw new Error('clearCookie blew up'); | ||
| }; | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| throwingClient as any, | ||
| sessionEncryption, | ||
| ); | ||
| await realService.createAuthorization('res'); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| await expect( | ||
| realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }), | ||
| ).rejects.toThrow('original exchange failure'); | ||
| }); | ||
| it('round-trips redirectUri override through the sealed state into the clear cookie', async () => { | ||
| const realStorage = makeStorage(); | ||
| const realService = new AuthService( | ||
| mockConfig as any, | ||
| realStorage as any, | ||
| makeClient() as any, | ||
| sessionEncryption, | ||
| ); | ||
| // Override with an http:// URL so the `secure` attribute differs from | ||
| // the https:// default — proves handleCallback used the override. | ||
| await realService.createAuthorization('res', { | ||
| redirectUri: 'http://localhost:3000/callback', | ||
| }); | ||
| const sealedState = realStorage.cookies.get('wos-auth-verifier')!; | ||
| expect(result.returnPathname).toBe('/dashboard'); | ||
| expect(result.state).toBe('user.state.with.dots'); | ||
| expect(realStorage.lastSetOptions.get('wos-auth-verifier')?.secure).toBe( | ||
| false, | ||
| ); | ||
| await realService.handleCallback('req', 'res', { | ||
| code: 'code', | ||
| state: sealedState, | ||
| }); | ||
| expect( | ||
| realStorage.lastClearOptions.get('wos-auth-verifier')?.secure, | ||
| ).toBe(false); | ||
| }); | ||
| }); | ||
| }); |
+198
-73
| import type { WorkOS } from '@workos-inc/node'; | ||
| import { AuthKitCore } from '../core/AuthKitCore.js'; | ||
| import type { AuthKitConfig } from '../core/config/types.js'; | ||
| import { | ||
| getPKCECookieOptions, | ||
| PKCE_COOKIE_NAME, | ||
| } from '../core/pkce/cookieOptions.js'; | ||
| import { AuthOperations } from '../operations/AuthOperations.js'; | ||
| import type { | ||
| AuthResult, | ||
| CreateAuthorizationResult, | ||
| CustomClaims, | ||
@@ -16,2 +21,35 @@ GetAuthorizationUrlOptions, | ||
| /** | ||
| * Merge two `HeadersBag` values. `Set-Cookie` matching is case-insensitive; | ||
| * existing key casing is preserved. Multiple `Set-Cookie` values are | ||
| * concatenated into a `string[]`. Other keys are shallow-merged (second wins). | ||
| */ | ||
| function mergeHeaderBags( | ||
| a: HeadersBag | undefined, | ||
| b: HeadersBag | undefined, | ||
| ): HeadersBag | undefined { | ||
| if (!a) return b; | ||
| if (!b) return a; | ||
| const merged: HeadersBag = { ...a }; | ||
| let setCookieKey = Object.keys(merged).find( | ||
| k => k.toLowerCase() === 'set-cookie', | ||
| ); | ||
| for (const [key, value] of Object.entries(b)) { | ||
| if (key.toLowerCase() !== 'set-cookie') { | ||
| merged[key] = value; | ||
| continue; | ||
| } | ||
| if (!setCookieKey) { | ||
| merged[key] = value; | ||
| setCookieKey = key; | ||
| continue; | ||
| } | ||
| const left = merged[setCookieKey]!; | ||
| const leftArr = Array.isArray(left) ? left : [left]; | ||
| const rightArr = Array.isArray(value) ? value : [value]; | ||
| merged[setCookieKey] = [...leftArr, ...rightArr]; | ||
| } | ||
| return merged; | ||
| } | ||
| /** | ||
| * Framework-agnostic authentication service. | ||
@@ -27,3 +65,3 @@ * | ||
| * - `handleCallback()` - Process OAuth callback | ||
| * - `signOut()`, `getSignInUrl()`, etc. - Delegate to AuthOperations | ||
| * - `signOut()`, `createSignIn()`, etc. - Delegate to AuthOperations | ||
| * | ||
@@ -49,3 +87,3 @@ * **Used by:** @workos/authkit-tanstack-react-start | ||
| this.core = new AuthKitCore(config, client, encryption); | ||
| this.operations = new AuthOperations(this.core, client, config); | ||
| this.operations = new AuthOperations(this.core, client, config, encryption); | ||
| } | ||
@@ -185,27 +223,74 @@ | ||
| /** | ||
| * Get authorization URL - delegates to AuthOperations. | ||
| * Create an authorization URL and write the PKCE verifier cookie. | ||
| * | ||
| * @param response - Framework-specific response object (may be undefined — | ||
| * adapters that mutate responses in-place will get back a mutated copy). | ||
| * @param options - screenHint, returnPathname, custom state, redirectUri, etc. | ||
| * @returns The URL to redirect the browser to, plus storage's | ||
| * `{ response?, headers? }` carrying the verifier `Set-Cookie`. | ||
| */ | ||
| async getAuthorizationUrl(options: GetAuthorizationUrlOptions = {}) { | ||
| return this.operations.getAuthorizationUrl(options); | ||
| async createAuthorization( | ||
| response: TResponse | undefined, | ||
| options: GetAuthorizationUrlOptions = {}, | ||
| ): Promise<CreateAuthorizationResult<TResponse>> { | ||
| const { url, sealedState, cookieOptions } = | ||
| await this.operations.createAuthorization(options); | ||
| const write = await this.storage.setCookie( | ||
| response, | ||
| PKCE_COOKIE_NAME, | ||
| sealedState, | ||
| cookieOptions, | ||
| ); | ||
| return { url, ...write }; | ||
| } | ||
| /** | ||
| * Convenience: Get sign-in URL. | ||
| * Convenience: Create sign-in URL and write the PKCE verifier cookie. | ||
| */ | ||
| async getSignInUrl( | ||
| async createSignIn( | ||
| response: TResponse | undefined, | ||
| options: Omit<GetAuthorizationUrlOptions, 'screenHint'> = {}, | ||
| ) { | ||
| return this.operations.getSignInUrl(options); | ||
| ): Promise<CreateAuthorizationResult<TResponse>> { | ||
| return this.createAuthorization(response, { | ||
| ...options, | ||
| screenHint: 'sign-in', | ||
| }); | ||
| } | ||
| /** | ||
| * Convenience: Get sign-up URL. | ||
| * Convenience: Create sign-up URL and write the PKCE verifier cookie. | ||
| */ | ||
| async getSignUpUrl( | ||
| async createSignUp( | ||
| response: TResponse | undefined, | ||
| options: Omit<GetAuthorizationUrlOptions, 'screenHint'> = {}, | ||
| ) { | ||
| return this.operations.getSignUpUrl(options); | ||
| ): Promise<CreateAuthorizationResult<TResponse>> { | ||
| return this.createAuthorization(response, { | ||
| ...options, | ||
| screenHint: 'sign-up', | ||
| }); | ||
| } | ||
| /** | ||
| * Emit a `Set-Cookie` header that clears the PKCE verifier cookie. | ||
| * | ||
| * Use on any exit path where a sign-in was started (verifier cookie | ||
| * written) but `handleCallback` will not run to clear it — OAuth error | ||
| * responses, missing `code`, early bail-outs. | ||
| * | ||
| * Pass `options.redirectUri` on requests that used a per-request | ||
| * `redirectUri` override at sign-in time, so the delete cookie's computed | ||
| * attributes (notably `secure`) match what was originally set. | ||
| */ | ||
| async clearPendingVerifier( | ||
| response: TResponse | undefined, | ||
| options?: Pick<GetAuthorizationUrlOptions, 'redirectUri'>, | ||
| ): Promise<{ response?: TResponse; headers?: HeadersBag }> { | ||
| return this.storage.clearCookie( | ||
| response, | ||
| PKCE_COOKIE_NAME, | ||
| getPKCECookieOptions(this.config, options?.redirectUri), | ||
| ); | ||
| } | ||
| /** | ||
| * Get the WorkOS client instance. | ||
@@ -220,74 +305,114 @@ * Useful for direct API calls not covered by AuthKit. | ||
| * Handle OAuth callback. | ||
| * This creates a new session after successful authentication. | ||
| * | ||
| * @param request - Framework-specific request (not currently used) | ||
| * @param response - Framework-specific response | ||
| * @param options - OAuth callback options (code, state) | ||
| * @returns Updated response, return pathname, and auth response | ||
| * Reads the verifier cookie via storage, verifies it against the URL | ||
| * `state`, exchanges the code, saves the session, and — on success — | ||
| * also emits a verifier-delete `Set-Cookie` so `HeadersBag['Set-Cookie']` | ||
| * carries both entries as a `string[]`. Adapters MUST append each | ||
| * `Set-Cookie` as its own header (never comma-join). | ||
| * | ||
| * Error-path cleanup: if any step after the cookie read throws | ||
| * (state mismatch, tampered seal, code-exchange failure, session-save | ||
| * failure), `handleCallback` attempts a best-effort `clearCookie` on the | ||
| * response before rethrowing so the verifier does not linger for its full | ||
| * 600s TTL. Response-mutating adapters get the delete `Set-Cookie` | ||
| * applied in place; headers-only adapters should still call | ||
| * `clearPendingVerifier` in their catch block to capture the headers bag. | ||
| */ | ||
| async handleCallback( | ||
| _request: TRequest, | ||
| request: TRequest, | ||
| response: TResponse, | ||
| options: { code: string; state?: string }, | ||
| options: { | ||
| code: string; | ||
| state: string | undefined; | ||
| }, | ||
| ) { | ||
| // Authenticate with WorkOS using the OAuth code | ||
| const authResponse = await this.client.userManagement.authenticateWithCode({ | ||
| code: options.code, | ||
| clientId: this.config.clientId, | ||
| }); | ||
| const cookieValue = await this.storage.getCookie(request, PKCE_COOKIE_NAME); | ||
| // Create and save the new session | ||
| const session: Session = { | ||
| accessToken: authResponse.accessToken, | ||
| refreshToken: authResponse.refreshToken, | ||
| user: authResponse.user, | ||
| impersonator: authResponse.impersonator, | ||
| }; | ||
| let unsealed; | ||
| try { | ||
| unsealed = await this.core.verifyCallbackState({ | ||
| stateFromUrl: options.state, | ||
| cookieValue: cookieValue ?? undefined, | ||
| }); | ||
| } catch (err) { | ||
| // Pre-unseal failure — we don't know the per-request redirectUri | ||
| // override, so emit a scheme-agnostic delete: same (name, domain, | ||
| // path) tuple as the original set (what the browser uses to match | ||
| // for replacement), with `Secure` dropped in the `sameSite: 'lax'` | ||
| // case so the Set-Cookie is accepted over http:// callbacks too. | ||
| // The `sameSite: 'none'` case already forces Secure on both set and | ||
| // clear, so there's no scheme-mismatch risk there. | ||
| await this.bestEffortClearVerifier(response, undefined, { | ||
| schemeAgnostic: true, | ||
| }); | ||
| throw err; | ||
| } | ||
| const encryptedSession = await this.core.encryptSession(session); | ||
| const { response: updatedResponse, headers } = await this.saveSession( | ||
| response, | ||
| encryptedSession, | ||
| ); | ||
| const { codeVerifier, returnPathname, customState, redirectUri } = unsealed; | ||
| const clearOptions = getPKCECookieOptions(this.config, redirectUri); | ||
| // Parse state: format is `{internal}.{userState}` or legacy `{base64JSON}` | ||
| let returnPathname = '/'; | ||
| let customState: string | undefined; | ||
| try { | ||
| const authResponse = | ||
| await this.client.userManagement.authenticateWithCode({ | ||
| code: options.code, | ||
| clientId: this.config.clientId, | ||
| codeVerifier, | ||
| }); | ||
| if (options.state) { | ||
| if (options.state.includes('.')) { | ||
| const [internal, ...rest] = options.state.split('.'); | ||
| customState = rest.join('.'); // Rejoin in case userState contains dots | ||
| try { | ||
| // Reverse URL-safe base64 encoding and decode | ||
| const decoded = (internal ?? '') | ||
| .replace(/-/g, '+') | ||
| .replace(/_/g, '/'); | ||
| const parsed = JSON.parse(atob(decoded)); | ||
| returnPathname = parsed.returnPathname || '/'; | ||
| } catch { | ||
| // Malformed internal state, use default | ||
| } | ||
| } else { | ||
| try { | ||
| const parsed = JSON.parse(atob(options.state)); | ||
| if (parsed.returnPathname) { | ||
| returnPathname = parsed.returnPathname; | ||
| } else { | ||
| customState = options.state; | ||
| } | ||
| } catch { | ||
| customState = options.state; | ||
| } | ||
| } | ||
| const session: Session = { | ||
| accessToken: authResponse.accessToken, | ||
| refreshToken: authResponse.refreshToken, | ||
| user: authResponse.user, | ||
| impersonator: authResponse.impersonator, | ||
| }; | ||
| const encryptedSession = await this.core.encryptSession(session); | ||
| const save = await this.storage.saveSession(response, encryptedSession); | ||
| const clear = await this.storage.clearCookie( | ||
| save.response ?? response, | ||
| PKCE_COOKIE_NAME, | ||
| clearOptions, | ||
| ); | ||
| return { | ||
| response: clear.response ?? save.response, | ||
| headers: mergeHeaderBags(save.headers, clear.headers), | ||
| returnPathname: returnPathname ?? '/', | ||
| state: customState, | ||
| authResponse, | ||
| }; | ||
| } catch (err) { | ||
| await this.bestEffortClearVerifier(response, redirectUri); | ||
| throw err; | ||
| } | ||
| } | ||
| return { | ||
| response: updatedResponse, | ||
| headers, | ||
| returnPathname, | ||
| state: customState, | ||
| authResponse, | ||
| }; | ||
| /** | ||
| * Best-effort verifier cleanup on a `handleCallback` error path. | ||
| * | ||
| * Mutates the response in place for response-mutating adapters. | ||
| * Swallows storage errors — cleanup must never mask the original failure. | ||
| * The original error is always rethrown by the caller. | ||
| * | ||
| * `schemeAgnostic` (pre-unseal failures only): drops the `Secure` attribute | ||
| * on the delete when `sameSite === 'lax'` so the browser accepts the | ||
| * `Set-Cookie` over http:// dev callbacks. Cookie replacement matches on | ||
| * (name, domain, path), not `Secure`, so the tuple-matched original cookie | ||
| * is still cleared regardless of its original `Secure` flag. | ||
| */ | ||
| private async bestEffortClearVerifier( | ||
| response: TResponse | undefined, | ||
| redirectUri: string | undefined, | ||
| { schemeAgnostic = false }: { schemeAgnostic?: boolean } = {}, | ||
| ): Promise<void> { | ||
| const options = getPKCECookieOptions(this.config, redirectUri); | ||
| if (schemeAgnostic && options.sameSite === 'lax') { | ||
| options.secure = false; | ||
| } | ||
| try { | ||
| await this.storage.clearCookie(response, PKCE_COOKIE_NAME, options); | ||
| } catch { | ||
| // Swallow: cleanup is opportunistic; callers get the original error. | ||
| } | ||
| } | ||
| } |
@@ -43,5 +43,6 @@ import { createAuthService } from './factory.js'; | ||
| expect(typeof service.signOut).toBe('function'); | ||
| expect(typeof service.getAuthorizationUrl).toBe('function'); | ||
| expect(typeof service.getSignInUrl).toBe('function'); | ||
| expect(typeof service.getSignUpUrl).toBe('function'); | ||
| expect(typeof service.createAuthorization).toBe('function'); | ||
| expect(typeof service.createSignIn).toBe('function'); | ||
| expect(typeof service.createSignUp).toBe('function'); | ||
| expect(typeof service.clearPendingVerifier).toBe('function'); | ||
| expect(typeof service.getWorkOS).toBe('function'); | ||
@@ -48,0 +49,0 @@ expect(typeof service.handleCallback).toBe('function'); |
@@ -75,5 +75,8 @@ import type { WorkOS } from '@workos-inc/node'; | ||
| getService().refreshSession(session, organizationId), | ||
| getAuthorizationUrl: opts => getService().getAuthorizationUrl(opts), | ||
| getSignInUrl: opts => getService().getSignInUrl(opts), | ||
| getSignUpUrl: opts => getService().getSignUpUrl(opts), | ||
| createAuthorization: (response, opts) => | ||
| getService().createAuthorization(response, opts), | ||
| createSignIn: (response, opts) => getService().createSignIn(response, opts), | ||
| createSignUp: (response, opts) => getService().createSignUp(response, opts), | ||
| clearPendingVerifier: (response, opts) => | ||
| getService().clearPendingVerifier(response, opts), | ||
| getWorkOS: () => getService().getWorkOS(), | ||
@@ -80,0 +83,0 @@ handleCallback: (request, response, opts) => |
+22
-0
@@ -22,1 +22,23 @@ /** | ||
| } | ||
| /** | ||
| * Constant-time byte-array equality. | ||
| * | ||
| * Used for comparing security-sensitive values (OAuth state, tokens, MACs) | ||
| * where early-return on mismatch would leak timing information about how | ||
| * many leading bytes matched. | ||
| * | ||
| * Runtime-portable: no `node:crypto`, no `Buffer` — works in Node, browsers, | ||
| * and edge runtimes. Callers produce `Uint8Array`s via `TextEncoder`. | ||
| * | ||
| * Contract: | ||
| * - Returns false immediately if lengths differ (length is not secret). | ||
| * - Once past the length gate, inspects EVERY byte regardless of mismatches. | ||
| * - No branches whose count depends on byte values. | ||
| */ | ||
| export function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { | ||
| if (a.length !== b.length) return false; | ||
| let diff = 0; | ||
| for (let i = 0; i < a.length; i++) diff |= a[i]! ^ b[i]!; | ||
| return diff === 0; | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
357388
48.45%117
24.47%7611
46.08%294
29.52%4
33.33%6
20%+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
Updated