@eventuras/fides-auth 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +32 -0
- package/dist/cookies.d.ts +105 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/cookies.js +118 -0
- package/dist/cookies.js.map +1 -0
- package/dist/heartbeat.d.ts +91 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +169 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/oauth-browser.js.map +1 -1
- package/dist/oauth.d.ts +52 -6
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +57 -33
- package/dist/oauth.js.map +1 -1
- package/dist/providers/vipps/index.js +2 -1
- package/dist/providers/vipps/index.js.map +1 -1
- package/dist/server/cookie-store.d.ts +10 -0
- package/dist/server/cookie-store.d.ts.map +1 -0
- package/dist/server/cookie-store.js +0 -0
- package/dist/server/heartbeat-handler.d.ts +21 -0
- package/dist/server/heartbeat-handler.d.ts.map +1 -0
- package/dist/server/index.d.ts +7 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +293 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/oidc-callback.d.ts +35 -0
- package/dist/server/oidc-callback.d.ts.map +1 -0
- package/dist/server/oidc-login.d.ts +23 -0
- package/dist/server/oidc-login.d.ts.map +1 -0
- package/dist/server/oidc-logout.d.ts +44 -0
- package/dist/server/oidc-logout.d.ts.map +1 -0
- package/dist/server/session.d.ts +33 -0
- package/dist/server/session.d.ts.map +1 -0
- package/dist/session-cookies.d.ts +52 -0
- package/dist/session-cookies.d.ts.map +1 -0
- package/dist/session-cookies.js +110 -0
- package/dist/session-cookies.js.map +1 -0
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +2 -1
- package/dist/session-refresh.js.map +1 -1
- package/dist/silent-login.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +16 -2
- package/dist/utils.js.map +1 -1
- package/docs/rp-initiated-logout.md +139 -0
- package/package.json +27 -9
package/dist/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","names":[],"sources":["../src/logger.ts"],"sourcesContent":["/**\n * Pluggable logger for fides-auth.\n *\n * The default writes newline-delimited JSON to `console.<level>` so output\n * is interoperable with Loki / Grafana / Datadog out of the box, even when\n * the consumer hasn't wired in a structured logger. Users can plug their\n * own backend (pino, winston, @eventuras/logger, ...) via `configureLogger()`.\n *\n * @example\n * // Use default JSONL console logger (no setup needed)\n * import { createLogger } from '@eventuras/fides-auth/logger';\n * const logger = createLogger({ namespace: 'my-app:auth' });\n * logger.info('Authentication successful');\n * // → {\"level\":\"info\",\"time\":\"…\",\"namespace\":\"my-app:auth\",\"msg\":\"Authentication successful\"}\n *\n * @example\n * // Plug in pino\n * import pino from 'pino';\n * import { configureLogger } from '@eventuras/fides-auth/logger';\n *\n * const pinoInstance = pino();\n * configureLogger({\n * create({ namespace, context }) {\n * return pinoInstance.child({ namespace, ...context });\n * },\n * });\n *\n * @example\n * // Plug in @eventuras/logger\n * import { Logger } from '@eventuras/logger';\n * import { configureLogger } from '@eventuras/fides-auth/logger';\n *\n * configureLogger({\n * create(options) {\n * return Logger.create(options);\n * },\n * });\n */\n\n/**\n * Logger interface compatible with pino, winston, and console.\n *\n * All methods accept either:\n * - A string message: `logger.info('hello')`\n * - A context object and message: `logger.info({ userId: 123 }, 'User logged in')`\n */\nexport interface FidesLogger {\n debug(data?: Record<string, unknown> | string, msg?: string): void;\n info(data?: Record<string, unknown> | string, msg?: string): void;\n warn(data?: Record<string, unknown> | string, msg?: string): void;\n error(data?: Record<string, unknown> | string, msg?: string): void;\n}\n\n/** Options for creating a logger instance. */\nexport interface FidesLoggerOptions {\n /** Namespace for log filtering (e.g., 'fides-auth:oauth') */\n namespace: string;\n /** Persistent context fields included in every log entry */\n context?: Record<string, unknown>;\n}\n\n/** Factory that creates logger instances. Implement this to integrate your preferred logger. */\nexport interface FidesLoggerFactory {\n create(options: FidesLoggerOptions): FidesLogger;\n}\n\n/**\n * Console-based logger implementation that emits newline-delimited JSON.\n *\n * Used as the default when no custom logger factory is configured. JSONL\n * keeps fides-auth's output interoperable with log shippers (Loki, Grafana,\n * Datadog, etc.) even when the consuming app hasn't wired in a structured\n * logger via `configureLogger()`. Each entry preserves the original\n * `console.<level>` call so browser devtools and CI log filters still work.\n *\n * Output shape (example):\n *\n * {\"level\":\"info\",\"time\":\"2026-04-15T19:40:57.300Z\",\"namespace\":\"fides-auth:oauth\",\"msg\":\"PKCE parameters generated\",\"codeChallenge\":\"…\"}\n *\n * Errors in the `data` field are serialized to `{ name, message, stack }`\n * so they survive `JSON.stringify`.\n */\ntype ConsoleMethod = 'debug' | 'info' | 'warn' | 'error';\n\nfunction serializeError(value: unknown): unknown {\n if (value instanceof Error) {\n return { name: value.name, message: value.message, stack: value.stack };\n }\n return value;\n}\n\n/** Field names the logger reserves so callers can't accidentally clobber them. */\nconst RESERVED_KEYS = new Set(['level', 'time', 'namespace']);\n\nfunction safeStringify(record: Record<string, unknown>): string {\n const seen = new WeakSet<object>();\n try {\n return JSON.stringify(record, (_key, value) => {\n if (value instanceof Error) return serializeError(value);\n if (typeof value === 'bigint') return value.toString();\n if (typeof value === 'object' && value !== null) {\n if (seen.has(value)) return '[Circular]';\n seen.add(value);\n }\n return value;\n });\n } catch {\n // Last-resort fallback: drop the original record entirely so a single\n // non-serializable payload can't take down the caller's request path.\n return JSON.stringify({\n level: record.level,\n time: record.time,\n namespace: record.namespace,\n msg: typeof record.msg === 'string' ? record.msg : '[unserializable log entry]',\n _serializeError: true,\n });\n }\n}\n\nclass ConsoleLogger implements FidesLogger {\n constructor(\n private readonly namespace: string,\n private readonly context?: Record<string, unknown>,\n ) { }\n\n private write(\n method: ConsoleMethod,\n level: 'debug' | 'info' | 'warn' | 'error',\n data?: Record<string, unknown> | string,\n msg?: string,\n ): void {\n // Skip noisy empty entries — match the pre-JSON ConsoleLogger, which\n // dropped calls with no data and no message.\n if (data === undefined && msg === undefined) return;\n\n // Reserved keys are set first so they appear at the front of the JSON\n // output. User-supplied fields are filtered against RESERVED_KEYS so\n // they can't clobber level/time/namespace.\n const entry: Record<string, unknown> = {\n level,\n time: new Date().toISOString(),\n namespace: this.namespace,\n };\n\n if (this.context) {\n for (const [key, value] of Object.entries(this.context)) {\n if (!RESERVED_KEYS.has(key)) entry[key] = value;\n }\n }\n\n if (typeof data === 'string') {\n entry.msg = data;\n } else {\n if (msg !== undefined) entry.msg = msg;\n if (data) {\n for (const [key, value] of Object.entries(data)) {\n if (!RESERVED_KEYS.has(key)) entry[key] = value;\n }\n }\n }\n\n if (entry.error !== undefined) {\n entry.error = serializeError(entry.error);\n }\n\n console[method](safeStringify(entry));\n }\n\n debug(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('debug', 'debug', data, msg);\n }\n\n info(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('info', 'info', data, msg);\n }\n\n warn(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('warn', 'warn', data, msg);\n }\n\n error(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('error', 'error', data, msg);\n }\n}\n\nconst defaultFactory: FidesLoggerFactory = {\n create({ namespace, context }) {\n return new ConsoleLogger(namespace, context);\n },\n};\n\nlet currentFactory: FidesLoggerFactory = defaultFactory;\nlet factoryVersion = 0;\n\n/**\n * Configure a custom logger factory for fides-auth.\n *\n * Can be called at any time — even after modules have already created loggers.\n * Existing loggers will pick up the new factory on their next log call.\n *\n * If not called, fides-auth uses a console-based logger.\n *\n * @param factory - A factory that creates logger instances\n */\nexport function configureLogger(factory: FidesLoggerFactory): void {\n currentFactory = factory;\n factoryVersion++;\n}\n\n/**\n * Create a logger instance using the configured factory.\n *\n * Returns a lazy proxy: the underlying logger is created (or re-created)\n * on first use and whenever the factory is changed via `configureLogger()`.\n * This means module-scope loggers work correctly even when `configureLogger()`\n * is called after module imports.\n *\n * @param options - Logger options including namespace and optional context\n * @returns A logger instance\n */\nexport function createLogger(options: FidesLoggerOptions): FidesLogger {\n let cached: FidesLogger | null = null;\n let cachedVersion = -1;\n\n function resolve(): FidesLogger {\n if (cached === null || cachedVersion !== factoryVersion) {\n cached = currentFactory.create(options);\n cachedVersion = factoryVersion;\n }\n return cached;\n }\n\n return {\n debug(data?, msg?) { resolve().debug(data, msg); },\n info(data?, msg?) { resolve().info(data, msg); },\n warn(data?, msg?) { resolve().warn(data, msg); },\n error(data?, msg?) { resolve().error(data, msg); },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"logger.js","names":[],"sources":["../src/logger.ts"],"sourcesContent":["/**\n * Pluggable logger for fides-auth.\n *\n * The default writes newline-delimited JSON to `console.<level>` so output\n * is interoperable with Loki / Grafana / Datadog out of the box, even when\n * the consumer hasn't wired in a structured logger. Users can plug their\n * own backend (pino, winston, @eventuras/logger, ...) via `configureLogger()`.\n *\n * @example\n * // Use default JSONL console logger (no setup needed)\n * import { createLogger } from '@eventuras/fides-auth/logger';\n * const logger = createLogger({ namespace: 'my-app:auth' });\n * logger.info('Authentication successful');\n * // → {\"level\":\"info\",\"time\":\"…\",\"namespace\":\"my-app:auth\",\"msg\":\"Authentication successful\"}\n *\n * @example\n * // Plug in pino\n * import pino from 'pino';\n * import { configureLogger } from '@eventuras/fides-auth/logger';\n *\n * const pinoInstance = pino();\n * configureLogger({\n * create({ namespace, context }) {\n * return pinoInstance.child({ namespace, ...context });\n * },\n * });\n *\n * @example\n * // Plug in @eventuras/logger\n * import { Logger } from '@eventuras/logger';\n * import { configureLogger } from '@eventuras/fides-auth/logger';\n *\n * configureLogger({\n * create(options) {\n * return Logger.create(options);\n * },\n * });\n */\n\n/**\n * Logger interface compatible with pino, winston, and console.\n *\n * All methods accept either:\n * - A string message: `logger.info('hello')`\n * - A context object and message: `logger.info({ userId: 123 }, 'User logged in')`\n */\nexport interface FidesLogger {\n debug(data?: Record<string, unknown> | string, msg?: string): void;\n info(data?: Record<string, unknown> | string, msg?: string): void;\n warn(data?: Record<string, unknown> | string, msg?: string): void;\n error(data?: Record<string, unknown> | string, msg?: string): void;\n}\n\n/** Options for creating a logger instance. */\nexport interface FidesLoggerOptions {\n /** Namespace for log filtering (e.g., 'fides-auth:oauth') */\n namespace: string;\n /** Persistent context fields included in every log entry */\n context?: Record<string, unknown>;\n}\n\n/** Factory that creates logger instances. Implement this to integrate your preferred logger. */\nexport interface FidesLoggerFactory {\n create(options: FidesLoggerOptions): FidesLogger;\n}\n\n/**\n * Console-based logger implementation that emits newline-delimited JSON.\n *\n * Used as the default when no custom logger factory is configured. JSONL\n * keeps fides-auth's output interoperable with log shippers (Loki, Grafana,\n * Datadog, etc.) even when the consuming app hasn't wired in a structured\n * logger via `configureLogger()`. Each entry preserves the original\n * `console.<level>` call so browser devtools and CI log filters still work.\n *\n * Output shape (example):\n *\n * {\"level\":\"info\",\"time\":\"2026-04-15T19:40:57.300Z\",\"namespace\":\"fides-auth:oauth\",\"msg\":\"PKCE parameters generated\",\"codeChallenge\":\"…\"}\n *\n * Errors in the `data` field are serialized to `{ name, message, stack }`\n * so they survive `JSON.stringify`.\n */\ntype ConsoleMethod = 'debug' | 'info' | 'warn' | 'error';\n\nfunction serializeError(value: unknown): unknown {\n if (value instanceof Error) {\n return { name: value.name, message: value.message, stack: value.stack };\n }\n return value;\n}\n\n/** Field names the logger reserves so callers can't accidentally clobber them. */\nconst RESERVED_KEYS = new Set(['level', 'time', 'namespace']);\n\nfunction safeStringify(record: Record<string, unknown>): string {\n const seen = new WeakSet<object>();\n try {\n return JSON.stringify(record, (_key, value) => {\n if (value instanceof Error) return serializeError(value);\n if (typeof value === 'bigint') return value.toString();\n if (typeof value === 'object' && value !== null) {\n if (seen.has(value)) return '[Circular]';\n seen.add(value);\n }\n return value;\n });\n } catch {\n // Last-resort fallback: drop the original record entirely so a single\n // non-serializable payload can't take down the caller's request path.\n return JSON.stringify({\n level: record.level,\n time: record.time,\n namespace: record.namespace,\n msg: typeof record.msg === 'string' ? record.msg : '[unserializable log entry]',\n _serializeError: true,\n });\n }\n}\n\nclass ConsoleLogger implements FidesLogger {\n constructor(\n private readonly namespace: string,\n private readonly context?: Record<string, unknown>,\n ) { }\n\n private write(\n method: ConsoleMethod,\n level: 'debug' | 'info' | 'warn' | 'error',\n data?: Record<string, unknown> | string,\n msg?: string,\n ): void {\n // Skip noisy empty entries — match the pre-JSON ConsoleLogger, which\n // dropped calls with no data and no message.\n if (data === undefined && msg === undefined) return;\n\n // Reserved keys are set first so they appear at the front of the JSON\n // output. User-supplied fields are filtered against RESERVED_KEYS so\n // they can't clobber level/time/namespace.\n const entry: Record<string, unknown> = {\n level,\n time: new Date().toISOString(),\n namespace: this.namespace,\n };\n\n if (this.context) {\n for (const [key, value] of Object.entries(this.context)) {\n if (!RESERVED_KEYS.has(key)) entry[key] = value;\n }\n }\n\n if (typeof data === 'string') {\n entry.msg = data;\n } else {\n if (msg !== undefined) entry.msg = msg;\n if (data) {\n for (const [key, value] of Object.entries(data)) {\n if (!RESERVED_KEYS.has(key)) entry[key] = value;\n }\n }\n }\n\n if (entry.error !== undefined) {\n entry.error = serializeError(entry.error);\n }\n\n console[method](safeStringify(entry));\n }\n\n debug(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('debug', 'debug', data, msg);\n }\n\n info(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('info', 'info', data, msg);\n }\n\n warn(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('warn', 'warn', data, msg);\n }\n\n error(data?: Record<string, unknown> | string, msg?: string): void {\n this.write('error', 'error', data, msg);\n }\n}\n\nconst defaultFactory: FidesLoggerFactory = {\n create({ namespace, context }) {\n return new ConsoleLogger(namespace, context);\n },\n};\n\nlet currentFactory: FidesLoggerFactory = defaultFactory;\nlet factoryVersion = 0;\n\n/**\n * Configure a custom logger factory for fides-auth.\n *\n * Can be called at any time — even after modules have already created loggers.\n * Existing loggers will pick up the new factory on their next log call.\n *\n * If not called, fides-auth uses a console-based logger.\n *\n * @param factory - A factory that creates logger instances\n */\nexport function configureLogger(factory: FidesLoggerFactory): void {\n currentFactory = factory;\n factoryVersion++;\n}\n\n/**\n * Create a logger instance using the configured factory.\n *\n * Returns a lazy proxy: the underlying logger is created (or re-created)\n * on first use and whenever the factory is changed via `configureLogger()`.\n * This means module-scope loggers work correctly even when `configureLogger()`\n * is called after module imports.\n *\n * @param options - Logger options including namespace and optional context\n * @returns A logger instance\n */\nexport function createLogger(options: FidesLoggerOptions): FidesLogger {\n let cached: FidesLogger | null = null;\n let cachedVersion = -1;\n\n function resolve(): FidesLogger {\n if (cached === null || cachedVersion !== factoryVersion) {\n cached = currentFactory.create(options);\n cachedVersion = factoryVersion;\n }\n return cached;\n }\n\n return {\n debug(data?, msg?) { resolve().debug(data, msg); },\n info(data?, msg?) { resolve().info(data, msg); },\n warn(data?, msg?) { resolve().warn(data, msg); },\n error(data?, msg?) { resolve().error(data, msg); },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-browser.js","names":[],"sources":["../src/oauth-browser.ts"],"sourcesContent":["/**\n * Browser-compatible OAuth 2.0 + PKCE helpers\n *\n * This module provides OAuth 2.0 helpers that work in browser environments.\n * Unlike `oauth.ts` which uses openid-client (Node.js only), this uses\n * browser-native Web Crypto API for PKCE generation.\n *\n * **Use this in:**\n * - Vite/React SPAs\n * - Browser-only applications\n * - Any frontend that can't use Node.js APIs\n *\n * **Usage:**\n * ```typescript\n * import { generatePKCE, buildAuthorizationUrl, exchangeCodeForTokens }\n * from '@eventuras/fides-auth/oauth-browser';\n *\n * // 1. Start OAuth flow\n * const pkce = await generatePKCE();\n * sessionStorage.setItem('code_verifier', pkce.code_verifier);\n * sessionStorage.setItem('state', pkce.state);\n *\n * const authUrl = buildAuthorizationUrl(config, pkce);\n * window.location.href = authUrl;\n *\n * // 2. Handle callback\n * const code = new URLSearchParams(window.location.search).get('code');\n * const verifier = sessionStorage.getItem('code_verifier');\n * const tokens = await exchangeCodeForTokens(config, code, verifier);\n * ```\n */\n\n/** Configuration for browser-side OAuth / OIDC flows (no client secret). */\nexport interface OAuthConfig {\n issuer: string;\n clientId: string;\n redirect_uri: string;\n scope: string;\n}\n\n/** PKCE parameters for a browser-initiated authorization request. */\nexport interface PKCEParams {\n code_verifier: string;\n code_challenge: string;\n state: string;\n}\n\n/**\n * Generate random string for code verifier (PKCE)\n * Uses Web Crypto API (browser-compatible)\n */\nfunction generateRandomString(length: number): string {\n const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n const randomValues = new Uint8Array(length);\n crypto.getRandomValues(randomValues);\n return Array.from(randomValues)\n .map((x) => charset[x % charset.length])\n .join('');\n}\n\n/**\n * Generate PKCE code challenge from verifier\n * Uses Web Crypto API SHA-256\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n\n // Base64URL encode\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate PKCE parameters for OAuth flow\n */\nexport async function generatePKCE(): Promise<PKCEParams> {\n const code_verifier = generateRandomString(128);\n const code_challenge = await generateCodeChallenge(code_verifier);\n const state = generateRandomString(32);\n\n return {\n code_verifier,\n code_challenge,\n state,\n };\n}\n\n/**\n * Build authorization URL for OAuth flow\n */\nexport function buildAuthorizationUrl(\n config: OAuthConfig,\n pkce: PKCEParams\n): string {\n const params = new URLSearchParams({\n client_id: config.clientId,\n redirect_uri: config.redirect_uri,\n response_type: 'code',\n scope: config.scope,\n state: pkce.state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: 'S256',\n });\n\n return `${config.issuer}/auth?${params.toString()}`;\n}\n\n/**\n * Exchange authorization code for tokens\n */\nexport async function exchangeCodeForTokens(\n config: OAuthConfig,\n code: string,\n codeVerifier: string\n): Promise<{\n access_token: string;\n id_token: string;\n refresh_token?: string;\n expires_in: number;\n}> {\n const response = await fetch(`${config.issuer}/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirect_uri,\n client_id: config.clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Token exchange failed: ${response.status} - ${error}`);\n }\n\n return response.json();\n}\n"],"mappings":";;;;;AAmDA,SAAS,qBAAqB,QAAwB;CACpD,MAAM,UAAU;CAChB,MAAM,eAAe,IAAI,WAAW,MAAM;CAC1C,OAAO,gBAAgB,YAAY;CACnC,OAAO,MAAM,KAAK,YAAY,
|
|
1
|
+
{"version":3,"file":"oauth-browser.js","names":[],"sources":["../src/oauth-browser.ts"],"sourcesContent":["/**\n * Browser-compatible OAuth 2.0 + PKCE helpers\n *\n * This module provides OAuth 2.0 helpers that work in browser environments.\n * Unlike `oauth.ts` which uses openid-client (Node.js only), this uses\n * browser-native Web Crypto API for PKCE generation.\n *\n * **Use this in:**\n * - Vite/React SPAs\n * - Browser-only applications\n * - Any frontend that can't use Node.js APIs\n *\n * **Usage:**\n * ```typescript\n * import { generatePKCE, buildAuthorizationUrl, exchangeCodeForTokens }\n * from '@eventuras/fides-auth/oauth-browser';\n *\n * // 1. Start OAuth flow\n * const pkce = await generatePKCE();\n * sessionStorage.setItem('code_verifier', pkce.code_verifier);\n * sessionStorage.setItem('state', pkce.state);\n *\n * const authUrl = buildAuthorizationUrl(config, pkce);\n * window.location.href = authUrl;\n *\n * // 2. Handle callback\n * const code = new URLSearchParams(window.location.search).get('code');\n * const verifier = sessionStorage.getItem('code_verifier');\n * const tokens = await exchangeCodeForTokens(config, code, verifier);\n * ```\n */\n\n/** Configuration for browser-side OAuth / OIDC flows (no client secret). */\nexport interface OAuthConfig {\n issuer: string;\n clientId: string;\n redirect_uri: string;\n scope: string;\n}\n\n/** PKCE parameters for a browser-initiated authorization request. */\nexport interface PKCEParams {\n code_verifier: string;\n code_challenge: string;\n state: string;\n}\n\n/**\n * Generate random string for code verifier (PKCE)\n * Uses Web Crypto API (browser-compatible)\n */\nfunction generateRandomString(length: number): string {\n const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';\n const randomValues = new Uint8Array(length);\n crypto.getRandomValues(randomValues);\n return Array.from(randomValues)\n .map((x) => charset[x % charset.length])\n .join('');\n}\n\n/**\n * Generate PKCE code challenge from verifier\n * Uses Web Crypto API SHA-256\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const hash = await crypto.subtle.digest('SHA-256', data);\n\n // Base64URL encode\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n}\n\n/**\n * Generate PKCE parameters for OAuth flow\n */\nexport async function generatePKCE(): Promise<PKCEParams> {\n const code_verifier = generateRandomString(128);\n const code_challenge = await generateCodeChallenge(code_verifier);\n const state = generateRandomString(32);\n\n return {\n code_verifier,\n code_challenge,\n state,\n };\n}\n\n/**\n * Build authorization URL for OAuth flow\n */\nexport function buildAuthorizationUrl(\n config: OAuthConfig,\n pkce: PKCEParams\n): string {\n const params = new URLSearchParams({\n client_id: config.clientId,\n redirect_uri: config.redirect_uri,\n response_type: 'code',\n scope: config.scope,\n state: pkce.state,\n code_challenge: pkce.code_challenge,\n code_challenge_method: 'S256',\n });\n\n return `${config.issuer}/auth?${params.toString()}`;\n}\n\n/**\n * Exchange authorization code for tokens\n */\nexport async function exchangeCodeForTokens(\n config: OAuthConfig,\n code: string,\n codeVerifier: string\n): Promise<{\n access_token: string;\n id_token: string;\n refresh_token?: string;\n expires_in: number;\n}> {\n const response = await fetch(`${config.issuer}/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n code,\n redirect_uri: config.redirect_uri,\n client_id: config.clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Token exchange failed: ${response.status} - ${error}`);\n }\n\n return response.json();\n}\n"],"mappings":";;;;;AAmDA,SAAS,qBAAqB,QAAwB;CACpD,MAAM,UAAU;CAChB,MAAM,eAAe,IAAI,WAAW,MAAM;CAC1C,OAAO,gBAAgB,YAAY;CACnC,OAAO,MAAM,KAAK,YAAY,CAAC,CAC5B,KAAK,MAAM,QAAQ,IAAI,GAAe,CAAC,CACvC,KAAK,EAAE;AACZ;;;;;AAMA,eAAe,sBAAsB,UAAmC;CAEtE,MAAM,OAAO,IADO,YACP,CAAA,CAAQ,OAAO,QAAQ;CACpC,MAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;CAGvD,OAAO,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,IAAI,CAAC,CAAC,CAAC,CACtD,WAAW,KAAK,GAAG,CAAC,CACpB,WAAW,KAAK,GAAG,CAAC,CACpB,QAAQ,OAAO,EAAE;AACtB;;;;AAKA,eAAsB,eAAoC;CACxD,MAAM,gBAAgB,qBAAqB,GAAG;CAI9C,OAAO;EACL;EACA,gBAAA,MAL2B,sBAAsB,aAAa;EAM9D,OALY,qBAAqB,EAKjC;CACF;AACF;;;;AAKA,SAAgB,sBACd,QACA,MACQ;CACR,MAAM,SAAS,IAAI,gBAAgB;EACjC,WAAW,OAAO;EAClB,cAAc,OAAO;EACrB,eAAe;EACf,OAAO,OAAO;EACd,OAAO,KAAK;EACZ,gBAAgB,KAAK;EACrB,uBAAuB;CACzB,CAAC;CAED,OAAO,GAAG,OAAO,OAAO,QAAQ,OAAO,SAAS;AAClD;;;;AAKA,eAAsB,sBACpB,QACA,MACA,cAMC;CACD,MAAM,WAAW,MAAM,MAAM,GAAG,OAAO,OAAO,SAAS;EACrD,QAAQ;EACR,SAAS,EACP,gBAAgB,oCAClB;EACA,MAAM,IAAI,gBAAgB;GACxB,YAAY;GACZ;GACA,cAAc,OAAO;GACrB,WAAW,OAAO;GAClB,eAAe;EACjB,CAAC;CACH,CAAC;CAED,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,QAAQ,MAAM,SAAS,KAAK;EAClC,MAAM,IAAI,MAAM,0BAA0B,SAAS,OAAO,KAAK,OAAO;CACxE;CAEA,OAAO,SAAS,KAAK;AACvB"}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -85,7 +85,7 @@ export declare function discoverAndBuildAuthorizationUrl(oauthConfig: OAuthConfi
|
|
|
85
85
|
* Configuration for OAuth client credentials flow (machine-to-machine authentication)
|
|
86
86
|
*/
|
|
87
87
|
export type ClientCredentialsConfig = {
|
|
88
|
-
/** OAuth token endpoint URL */
|
|
88
|
+
/** OAuth token endpoint URL. Must be https. */
|
|
89
89
|
tokenEndpoint: string;
|
|
90
90
|
/** OAuth client ID */
|
|
91
91
|
clientId: string;
|
|
@@ -93,6 +93,16 @@ export type ClientCredentialsConfig = {
|
|
|
93
93
|
clientSecret: string;
|
|
94
94
|
/** Optional scope for the access token */
|
|
95
95
|
scope?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Issuer identifier. Only used to satisfy the server metadata; defaults to the
|
|
98
|
+
* token endpoint's origin, which is right for the common case.
|
|
99
|
+
*/
|
|
100
|
+
issuer?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Request timeout in seconds.
|
|
103
|
+
* @default 30
|
|
104
|
+
*/
|
|
105
|
+
timeout?: number;
|
|
96
106
|
};
|
|
97
107
|
/**
|
|
98
108
|
* Exchanges an authorization code for tokens using OIDC discovery and PKCE.
|
|
@@ -142,21 +152,57 @@ export declare function buildSessionFromTokens(tokens: openid.TokenEndpointRespo
|
|
|
142
152
|
* @returns A safe, validated URL
|
|
143
153
|
*/
|
|
144
154
|
export declare function validateReturnUrl(returnTo: string | null | undefined, applicationOrigin: string, defaultPath?: string, validate?: (pathname: string) => boolean): URL;
|
|
155
|
+
/** Parameters for an RP-initiated logout request (OIDC RP-Initiated Logout 1.0 §2). */
|
|
156
|
+
export interface OidcLogoutOptions {
|
|
157
|
+
/** Post-logout redirect URI. Must be registered with the provider. */
|
|
158
|
+
postLogoutRedirectUri?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Raw ID token from `session.tokens.idToken`, sent as `id_token_hint`. Tells the
|
|
161
|
+
* OP which session to end, and is usually what lets it skip the confirmation
|
|
162
|
+
* interstitial.
|
|
163
|
+
*/
|
|
164
|
+
idTokenHint?: string;
|
|
165
|
+
/** Opaque value echoed back on the post-logout redirect. */
|
|
166
|
+
state?: string;
|
|
167
|
+
/** `logout_hint`, when the provider documents one. */
|
|
168
|
+
logoutHint?: string;
|
|
169
|
+
/**
|
|
170
|
+
* Whether to send `client_id`. Default true, also alongside `id_token_hint`: the
|
|
171
|
+
* spec allows both (the OP must then verify they match), and `client_id` is often
|
|
172
|
+
* what lets the OP accept `post_logout_redirect_uri` when the hint is stale or
|
|
173
|
+
* absent. Set false only for a provider that documents rejecting the combination.
|
|
174
|
+
*/
|
|
175
|
+
includeClientId?: boolean;
|
|
176
|
+
}
|
|
145
177
|
/**
|
|
146
|
-
* Discovers the OIDC provider's end_session_endpoint and builds
|
|
147
|
-
* Returns null
|
|
178
|
+
* Discovers the OIDC provider's end_session_endpoint and builds an RP-initiated
|
|
179
|
+
* logout URL. Returns null when the provider exposes none, so callers can fall
|
|
180
|
+
* back to clearing the local session only.
|
|
148
181
|
*
|
|
149
182
|
* @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)
|
|
150
|
-
* @param
|
|
183
|
+
* @param options - Logout parameters, or a string as shorthand for
|
|
184
|
+
* `postLogoutRedirectUri`
|
|
151
185
|
* @returns Logout URL or null if not supported
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const session = await readSession(cookies, secret);
|
|
190
|
+
* const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {
|
|
191
|
+
* postLogoutRedirectUri: 'https://app.example.com/',
|
|
192
|
+
* idTokenHint: session?.tokens?.idToken,
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
152
195
|
*/
|
|
153
|
-
export declare function buildOidcLogoutUrl(oauthConfig: OAuthConfig,
|
|
196
|
+
export declare function buildOidcLogoutUrl(oauthConfig: OAuthConfig, options: OidcLogoutOptions | string): Promise<URL | null>;
|
|
154
197
|
/**
|
|
155
198
|
* Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
|
|
156
199
|
*
|
|
200
|
+
* Uses client_secret_post against the given token endpoint. The endpoint must be
|
|
201
|
+
* https — the request carries the client secret.
|
|
202
|
+
*
|
|
157
203
|
* @param config - Client credentials configuration including token endpoint, client ID, and secret
|
|
158
204
|
* @returns Token response from the OAuth provider
|
|
159
|
-
* @throws Error if the
|
|
205
|
+
* @throws Error if the endpoint is not https, the request times out, or the grant fails
|
|
160
206
|
*
|
|
161
207
|
* @example
|
|
162
208
|
* ```typescript
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAIxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAE1E,uFAAuF;AACvF,eAAO,MAAM,YAAY,wCAAwC,CAAC;AAElE,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,WAAW,EACxB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CA4CvC;AAED,8DAA8D;AAC9D,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAmBhF;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,MAAM,CAAC,aAAa,EAC5B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,GAAG,CAAC,CASd;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,MAAM,CAAC,aAAa,EAC5B,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,GAAG,CAAC,CAYd;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gCAAgC,CACpD,WAAW,EAAE,WAAW,EACxB,WAAW,EAAE,WAAW,GACvB,OAAO,CAAC,GAAG,CAAC,CAkCd;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,+
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.js
CHANGED
|
@@ -217,7 +217,8 @@ function buildSessionFromTokens(tokens, rolesClaim = "roles") {
|
|
|
217
217
|
tokens: {
|
|
218
218
|
accessToken: tokens.access_token,
|
|
219
219
|
accessTokenExpiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1e3).toISOString() : void 0,
|
|
220
|
-
refreshToken: tokens.refresh_token
|
|
220
|
+
refreshToken: tokens.refresh_token,
|
|
221
|
+
idToken: tokens.id_token
|
|
221
222
|
},
|
|
222
223
|
user: {
|
|
223
224
|
name: userInfo.name,
|
|
@@ -262,24 +263,56 @@ function validateReturnUrl(returnTo, applicationOrigin, defaultPath = "/", valid
|
|
|
262
263
|
return redirectUrl;
|
|
263
264
|
}
|
|
264
265
|
/**
|
|
265
|
-
* Discovers the OIDC provider's end_session_endpoint and builds
|
|
266
|
-
* Returns null
|
|
266
|
+
* Discovers the OIDC provider's end_session_endpoint and builds an RP-initiated
|
|
267
|
+
* logout URL. Returns null when the provider exposes none, so callers can fall
|
|
268
|
+
* back to clearing the local session only.
|
|
267
269
|
*
|
|
268
270
|
* @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)
|
|
269
|
-
* @param
|
|
271
|
+
* @param options - Logout parameters, or a string as shorthand for
|
|
272
|
+
* `postLogoutRedirectUri`
|
|
270
273
|
* @returns Logout URL or null if not supported
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const session = await readSession(cookies, secret);
|
|
278
|
+
* const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {
|
|
279
|
+
* postLogoutRedirectUri: 'https://app.example.com/',
|
|
280
|
+
* idTokenHint: session?.tokens?.idToken,
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
271
283
|
*/
|
|
272
|
-
async function buildOidcLogoutUrl(oauthConfig,
|
|
284
|
+
async function buildOidcLogoutUrl(oauthConfig, options) {
|
|
285
|
+
const { postLogoutRedirectUri, idTokenHint, state, logoutHint, includeClientId = true } = typeof options === "string" ? { postLogoutRedirectUri: options } : options;
|
|
273
286
|
try {
|
|
274
|
-
const
|
|
287
|
+
const config = await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret));
|
|
288
|
+
const endSessionEndpoint = config.serverMetadata().end_session_endpoint;
|
|
275
289
|
if (!endSessionEndpoint) {
|
|
276
290
|
logger.info({ issuer: oauthConfig.issuer }, "No end_session_endpoint found");
|
|
277
291
|
return null;
|
|
278
292
|
}
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
const parameters = {};
|
|
294
|
+
if (postLogoutRedirectUri) parameters.post_logout_redirect_uri = postLogoutRedirectUri;
|
|
295
|
+
if (idTokenHint) parameters.id_token_hint = idTokenHint;
|
|
296
|
+
if (state) parameters.state = state;
|
|
297
|
+
if (logoutHint) parameters.logout_hint = logoutHint;
|
|
298
|
+
let logoutUrl;
|
|
299
|
+
if (includeClientId) logoutUrl = openid.buildEndSessionUrl(config, parameters);
|
|
300
|
+
else {
|
|
301
|
+
logoutUrl = new URL(endSessionEndpoint);
|
|
302
|
+
if (logoutUrl.protocol !== "https:") {
|
|
303
|
+
logger.error({
|
|
304
|
+
issuer: oauthConfig.issuer,
|
|
305
|
+
protocol: logoutUrl.protocol
|
|
306
|
+
}, "end_session_endpoint is not https — refusing to build a logout URL");
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
for (const [key, value] of Object.entries(parameters)) logoutUrl.searchParams.append(key, value);
|
|
310
|
+
}
|
|
311
|
+
logger.debug({
|
|
312
|
+
endSessionEndpoint,
|
|
313
|
+
hasIdTokenHint: !!idTokenHint,
|
|
314
|
+
hasState: !!state
|
|
315
|
+
}, "Built OIDC logout URL");
|
|
283
316
|
return logoutUrl;
|
|
284
317
|
} catch (error) {
|
|
285
318
|
logger.warn({
|
|
@@ -292,9 +325,12 @@ async function buildOidcLogoutUrl(oauthConfig, postLogoutRedirectUri) {
|
|
|
292
325
|
/**
|
|
293
326
|
* Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
|
|
294
327
|
*
|
|
328
|
+
* Uses client_secret_post against the given token endpoint. The endpoint must be
|
|
329
|
+
* https — the request carries the client secret.
|
|
330
|
+
*
|
|
295
331
|
* @param config - Client credentials configuration including token endpoint, client ID, and secret
|
|
296
332
|
* @returns Token response from the OAuth provider
|
|
297
|
-
* @throws Error if the
|
|
333
|
+
* @throws Error if the endpoint is not https, the request times out, or the grant fails
|
|
298
334
|
*
|
|
299
335
|
* @example
|
|
300
336
|
* ```typescript
|
|
@@ -308,31 +344,19 @@ async function buildOidcLogoutUrl(oauthConfig, postLogoutRedirectUri) {
|
|
|
308
344
|
*/
|
|
309
345
|
async function clientCredentialsGrant(config) {
|
|
310
346
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
320
|
-
body: params
|
|
321
|
-
});
|
|
322
|
-
if (!response.ok) {
|
|
323
|
-
const errorText = await response.text();
|
|
324
|
-
logger.error({
|
|
325
|
-
status: response.status,
|
|
326
|
-
error: errorText
|
|
327
|
-
}, "Client credentials grant failed");
|
|
328
|
-
throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);
|
|
329
|
-
}
|
|
330
|
-
return await response.json();
|
|
347
|
+
const openidConfig = new openid.Configuration({
|
|
348
|
+
issuer: config.issuer ?? new URL(config.tokenEndpoint).origin,
|
|
349
|
+
token_endpoint: config.tokenEndpoint
|
|
350
|
+
}, config.clientId, config.clientSecret, openid.ClientSecretPost(config.clientSecret));
|
|
351
|
+
openidConfig.timeout = config.timeout ?? 30;
|
|
352
|
+
const tokens = await openid.clientCredentialsGrant(openidConfig, config.scope ? { scope: config.scope } : void 0);
|
|
353
|
+
logger.debug({ tokenEndpoint: config.tokenEndpoint }, "Client credentials grant successful");
|
|
354
|
+
return tokens;
|
|
331
355
|
} catch (error) {
|
|
332
356
|
logger.error({
|
|
333
|
-
error,
|
|
357
|
+
...getOAuthErrorLogContext(error),
|
|
334
358
|
tokenEndpoint: config.tokenEndpoint
|
|
335
|
-
}, "Client credentials grant
|
|
359
|
+
}, "Client credentials grant failed");
|
|
336
360
|
throw error;
|
|
337
361
|
}
|
|
338
362
|
}
|
package/dist/oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport type { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:oauth' });\n\n// Re-export commonly used types from openid-client for convenience\nexport type { Configuration, TokenEndpointResponse } from 'openid-client';\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Configuration for server-side OAuth / OIDC flows. */\nexport type OAuthConfig = {\n issuer: string;\n clientId: string;\n clientSecret: string;\n redirect_uri: string;\n scope: string;\n /**\n * Opt in to Pushed Authorization Requests (RFC 9126) when the provider\n * supports it. When true, authorization parameters are posted to the\n * provider's PAR endpoint ahead of the redirect, and only `client_id` +\n * `request_uri` travel through the user-agent. The provider must advertise\n * `pushed_authorization_request_endpoint` in its discovery metadata.\n */\n usePar?: boolean;\n};\n\n/**\n * Refreshes an access token using a refresh token grant.\n *\n * @param oAuthConfig - OAuth configuration for the identity provider\n * @param refreshToken - The refresh token to exchange\n * @returns Token endpoint response containing new access and refresh tokens\n */\nexport async function refreshAccessToken(\n oAuthConfig: OAuthConfig,\n refreshToken: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug({ issuer: oAuthConfig.issuer }, 'Starting token refresh');\n\n try {\n const config = await openid.discovery(\n new URL(oAuthConfig.issuer),\n oAuthConfig.clientId,\n oAuthConfig.clientSecret,\n openid.ClientSecretPost(oAuthConfig.clientSecret),\n );\n\n const tokens = await openid.refreshTokenGrant(\n config,\n refreshToken,\n {\n scope: oAuthConfig.scope,\n },\n );\n\n logger.info({\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in\n }, 'Token refresh successful');\n\n return tokens;\n } catch (error) {\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Most often the refresh token has expired - log at info level\n logger.info(\n { ...errorContext, issuer: oAuthConfig.issuer },\n 'Token refresh failed - refresh token expired or invalid',\n );\n } else {\n // Unexpected error - log at error level\n logger.error({ error, issuer: oAuthConfig.issuer }, 'Token refresh failed');\n }\n\n throw error;\n }\n}\n\n/** PKCE parameters generated for an authorization request. */\nexport interface PKCEOptions {\n code_verifier: string;\n code_challenge: string;\n state: string;\n parameters: Record<string, string>;\n}\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @returns A promise that resolves to an object containing:\n * - code_verifier: The randomly generated PKCE code verifier.\n * - code_challenge: The derived PKCE code challenge.\n * - state: A random state to protect against CSRF.\n * - parameters: A Record<string,string> with the assembled parameters.\n */\nexport async function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions> {\n const code_verifier = openid.randomPKCECodeVerifier();\n const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);\n const state = openid.randomState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope ?? defaultScope,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n logger.info(\n { redirectUri: config.redirect_uri, scope: parameters.scope },\n 'PKCE parameters generated'\n );\n\n return { code_verifier, code_challenge, state, parameters };\n}\n\n/**\n * Builds an authorization URL with PKCE parameters.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function buildAuthorizationUrl(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);\n logger.info('Authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build authorization URL');\n throw error;\n }\n}\n\n/**\n * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).\n *\n * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL\n * containing only `client_id` and `request_uri`. The provider's discovery\n * metadata must include `pushed_authorization_request_endpoint`.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the PAR-based authorization URL\n */\nexport async function buildAuthorizationUrlWithPAR(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = await openid.buildAuthorizationUrlWithPAR(\n config,\n pkceOptions.parameters,\n );\n logger.info('PAR authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build PAR authorization URL');\n throw error;\n }\n}\n\n/**\n * Convenience function to discover OpenID configuration and build an\n * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and\n * the provider advertises `pushed_authorization_request_endpoint`.\n *\n * - `usePar === true` + provider advertises PAR → uses PAR.\n * - `usePar === true` + provider does **not** advertise PAR → throws.\n * - `usePar` unset/false + provider advertises PAR → standard flow + one-line\n * `info` advisory that PAR is available but not enabled.\n * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.\n *\n * @param oauthConfig - Your OAuth configuration\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function discoverAndBuildAuthorizationUrl(\n oauthConfig: OAuthConfig,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n logger.debug({ issuer: oauthConfig.issuer }, 'Discovering OpenID configuration');\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret)\n );\n\n const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;\n\n if (oauthConfig.usePar) {\n if (!parEndpoint) {\n throw new Error(\n 'PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint',\n );\n }\n return buildAuthorizationUrlWithPAR(config, pkceOptions);\n }\n\n if (parEndpoint) {\n logger.info(\n { issuer: oauthConfig.issuer },\n 'Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it',\n );\n }\n\n return buildAuthorizationUrl(config, pkceOptions);\n } catch (error) {\n logger.error({ error, issuer: oauthConfig.issuer }, 'Failed to discover config or build URL');\n throw error;\n }\n}\n\n/**\n * Configuration for OAuth client credentials flow (machine-to-machine authentication)\n */\nexport type ClientCredentialsConfig = {\n /** OAuth token endpoint URL */\n tokenEndpoint: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Optional scope for the access token */\n scope?: string;\n};\n\n/**\n * Exchanges an authorization code for tokens using OIDC discovery and PKCE.\n *\n * @param oauthConfig - OAuth configuration\n * @param callbackUrl - The full callback URL with query parameters from the OIDC provider\n * @param codeVerifier - The PKCE code verifier stored during login initiation\n * @param expectedState - The state parameter stored during login initiation\n * @returns Token response from the OIDC provider\n */\nexport async function exchangeAuthorizationCode(\n oauthConfig: OAuthConfig,\n callbackUrl: URL,\n codeVerifier: string,\n expectedState: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug(\n { issuer: oauthConfig.issuer, clientId: oauthConfig.clientId },\n 'Exchanging authorization code for tokens',\n );\n\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n\n // openid-client overrides the explicit redirect_uri option with\n // stripParams(callbackUrl). Anchor to oauthConfig.redirect_uri so PAR and\n // token-exchange stay consistent behind reverse proxies that don't forward\n // the original scheme. Build from the trusted base and then copy pathname/\n // search by assignment — `new URL(callbackUrl.pathname, base)` would let a\n // `//evil.com/...` pathname swap the host.\n const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);\n normalizedCallbackUrl.pathname = callbackUrl.pathname;\n normalizedCallbackUrl.search = callbackUrl.search;\n normalizedCallbackUrl.hash = '';\n\n const tokens = await openid.authorizationCodeGrant(\n config,\n normalizedCallbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState,\n },\n { redirect_uri: oauthConfig.redirect_uri },\n );\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Authorization code exchange successful',\n );\n\n return tokens;\n}\n\n/**\n * Result of extracting user information from an OIDC token response.\n */\nexport interface OidcUserInfo {\n sub: string;\n name: string;\n email: string;\n roles: string[];\n}\n\n/**\n * Extracts user information from an OIDC token response.\n * Decodes the ID token and extracts standard claims plus roles from a configurable claim.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns Extracted user information\n */\nexport function extractUserFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): OidcUserInfo {\n if (!tokens.id_token) {\n throw new Error('OIDC token response is missing id_token');\n }\n\n const idToken = decodeJwt(tokens.id_token);\n\n // Normalize roles: some providers send a single string instead of an array\n const rawRoles = idToken[rolesClaim];\n const nonArrayRoles = typeof rawRoles === 'string' ? [rawRoles] : [];\n const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;\n\n logger.debug({ sub: idToken.sub, rolesClaim, rolesCount: roles.length }, 'Extracted user from ID token');\n\n return {\n sub: idToken.sub ?? '',\n name: idToken.name as string,\n email: idToken.email as string,\n roles,\n };\n}\n\n/**\n * Builds a Session object from an OIDC token response.\n * Maps provider tokens and user claims into the internal Session format.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): Session {\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n const userInfo = extractUserFromTokens(tokens, rolesClaim);\n\n let scopes: string[] | undefined;\n if (typeof tokens.scope === 'string') {\n const parsed = tokens.scope.split(' ').filter(s => s.length > 0);\n if (parsed.length > 0) scopes = parsed;\n }\n\n return {\n tokens: {\n accessToken: tokens.access_token,\n accessTokenExpiresAt: tokens.expires_in\n ? new Date(Date.now() + tokens.expires_in * 1000).toISOString()\n : undefined,\n refreshToken: tokens.refresh_token,\n },\n user: {\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n}\n\n/**\n * Validates a return URL against open redirect attacks.\n * Ensures the URL is same-origin and passes optional custom validation.\n *\n * @param returnTo - The raw returnTo path/URL to validate\n * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')\n * @param defaultPath - Fallback path if validation fails (default: '/')\n * @param validate - Optional custom validation function for the pathname\n * @returns A safe, validated URL\n */\nexport function validateReturnUrl(\n returnTo: string | null | undefined,\n applicationOrigin: string,\n defaultPath: string = '/',\n validate?: (pathname: string) => boolean,\n): URL {\n const redirectPath = returnTo ?? defaultPath;\n let redirectUrl: URL;\n\n try {\n redirectUrl = new URL(redirectPath, applicationOrigin);\n } catch {\n logger.warn({ returnTo: redirectPath }, 'Invalid returnTo URL, using default');\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Enforce same-origin — cross-origin returnTo is a potential open redirect attack\n const originUrl = new URL(applicationOrigin);\n if (redirectUrl.origin !== originUrl.origin) {\n logger.error(\n { returnTo: redirectPath, redirectOrigin: redirectUrl.origin, applicationOrigin: originUrl.origin },\n 'Possible open redirect attack: returnTo points outside application origin',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Custom validation\n if (validate && !validate(redirectUrl.pathname)) {\n logger.warn(\n { pathname: redirectUrl.pathname },\n 'returnTo failed custom validation, falling back to default',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n return redirectUrl;\n}\n\n/**\n * Discovers the OIDC provider's end_session_endpoint and builds a logout URL.\n * Returns null if the provider doesn't expose an end_session_endpoint.\n *\n * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)\n * @param postLogoutRedirectUri - URL to redirect to after provider logout\n * @returns Logout URL or null if not supported\n */\nexport async function buildOidcLogoutUrl(\n oauthConfig: OAuthConfig,\n postLogoutRedirectUri: string,\n): Promise<URL | null> {\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n const endSessionEndpoint = config.serverMetadata().end_session_endpoint;\n\n if (!endSessionEndpoint) {\n logger.info({ issuer: oauthConfig.issuer }, 'No end_session_endpoint found');\n return null;\n }\n\n const logoutUrl = new URL(endSessionEndpoint);\n logoutUrl.searchParams.set('client_id', oauthConfig.clientId);\n logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);\n\n logger.debug({ logoutUrl: logoutUrl.toString() }, 'Built OIDC logout URL');\n return logoutUrl;\n } catch (error) {\n logger.warn({ error, issuer: oauthConfig.issuer }, 'OIDC discovery failed for logout');\n return null;\n }\n}\n\n/**\n * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.\n *\n * @param config - Client credentials configuration including token endpoint, client ID, and secret\n * @returns Token response from the OAuth provider\n * @throws Error if the token request fails\n *\n * @example\n * ```typescript\n * const tokens = await clientCredentialsGrant({\n * tokenEndpoint: 'https://api.example.com/oauth2/token',\n * clientId: 'my-client-id',\n * clientSecret: 'my-client-secret',\n * scope: 'api:read api:write',\n * });\n * ```\n */\nexport async function clientCredentialsGrant(\n config: ClientCredentialsConfig\n): Promise<openid.TokenEndpointResponse> {\n try {\n const params = new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: config.clientId,\n client_secret: config.clientSecret,\n });\n\n if (config.scope) {\n params.set('scope', config.scope);\n }\n\n const response = await fetch(config.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: params,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n { status: response.status, error: errorText },\n 'Client credentials grant failed'\n );\n throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);\n }\n\n return (await response.json()) as openid.TokenEndpointResponse;\n } catch (error) {\n logger.error({ error, tokenEndpoint: config.tokenEndpoint }, 'Client credentials grant error');\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport type { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:oauth' });\n\n// Re-export commonly used types from openid-client for convenience\nexport type { Configuration, TokenEndpointResponse } from 'openid-client';\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Configuration for server-side OAuth / OIDC flows. */\nexport type OAuthConfig = {\n issuer: string;\n clientId: string;\n clientSecret: string;\n redirect_uri: string;\n scope: string;\n /**\n * Opt in to Pushed Authorization Requests (RFC 9126) when the provider\n * supports it. When true, authorization parameters are posted to the\n * provider's PAR endpoint ahead of the redirect, and only `client_id` +\n * `request_uri` travel through the user-agent. The provider must advertise\n * `pushed_authorization_request_endpoint` in its discovery metadata.\n */\n usePar?: boolean;\n};\n\n/**\n * Refreshes an access token using a refresh token grant.\n *\n * @param oAuthConfig - OAuth configuration for the identity provider\n * @param refreshToken - The refresh token to exchange\n * @returns Token endpoint response containing new access and refresh tokens\n */\nexport async function refreshAccessToken(\n oAuthConfig: OAuthConfig,\n refreshToken: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug({ issuer: oAuthConfig.issuer }, 'Starting token refresh');\n\n try {\n const config = await openid.discovery(\n new URL(oAuthConfig.issuer),\n oAuthConfig.clientId,\n oAuthConfig.clientSecret,\n openid.ClientSecretPost(oAuthConfig.clientSecret),\n );\n\n const tokens = await openid.refreshTokenGrant(\n config,\n refreshToken,\n {\n scope: oAuthConfig.scope,\n },\n );\n\n logger.info({\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in\n }, 'Token refresh successful');\n\n return tokens;\n } catch (error) {\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Most often the refresh token has expired - log at info level\n logger.info(\n { ...errorContext, issuer: oAuthConfig.issuer },\n 'Token refresh failed - refresh token expired or invalid',\n );\n } else {\n // Unexpected error - log at error level\n logger.error({ error, issuer: oAuthConfig.issuer }, 'Token refresh failed');\n }\n\n throw error;\n }\n}\n\n/** PKCE parameters generated for an authorization request. */\nexport interface PKCEOptions {\n code_verifier: string;\n code_challenge: string;\n state: string;\n parameters: Record<string, string>;\n}\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @returns A promise that resolves to an object containing:\n * - code_verifier: The randomly generated PKCE code verifier.\n * - code_challenge: The derived PKCE code challenge.\n * - state: A random state to protect against CSRF.\n * - parameters: A Record<string,string> with the assembled parameters.\n */\nexport async function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions> {\n const code_verifier = openid.randomPKCECodeVerifier();\n const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);\n const state = openid.randomState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope ?? defaultScope,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n logger.info(\n { redirectUri: config.redirect_uri, scope: parameters.scope },\n 'PKCE parameters generated'\n );\n\n return { code_verifier, code_challenge, state, parameters };\n}\n\n/**\n * Builds an authorization URL with PKCE parameters.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function buildAuthorizationUrl(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);\n logger.info('Authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build authorization URL');\n throw error;\n }\n}\n\n/**\n * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).\n *\n * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL\n * containing only `client_id` and `request_uri`. The provider's discovery\n * metadata must include `pushed_authorization_request_endpoint`.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the PAR-based authorization URL\n */\nexport async function buildAuthorizationUrlWithPAR(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = await openid.buildAuthorizationUrlWithPAR(\n config,\n pkceOptions.parameters,\n );\n logger.info('PAR authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build PAR authorization URL');\n throw error;\n }\n}\n\n/**\n * Convenience function to discover OpenID configuration and build an\n * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and\n * the provider advertises `pushed_authorization_request_endpoint`.\n *\n * - `usePar === true` + provider advertises PAR → uses PAR.\n * - `usePar === true` + provider does **not** advertise PAR → throws.\n * - `usePar` unset/false + provider advertises PAR → standard flow + one-line\n * `info` advisory that PAR is available but not enabled.\n * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.\n *\n * @param oauthConfig - Your OAuth configuration\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function discoverAndBuildAuthorizationUrl(\n oauthConfig: OAuthConfig,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n logger.debug({ issuer: oauthConfig.issuer }, 'Discovering OpenID configuration');\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret)\n );\n\n const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;\n\n if (oauthConfig.usePar) {\n if (!parEndpoint) {\n throw new Error(\n 'PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint',\n );\n }\n return buildAuthorizationUrlWithPAR(config, pkceOptions);\n }\n\n if (parEndpoint) {\n logger.info(\n { issuer: oauthConfig.issuer },\n 'Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it',\n );\n }\n\n return buildAuthorizationUrl(config, pkceOptions);\n } catch (error) {\n logger.error({ error, issuer: oauthConfig.issuer }, 'Failed to discover config or build URL');\n throw error;\n }\n}\n\n/**\n * Configuration for OAuth client credentials flow (machine-to-machine authentication)\n */\nexport type ClientCredentialsConfig = {\n /** OAuth token endpoint URL. Must be https. */\n tokenEndpoint: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Optional scope for the access token */\n scope?: string;\n /**\n * Issuer identifier. Only used to satisfy the server metadata; defaults to the\n * token endpoint's origin, which is right for the common case.\n */\n issuer?: string;\n /**\n * Request timeout in seconds.\n * @default 30\n */\n timeout?: number;\n};\n\n/**\n * Exchanges an authorization code for tokens using OIDC discovery and PKCE.\n *\n * @param oauthConfig - OAuth configuration\n * @param callbackUrl - The full callback URL with query parameters from the OIDC provider\n * @param codeVerifier - The PKCE code verifier stored during login initiation\n * @param expectedState - The state parameter stored during login initiation\n * @returns Token response from the OIDC provider\n */\nexport async function exchangeAuthorizationCode(\n oauthConfig: OAuthConfig,\n callbackUrl: URL,\n codeVerifier: string,\n expectedState: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug(\n { issuer: oauthConfig.issuer, clientId: oauthConfig.clientId },\n 'Exchanging authorization code for tokens',\n );\n\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n\n // openid-client overrides the explicit redirect_uri option with\n // stripParams(callbackUrl). Anchor to oauthConfig.redirect_uri so PAR and\n // token-exchange stay consistent behind reverse proxies that don't forward\n // the original scheme. Build from the trusted base and then copy pathname/\n // search by assignment — `new URL(callbackUrl.pathname, base)` would let a\n // `//evil.com/...` pathname swap the host.\n const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);\n normalizedCallbackUrl.pathname = callbackUrl.pathname;\n normalizedCallbackUrl.search = callbackUrl.search;\n normalizedCallbackUrl.hash = '';\n\n const tokens = await openid.authorizationCodeGrant(\n config,\n normalizedCallbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState,\n },\n { redirect_uri: oauthConfig.redirect_uri },\n );\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Authorization code exchange successful',\n );\n\n return tokens;\n}\n\n/**\n * Result of extracting user information from an OIDC token response.\n */\nexport interface OidcUserInfo {\n sub: string;\n name: string;\n email: string;\n roles: string[];\n}\n\n/**\n * Extracts user information from an OIDC token response.\n * Decodes the ID token and extracts standard claims plus roles from a configurable claim.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns Extracted user information\n */\nexport function extractUserFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): OidcUserInfo {\n if (!tokens.id_token) {\n throw new Error('OIDC token response is missing id_token');\n }\n\n const idToken = decodeJwt(tokens.id_token);\n\n // Normalize roles: some providers send a single string instead of an array\n const rawRoles = idToken[rolesClaim];\n const nonArrayRoles = typeof rawRoles === 'string' ? [rawRoles] : [];\n const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;\n\n logger.debug({ sub: idToken.sub, rolesClaim, rolesCount: roles.length }, 'Extracted user from ID token');\n\n return {\n sub: idToken.sub ?? '',\n name: idToken.name as string,\n email: idToken.email as string,\n roles,\n };\n}\n\n/**\n * Builds a Session object from an OIDC token response.\n * Maps provider tokens and user claims into the internal Session format.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): Session {\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n const userInfo = extractUserFromTokens(tokens, rolesClaim);\n\n let scopes: string[] | undefined;\n if (typeof tokens.scope === 'string') {\n const parsed = tokens.scope.split(' ').filter(s => s.length > 0);\n if (parsed.length > 0) scopes = parsed;\n }\n\n return {\n tokens: {\n accessToken: tokens.access_token,\n accessTokenExpiresAt: tokens.expires_in\n ? new Date(Date.now() + tokens.expires_in * 1000).toISOString()\n : undefined,\n refreshToken: tokens.refresh_token,\n // Kept for `id_token_hint` on RP-initiated logout.\n idToken: tokens.id_token,\n },\n user: {\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n}\n\n/**\n * Validates a return URL against open redirect attacks.\n * Ensures the URL is same-origin and passes optional custom validation.\n *\n * @param returnTo - The raw returnTo path/URL to validate\n * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')\n * @param defaultPath - Fallback path if validation fails (default: '/')\n * @param validate - Optional custom validation function for the pathname\n * @returns A safe, validated URL\n */\nexport function validateReturnUrl(\n returnTo: string | null | undefined,\n applicationOrigin: string,\n defaultPath: string = '/',\n validate?: (pathname: string) => boolean,\n): URL {\n const redirectPath = returnTo ?? defaultPath;\n let redirectUrl: URL;\n\n try {\n redirectUrl = new URL(redirectPath, applicationOrigin);\n } catch {\n logger.warn({ returnTo: redirectPath }, 'Invalid returnTo URL, using default');\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Enforce same-origin — cross-origin returnTo is a potential open redirect attack\n const originUrl = new URL(applicationOrigin);\n if (redirectUrl.origin !== originUrl.origin) {\n logger.error(\n { returnTo: redirectPath, redirectOrigin: redirectUrl.origin, applicationOrigin: originUrl.origin },\n 'Possible open redirect attack: returnTo points outside application origin',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Custom validation\n if (validate && !validate(redirectUrl.pathname)) {\n logger.warn(\n { pathname: redirectUrl.pathname },\n 'returnTo failed custom validation, falling back to default',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n return redirectUrl;\n}\n\n/** Parameters for an RP-initiated logout request (OIDC RP-Initiated Logout 1.0 §2). */\nexport interface OidcLogoutOptions {\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri?: string;\n\n /**\n * Raw ID token from `session.tokens.idToken`, sent as `id_token_hint`. Tells the\n * OP which session to end, and is usually what lets it skip the confirmation\n * interstitial.\n */\n idTokenHint?: string;\n\n /** Opaque value echoed back on the post-logout redirect. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /**\n * Whether to send `client_id`. Default true, also alongside `id_token_hint`: the\n * spec allows both (the OP must then verify they match), and `client_id` is often\n * what lets the OP accept `post_logout_redirect_uri` when the hint is stale or\n * absent. Set false only for a provider that documents rejecting the combination.\n */\n includeClientId?: boolean;\n}\n\n/**\n * Discovers the OIDC provider's end_session_endpoint and builds an RP-initiated\n * logout URL. Returns null when the provider exposes none, so callers can fall\n * back to clearing the local session only.\n *\n * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)\n * @param options - Logout parameters, or a string as shorthand for\n * `postLogoutRedirectUri`\n * @returns Logout URL or null if not supported\n *\n * @example\n * ```typescript\n * const session = await readSession(cookies, secret);\n * const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n * postLogoutRedirectUri: 'https://app.example.com/',\n * idTokenHint: session?.tokens?.idToken,\n * });\n * ```\n */\nexport async function buildOidcLogoutUrl(\n oauthConfig: OAuthConfig,\n options: OidcLogoutOptions | string,\n): Promise<URL | null> {\n const {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId = true,\n } = typeof options === 'string' ? { postLogoutRedirectUri: options } : options;\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n const endSessionEndpoint = config.serverMetadata().end_session_endpoint;\n\n if (!endSessionEndpoint) {\n logger.info({ issuer: oauthConfig.issuer }, 'No end_session_endpoint found');\n return null;\n }\n\n const parameters: Record<string, string> = {};\n if (postLogoutRedirectUri) parameters.post_logout_redirect_uri = postLogoutRedirectUri;\n if (idTokenHint) parameters.id_token_hint = idTokenHint;\n if (state) parameters.state = state;\n if (logoutHint) parameters.logout_hint = logoutHint;\n\n // buildEndSessionUrl always adds client_id, and deleting it afterwards would\n // also drop one the provider baked into its own endpoint — so when it isn't\n // wanted, assemble the URL directly instead.\n let logoutUrl: URL;\n if (includeClientId) {\n logoutUrl = openid.buildEndSessionUrl(config, parameters);\n } else {\n logoutUrl = new URL(endSessionEndpoint);\n\n // openid-client rejects a non-https endpoint on the path above; the manual\n // one must too. The URL carries a raw id_token_hint, so a discovery document\n // that downgrades the scheme would put it on the wire in the clear.\n if (logoutUrl.protocol !== 'https:') {\n logger.error(\n { issuer: oauthConfig.issuer, protocol: logoutUrl.protocol },\n 'end_session_endpoint is not https — refusing to build a logout URL',\n );\n return null;\n }\n\n for (const [key, value] of Object.entries(parameters)) {\n logoutUrl.searchParams.append(key, value);\n }\n }\n\n logger.debug(\n { endSessionEndpoint, hasIdTokenHint: !!idTokenHint, hasState: !!state },\n 'Built OIDC logout URL',\n );\n return logoutUrl;\n } catch (error) {\n logger.warn({ error, issuer: oauthConfig.issuer }, 'OIDC discovery failed for logout');\n return null;\n }\n}\n\n/**\n * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.\n *\n * Uses client_secret_post against the given token endpoint. The endpoint must be\n * https — the request carries the client secret.\n *\n * @param config - Client credentials configuration including token endpoint, client ID, and secret\n * @returns Token response from the OAuth provider\n * @throws Error if the endpoint is not https, the request times out, or the grant fails\n *\n * @example\n * ```typescript\n * const tokens = await clientCredentialsGrant({\n * tokenEndpoint: 'https://api.example.com/oauth2/token',\n * clientId: 'my-client-id',\n * clientSecret: 'my-client-secret',\n * scope: 'api:read api:write',\n * });\n * ```\n */\nexport async function clientCredentialsGrant(\n config: ClientCredentialsConfig,\n): Promise<openid.TokenEndpointResponse> {\n try {\n // Build the configuration from metadata rather than discovering it: callers\n // give us a token endpoint, not an issuer. Going through openid-client anyway\n // buys the https guard and the timeout, which a raw fetch of a caller-supplied\n // URL carrying a client secret would not have.\n const openidConfig = new openid.Configuration(\n {\n issuer: config.issuer ?? new URL(config.tokenEndpoint).origin,\n token_endpoint: config.tokenEndpoint,\n },\n config.clientId,\n config.clientSecret,\n // Explicit rather than relying on openid-client's default, matching the\n // discovery calls above: the JSDoc promises client_secret_post.\n openid.ClientSecretPost(config.clientSecret),\n );\n openidConfig.timeout = config.timeout ?? 30;\n\n const tokens = await openid.clientCredentialsGrant(\n openidConfig,\n config.scope ? { scope: config.scope } : undefined,\n );\n\n logger.debug({ tokenEndpoint: config.tokenEndpoint }, 'Client credentials grant successful');\n return tokens;\n } catch (error) {\n logger.error(\n { ...getOAuthErrorLogContext(error), tokenEndpoint: config.tokenEndpoint },\n 'Client credentials grant failed',\n );\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
@@ -108,7 +108,8 @@ var VippsLoginClient = class {
|
|
|
108
108
|
*/
|
|
109
109
|
async buildAuthorizationUrl(pkceOptions) {
|
|
110
110
|
logger.debug("Building authorization URL");
|
|
111
|
-
|
|
111
|
+
const config = await this.getConfig();
|
|
112
|
+
return buildAuthorizationUrl(config, pkceOptions);
|
|
112
113
|
}
|
|
113
114
|
/**
|
|
114
115
|
* Exchange authorization code for tokens
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n private configPromise: Promise<openid.Configuration> | null = null;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get OpenID Configuration (cached)\n * This ensures we use the same Configuration instance across all operations\n */\n private async getConfig(): Promise<openid.Configuration> {\n if (!this.configPromise) {\n this.configPromise = openid.discovery(\n new URL(this.oauthConfig.issuer),\n this.oauthConfig.clientId,\n this.oauthConfig.clientSecret,\n openid.ClientSecretPost(this.oauthConfig.clientSecret)\n );\n }\n return this.configPromise;\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n private configPromise: Promise<openid.Configuration> | null = null;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get OpenID Configuration (cached)\n * This ensures we use the same Configuration instance across all operations\n */\n private async getConfig(): Promise<openid.Configuration> {\n if (!this.configPromise) {\n this.configPromise = openid.discovery(\n new URL(this.oauthConfig.issuer),\n this.oauthConfig.clientId,\n this.oauthConfig.clientSecret,\n openid.ClientSecretPost(this.oauthConfig.clientSecret)\n );\n }\n return this.configPromise;\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CookieOptions } from '../cookies';
|
|
2
|
+
export interface CookieStore {
|
|
3
|
+
/** Returns the cookie's value, or null/undefined when it is not set. */
|
|
4
|
+
get(name: string): string | null | undefined | Promise<string | null | undefined>;
|
|
5
|
+
/** Sets a cookie. Adapters apply their own defaults for any omitted options. */
|
|
6
|
+
set(name: string, value: string, options?: CookieOptions): void | Promise<void>;
|
|
7
|
+
/** Deletes a cookie by name. */
|
|
8
|
+
delete(name: string): void | Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=cookie-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cookie-store.d.ts","sourceRoot":"","sources":["../../src/server/cookie-store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IAClF,gFAAgF;IAChF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChF,gCAAgC;IAChC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5C"}
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { OAuthConfig } from '../oauth';
|
|
2
|
+
import { CookieStore } from './cookie-store';
|
|
3
|
+
export interface HeartbeatHandlerConfig {
|
|
4
|
+
/** OAuth/OIDC configuration used to refresh the access token. */
|
|
5
|
+
oauthConfig: OAuthConfig;
|
|
6
|
+
/** Cookie store holding the session. */
|
|
7
|
+
cookies: CookieStore;
|
|
8
|
+
/** Optional rate-limit gate. When it resolves false, the handler responds 429. */
|
|
9
|
+
rateLimit?: () => boolean | Promise<boolean>;
|
|
10
|
+
/** Session encryption secret. Defaults to {@link getSessionSecret}. */
|
|
11
|
+
secret?: string | Uint8Array;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Handles a heartbeat request — refresh the active session if there is one.
|
|
15
|
+
*
|
|
16
|
+
* Returns 200 with `{ accessTokenExpiresAt }` on success, 401 when there is no
|
|
17
|
+
* session or the refresh token is dead, 405 for non-POST methods, and 429 when
|
|
18
|
+
* rate-limited.
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleHeartbeat(request: Request, config: HeartbeatHandlerConfig): Promise<Response>;
|
|
21
|
+
//# sourceMappingURL=heartbeat-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat-handler.d.ts","sourceRoot":"","sources":["../../src/server/heartbeat-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKlD,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,WAAW,EAAE,WAAW,CAAC;IAEzB,wCAAwC;IACxC,OAAO,EAAE,WAAW,CAAC;IAErB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,QAAQ,CAAC,CAwCnB"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { CookieStore } from './cookie-store';
|
|
2
|
+
export { persistSession, readSession, refreshSessionInStore, readIdToken, clearSession, } from './session';
|
|
3
|
+
export { handleOidcLogin, type OidcLoginConfig } from './oidc-login';
|
|
4
|
+
export { handleOidcCallback, type OidcCallbackConfig } from './oidc-callback';
|
|
5
|
+
export { handleOidcLogout, type OidcLogoutConfig } from './oidc-logout';
|
|
6
|
+
export { handleHeartbeat, type HeartbeatHandlerConfig } from './heartbeat-handler';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,EACL,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,WAAW,EACX,YAAY,GACb,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,eAAe,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC"}
|