@eventuras/fides-auth 0.14.1 → 0.15.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.
Files changed (94) hide show
  1. package/README.md +225 -33
  2. package/dist/activity-tracker.js +1 -2
  3. package/dist/cookies.d.ts +6 -0
  4. package/dist/cookies.d.ts.map +1 -1
  5. package/dist/cookies.js +8 -3
  6. package/dist/cookies.js.map +1 -1
  7. package/dist/heartbeat.js +1 -2
  8. package/dist/id-token.d.ts +31 -0
  9. package/dist/id-token.d.ts.map +1 -0
  10. package/dist/id-token.js +60 -0
  11. package/dist/id-token.js.map +1 -0
  12. package/dist/index.js +3 -3
  13. package/dist/logger.js +1 -2
  14. package/dist/oauth-browser.d.ts +13 -10
  15. package/dist/oauth-browser.d.ts.map +1 -1
  16. package/dist/oauth-browser.js +4 -2
  17. package/dist/oauth-browser.js.map +1 -1
  18. package/dist/oauth-config.d.ts +70 -0
  19. package/dist/oauth-config.d.ts.map +1 -0
  20. package/dist/oauth-logging.d.ts +4 -3
  21. package/dist/oauth-logging.d.ts.map +1 -1
  22. package/dist/oauth-logging.js +5 -5
  23. package/dist/oauth-logging.js.map +1 -1
  24. package/dist/oauth.d.ts +47 -45
  25. package/dist/oauth.d.ts.map +1 -1
  26. package/dist/oauth.js +58 -29
  27. package/dist/oauth.js.map +1 -1
  28. package/dist/oidc-discovery.d.ts +13 -4
  29. package/dist/oidc-discovery.d.ts.map +1 -1
  30. package/dist/oidc-discovery.js +82 -15
  31. package/dist/oidc-discovery.js.map +1 -1
  32. package/dist/pkce.d.ts +10 -0
  33. package/dist/pkce.d.ts.map +1 -0
  34. package/dist/pkce.js +23 -0
  35. package/dist/pkce.js.map +1 -0
  36. package/dist/providers/vipps/client.d.ts +3 -2
  37. package/dist/providers/vipps/client.d.ts.map +1 -1
  38. package/dist/providers/vipps/index.js +10 -10
  39. package/dist/providers/vipps/index.js.map +1 -1
  40. package/dist/providers/vipps/types.d.ts +2 -2
  41. package/dist/providers/vipps/types.d.ts.map +1 -1
  42. package/dist/rate-limit.js +1 -2
  43. package/dist/server/authorized-fetch.d.ts +78 -0
  44. package/dist/server/authorized-fetch.d.ts.map +1 -0
  45. package/dist/server/heartbeat-handler.d.ts +6 -4
  46. package/dist/server/heartbeat-handler.d.ts.map +1 -1
  47. package/dist/server/index.d.ts +4 -0
  48. package/dist/server/index.d.ts.map +1 -1
  49. package/dist/server/index.js +744 -94
  50. package/dist/server/index.js.map +1 -1
  51. package/dist/server/login-transaction.d.ts +55 -0
  52. package/dist/server/login-transaction.d.ts.map +1 -0
  53. package/dist/server/oidc-callback.d.ts +13 -4
  54. package/dist/server/oidc-callback.d.ts.map +1 -1
  55. package/dist/server/oidc-login.d.ts +14 -5
  56. package/dist/server/oidc-login.d.ts.map +1 -1
  57. package/dist/server/oidc-logout.d.ts +6 -4
  58. package/dist/server/oidc-logout.d.ts.map +1 -1
  59. package/dist/server/session-persistence.d.ts +102 -0
  60. package/dist/server/session-persistence.d.ts.map +1 -0
  61. package/dist/server/session-store.d.ts +39 -0
  62. package/dist/server/session-store.d.ts.map +1 -0
  63. package/dist/server/session.d.ts +35 -28
  64. package/dist/server/session.d.ts.map +1 -1
  65. package/dist/session-cookies.d.ts +1 -0
  66. package/dist/session-cookies.d.ts.map +1 -1
  67. package/dist/session-cookies.js +4 -3
  68. package/dist/session-cookies.js.map +1 -1
  69. package/dist/session-events.d.ts +10 -3
  70. package/dist/session-events.d.ts.map +1 -1
  71. package/dist/session-events.js +4 -3
  72. package/dist/session-events.js.map +1 -1
  73. package/dist/session-refresh.d.ts +1 -1
  74. package/dist/session-refresh.d.ts.map +1 -1
  75. package/dist/session-refresh.js +1 -2
  76. package/dist/session-refresh.js.map +1 -1
  77. package/dist/session-validation.d.ts.map +1 -1
  78. package/dist/session-validation.js +9 -6
  79. package/dist/session-validation.js.map +1 -1
  80. package/dist/silent-login.d.ts +1 -1
  81. package/dist/silent-login.d.ts.map +1 -1
  82. package/dist/silent-login.js +3 -3
  83. package/dist/silent-login.js.map +1 -1
  84. package/dist/types.d.ts +4 -1
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/utils.d.ts +0 -14
  87. package/dist/utils.d.ts.map +1 -1
  88. package/dist/utils.js +2 -24
  89. package/dist/utils.js.map +1 -1
  90. package/docs/discovery-cache.md +7 -3
  91. package/docs/rp-initiated-logout.md +6 -3
  92. package/docs/server-sessions.md +154 -0
  93. package/docs/session-events.md +17 -16
  94. package/package.json +14 -6
@@ -1,4 +1,4 @@
1
- //#region src/oauth-browser.ts
1
+ // src/oauth-browser.ts
2
2
  /**
3
3
  * Generate random string for code verifier (PKCE)
4
4
  * Uses Web Crypto API (browser-compatible)
@@ -20,6 +20,7 @@ async function generateCodeChallenge(verifier) {
20
20
  }
21
21
  /**
22
22
  * Generate PKCE parameters for OAuth flow
23
+ * @deprecated The random strings have modulo bias. Use `handleOidcLogin`, which builds PKCE server-side.
23
24
  */
24
25
  async function generatePKCE() {
25
26
  const code_verifier = generateRandomString(128);
@@ -31,6 +32,7 @@ async function generatePKCE() {
31
32
  }
32
33
  /**
33
34
  * Build authorization URL for OAuth flow
35
+ * @deprecated Hardcodes `${issuer}/auth` instead of using discovery. Use `handleOidcLogin`.
34
36
  */
35
37
  function buildAuthorizationUrl(config, pkce) {
36
38
  const params = new URLSearchParams({
@@ -46,6 +48,7 @@ function buildAuthorizationUrl(config, pkce) {
46
48
  }
47
49
  /**
48
50
  * Exchange authorization code for tokens
51
+ * @deprecated Hardcodes `${issuer}/token` and doesn't validate the response. Use `handleOidcCallback`.
49
52
  */
50
53
  async function exchangeCodeForTokens(config, code, codeVerifier) {
51
54
  const response = await fetch(`${config.issuer}/token`, {
@@ -65,7 +68,6 @@ async function exchangeCodeForTokens(config, code, codeVerifier) {
65
68
  }
66
69
  return response.json();
67
70
  }
68
- //#endregion
69
71
  export { buildAuthorizationUrl, exchangeCodeForTokens, generatePKCE };
70
72
 
71
73
  //# sourceMappingURL=oauth-browser.js.map
@@ -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,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"}
1
+ {"version":3,"file":"oauth-browser.js","names":[],"sources":["../src/oauth-browser.ts"],"sourcesContent":["/**\n * Browser-compatible OAuth 2.0 + PKCE helpers\n *\n * Deprecated and will be removed in a later minor. Use the server flow instead:\n * `handleOidcLogin` / `handleOidcCallback` from `@eventuras/fides-auth/server`.\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/**\n * Configuration for browser-side OAuth / OIDC flows (no client secret).\n * @deprecated Only used by the deprecated `oauth-browser` helpers. Use `handleOidcLogin` / `handleOidcCallback`.\n */\nexport interface OAuthConfig {\n issuer: string;\n clientId: string;\n redirect_uri: string;\n scope: string;\n}\n\n/**\n * PKCE parameters for a browser-initiated authorization request.\n * @deprecated Only used by the deprecated `oauth-browser` helpers. Use `handleOidcLogin`.\n */\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 * @deprecated The random strings have modulo bias. Use `handleOidcLogin`, which builds PKCE server-side.\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 * @deprecated Hardcodes `${issuer}/auth` instead of using discovery. Use `handleOidcLogin`.\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 * @deprecated Hardcodes `${issuer}/token` and doesn't validate the response. Use `handleOidcCallback`.\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;;;;;AAMA,eAAsB,eAAoC;CACxD,MAAM,gBAAgB,qBAAqB,GAAG;CAI9C,OAAO;EACL;EACA,gBAAA,MAL2B,sBAAsB,aAAa;EAM9D,OALY,qBAAqB,EAKjC;CACF;AACF;;;;;AAMA,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;;;;;AAMA,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"}
@@ -0,0 +1,70 @@
1
+ import type * as openid from 'openid-client';
2
+ /**
3
+ * How the client authenticates at the token endpoint. Omit `clientAuth` to use
4
+ * `client_secret_post`.
5
+ */
6
+ export type ClientAuthConfig = {
7
+ /** @default 'client_secret_post' */
8
+ clientAuth?: 'client_secret_post' | 'client_secret_basic';
9
+ clientSecret: string;
10
+ } | {
11
+ /** Public client: sends only `client_id`. Pair it with PKCE. */
12
+ clientAuth: 'none';
13
+ } | {
14
+ clientAuth: 'private_key_jwt';
15
+ /** Signs the client assertion. Reuse one key object: a new one means a new cache entry. */
16
+ privateKey: openid.CryptoKey | openid.PrivateKey;
17
+ };
18
+ /** Configuration for server-side OAuth / OIDC flows. */
19
+ export type OAuthConfig = ClientAuthConfig & {
20
+ issuer: string;
21
+ clientId: string;
22
+ redirect_uri: string;
23
+ /** Space-separated string or an array of scopes. */
24
+ scope: string | string[];
25
+ /**
26
+ * Extra authorization request parameters (e.g. `prompt`, `resource`), added by `buildPKCEOptions`.
27
+ * Reserved keys throw: `state`, `code_challenge`, `code_challenge_method`, `redirect_uri`,
28
+ * `client_id`, `response_type`, `request`, `request_uri` and `nonce`.
29
+ */
30
+ authorizationParameters?: Record<string, string>;
31
+ /**
32
+ * Opt in to Pushed Authorization Requests (RFC 9126) when the provider
33
+ * supports it. When true, authorization parameters are posted to the
34
+ * provider's PAR endpoint ahead of the redirect, and only `client_id` +
35
+ * `request_uri` travel through the user-agent. The provider must advertise
36
+ * `pushed_authorization_request_endpoint` in its discovery metadata.
37
+ */
38
+ usePar?: boolean;
39
+ /**
40
+ * Server metadata to use instead of OIDC discovery, e.g. an RFC 8414 document.
41
+ * Must include `issuer` when `scope` has `openid`. `issuer` above is then used
42
+ * only for logs and caching.
43
+ */
44
+ serverMetadata?: openid.ServerMetadata;
45
+ };
46
+ /**
47
+ * Configuration for OAuth client credentials flow (machine-to-machine authentication).
48
+ * Takes any {@link ClientAuthConfig} except `none`: the grant needs client authentication.
49
+ */
50
+ export type ClientCredentialsConfig = Exclude<ClientAuthConfig, {
51
+ clientAuth: 'none';
52
+ }> & {
53
+ /** OAuth token endpoint URL. Must be https. */
54
+ tokenEndpoint: string;
55
+ /** OAuth client ID */
56
+ clientId: string;
57
+ /** Optional scope for the access token */
58
+ scope?: string;
59
+ /**
60
+ * Issuer identifier; defaults to the token endpoint's origin. With
61
+ * `private_key_jwt` it is the client assertion's `aud`, so set it to what the server expects.
62
+ */
63
+ issuer?: string;
64
+ /**
65
+ * Request timeout in seconds.
66
+ * @default 30
67
+ */
68
+ timeout?: number;
69
+ };
70
+ //# sourceMappingURL=oauth-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-config.d.ts","sourceRoot":"","sources":["../src/oauth-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAC;AAE7C;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GACxB;IACE,oCAAoC;IACpC,UAAU,CAAC,EAAE,oBAAoB,GAAG,qBAAqB,CAAC;IAC1D,YAAY,EAAE,MAAM,CAAC;CACtB,GACD;IACE,gEAAgE;IAChE,UAAU,EAAE,MAAM,CAAC;CACpB,GACD;IACE,UAAU,EAAE,iBAAiB,CAAC;IAC9B,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC;CAClD,CAAC;AAEN,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;CACxC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,uBAAuB,GAAG,OAAO,CAAC,gBAAgB,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG;IACxF,+CAA+C;IAC/C,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC"}
@@ -1,10 +1,11 @@
1
- import type { OAuthConfig } from './oauth';
1
+ import type { OAuthConfig } from './oauth-config';
2
2
  import type { RefreshFailureCause } from './session-events';
3
3
  /**
4
4
  * Returns a structured, log-safe context describing an `OAuthConfig`.
5
5
  *
6
- * Excludes `clientSecret`. Use this to attach provider configuration to log
7
- * lines for debugging OAuth/OIDC flows without leaking credentials.
6
+ * Excludes credentials (`clientSecret`, `privateKey`). Use this to attach
7
+ * provider configuration to log lines for debugging OAuth/OIDC flows without
8
+ * leaking credentials.
8
9
  */
9
10
  export declare function getOAuthConfigLogContext(config: OAuthConfig): {
10
11
  clientId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-logging.d.ts","sourceRoot":"","sources":["../src/oauth-logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,WAAW;IAExD,QAAQ;IACR,MAAM;IACN,WAAW;IACX,KAAK;IACL,MAAM;EAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;EA0BrD;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAY1E"}
1
+ {"version":3,"file":"oauth-logging.d.ts","sourceRoot":"","sources":["../src/oauth-logging.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE5D;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,WAAW;IAExD,QAAQ;IACR,MAAM;IACN,WAAW;IACX,KAAK;IACL,MAAM;EAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;EA0BrD;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,CAY1E"}
@@ -1,16 +1,17 @@
1
- //#region src/oauth-logging.ts
1
+ // src/oauth-logging.ts
2
2
  /**
3
3
  * Returns a structured, log-safe context describing an `OAuthConfig`.
4
4
  *
5
- * Excludes `clientSecret`. Use this to attach provider configuration to log
6
- * lines for debugging OAuth/OIDC flows without leaking credentials.
5
+ * Excludes credentials (`clientSecret`, `privateKey`). Use this to attach
6
+ * provider configuration to log lines for debugging OAuth/OIDC flows without
7
+ * leaking credentials.
7
8
  */
8
9
  function getOAuthConfigLogContext(config) {
9
10
  return {
10
11
  clientId: config.clientId,
11
12
  issuer: config.issuer,
12
13
  redirectUri: config.redirect_uri,
13
- scope: config.scope,
14
+ scope: Array.isArray(config.scope) ? config.scope.join(" ") : config.scope,
14
15
  usePar: config.usePar ?? false
15
16
  };
16
17
  }
@@ -106,7 +107,6 @@ function syscallCode(error) {
106
107
  const cause = typeof candidate.cause === "object" && candidate.cause !== null ? candidate.cause : null;
107
108
  return cause ? stringProp(cause, "code") : void 0;
108
109
  }
109
- //#endregion
110
110
  export { classifyRefreshFailure, getOAuthConfigLogContext, getOAuthErrorLogContext };
111
111
 
112
112
  //# sourceMappingURL=oauth-logging.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-logging.js","names":[],"sources":["../src/oauth-logging.ts"],"sourcesContent":["import type { OAuthConfig } from './oauth';\nimport type { RefreshFailureCause } from './session-events';\n\n/**\n * Returns a structured, log-safe context describing an `OAuthConfig`.\n *\n * Excludes `clientSecret`. Use this to attach provider configuration to log\n * lines for debugging OAuth/OIDC flows without leaking credentials.\n */\nexport function getOAuthConfigLogContext(config: OAuthConfig) {\n return {\n clientId: config.clientId,\n issuer: config.issuer,\n redirectUri: config.redirect_uri,\n scope: config.scope,\n usePar: config.usePar ?? false,\n };\n}\n\n/**\n * Returns a structured log context for an unknown OAuth-related error.\n *\n * `oauth4webapi` (used via `openid-client`) wraps OAuth response errors. The\n * original OAuth response (`{error, error_description, ...}`) usually lives\n * either on the error itself or on `cause`.\n *\n * @see https://github.com/panva/oauth4webapi/blob/main/docs/classes/ResponseBodyError.md\n */\nexport function getOAuthErrorLogContext(error: unknown) {\n if (typeof error !== 'object' || error === null) {\n return { value: error };\n }\n\n const candidate = error as Record<string, unknown>;\n const cause =\n typeof candidate.cause === 'object' && candidate.cause !== null\n ? (candidate.cause as Record<string, unknown>)\n : null;\n\n // OAuth-specific fields (code/error/error_description) may live either on\n // the error itself or on `cause` depending on how oauth4webapi wraps the\n // response. Prefer the top-level value but fall back to `cause`.\n return {\n code: stringProp(candidate, 'code') ?? (cause ? stringProp(cause, 'code') : undefined),\n error: stringProp(candidate, 'error') ?? (cause ? stringProp(cause, 'error') : undefined),\n errorDescription:\n stringProp(candidate, 'error_description') ??\n (cause ? stringProp(cause, 'error_description') : undefined),\n message: stringProp(candidate, 'message'),\n name: stringProp(candidate, 'name'),\n status: numberProp(candidate, 'status'),\n causeMessage: cause ? stringProp(cause, 'message') : undefined,\n causeName: cause ? stringProp(cause, 'name') : undefined,\n };\n}\n\nfunction stringProp(value: Record<string, unknown>, key: string): string | undefined {\n return typeof value[key] === 'string' ? (value[key] as string) : undefined;\n}\n\nfunction numberProp(value: Record<string, unknown>, key: string): number | undefined {\n return typeof value[key] === 'number' ? (value[key] as number) : undefined;\n}\n\n/**\n * Node/undici and browser fetch surface connectivity failures as syscall codes on\n * `cause`. Any of these means the request never reached the provider.\n */\nconst TRANSPORT_CODES = new Set([\n 'ECONNREFUSED',\n 'ECONNRESET',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ETIMEDOUT',\n 'EHOSTUNREACH',\n 'ENETUNREACH',\n 'EPIPE',\n 'EPROTO',\n 'CERT_HAS_EXPIRED',\n 'DEPTH_ZERO_SELF_SIGNED_CERT',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\n/** `fetch` rejects with a bare TypeError on network failure; the wording is runtime-specific. */\nconst TRANSPORT_MESSAGE = /fetch failed|failed to fetch|network(?:\\s|error)|load failed|socket hang up/i;\n\n/**\n * Classifies a refresh failure into the three causes that call for different\n * responses.\n *\n * The distinction is not cosmetic: `invalid_grant` is the provider stating the\n * refresh token is dead, and the user genuinely has to log in again. A transport\n * fault or a provider 5xx says nothing about the session — treating those the\n * same way logs out a user whose session was perfectly valid, which is exactly\n * the bug that makes \"logged out every few minutes\" so hard to diagnose.\n *\n * Unknown errors fall back to `idp_error` rather than `invalid_grant`, so an\n * unrecognised failure never silently ends a session.\n */\nexport function classifyRefreshFailure(error: unknown): RefreshFailureCause {\n const context = getOAuthErrorLogContext(error);\n\n if (context.code === 'OAUTH_RESPONSE_BODY_ERROR' && context.error === 'invalid_grant') {\n return 'invalid_grant';\n }\n\n if (isTransportFailure(error, context)) {\n return 'transport';\n }\n\n return 'idp_error';\n}\n\nfunction isTransportFailure(\n error: unknown,\n context: ReturnType<typeof getOAuthErrorLogContext>,\n): boolean {\n // An aborted or timed-out request never got an answer.\n if (context.name === 'AbortError' || context.name === 'TimeoutError') return true;\n if (context.causeName === 'AbortError' || context.causeName === 'TimeoutError') return true;\n\n const code = syscallCode(error);\n if (code !== undefined && TRANSPORT_CODES.has(code)) return true;\n\n // Only trust the message when the error is a bare TypeError — the shape fetch\n // uses for connectivity failures. A provider error that happens to mention a\n // network is still a provider error.\n if (context.name === 'TypeError') {\n if (context.message && TRANSPORT_MESSAGE.test(context.message)) return true;\n if (context.causeMessage && TRANSPORT_MESSAGE.test(context.causeMessage)) return true;\n }\n\n return false;\n}\n\n/** Reads a syscall code off the error or its `cause`, where fetch buries connectivity failures. */\nfunction syscallCode(error: unknown): string | undefined {\n if (typeof error !== 'object' || error === null) return undefined;\n const candidate = error as Record<string, unknown>;\n\n const own = stringProp(candidate, 'code');\n if (own !== undefined && own !== 'OAUTH_RESPONSE_BODY_ERROR') return own;\n\n const cause =\n typeof candidate.cause === 'object' && candidate.cause !== null\n ? (candidate.cause as Record<string, unknown>)\n : null;\n return cause ? stringProp(cause, 'code') : undefined;\n}\n"],"mappings":";;;;;;;AASA,SAAgB,yBAAyB,QAAqB;CAC5D,OAAO;EACL,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,aAAa,OAAO;EACpB,OAAO,OAAO;EACd,QAAQ,OAAO,UAAU;CAC3B;AACF;;;;;;;;;;AAWA,SAAgB,wBAAwB,OAAgB;CACtD,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,OAAO,MAAM;CAGxB,MAAM,YAAY;CAClB,MAAM,QACJ,OAAO,UAAU,UAAU,YAAY,UAAU,UAAU,OACtD,UAAU,QACX;CAKN,OAAO;EACL,MAAM,WAAW,WAAW,MAAM,MAAM,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;EAC5E,OAAO,WAAW,WAAW,OAAO,MAAM,QAAQ,WAAW,OAAO,OAAO,IAAI,KAAA;EAC/E,kBACE,WAAW,WAAW,mBAAmB,MACxC,QAAQ,WAAW,OAAO,mBAAmB,IAAI,KAAA;EACpD,SAAS,WAAW,WAAW,SAAS;EACxC,MAAM,WAAW,WAAW,MAAM;EAClC,QAAQ,WAAW,WAAW,QAAQ;EACtC,cAAc,QAAQ,WAAW,OAAO,SAAS,IAAI,KAAA;EACrD,WAAW,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;CACjD;AACF;AAEA,SAAS,WAAW,OAAgC,KAAiC;CACnF,OAAO,OAAO,MAAM,SAAS,WAAY,MAAM,OAAkB,KAAA;AACnE;AAEA,SAAS,WAAW,OAAgC,KAAiC;CACnF,OAAO,OAAO,MAAM,SAAS,WAAY,MAAM,OAAkB,KAAA;AACnE;;;;;AAMA,IAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,IAAM,oBAAoB;;;;;;;;;;;;;;AAe1B,SAAgB,uBAAuB,OAAqC;CAC1E,MAAM,UAAU,wBAAwB,KAAK;CAE7C,IAAI,QAAQ,SAAS,+BAA+B,QAAQ,UAAU,iBACpE,OAAO;CAGT,IAAI,mBAAmB,OAAO,OAAO,GACnC,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,mBACP,OACA,SACS;CAET,IAAI,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,gBAAgB,OAAO;CAC7E,IAAI,QAAQ,cAAc,gBAAgB,QAAQ,cAAc,gBAAgB,OAAO;CAEvF,MAAM,OAAO,YAAY,KAAK;CAC9B,IAAI,SAAS,KAAA,KAAa,gBAAgB,IAAI,IAAI,GAAG,OAAO;CAK5D,IAAI,QAAQ,SAAS,aAAa;EAChC,IAAI,QAAQ,WAAW,kBAAkB,KAAK,QAAQ,OAAO,GAAG,OAAO;EACvE,IAAI,QAAQ,gBAAgB,kBAAkB,KAAK,QAAQ,YAAY,GAAG,OAAO;CACnF;CAEA,OAAO;AACT;;AAGA,SAAS,YAAY,OAAoC;CACvD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,MAAM,YAAY;CAElB,MAAM,MAAM,WAAW,WAAW,MAAM;CACxC,IAAI,QAAQ,KAAA,KAAa,QAAQ,6BAA6B,OAAO;CAErE,MAAM,QACJ,OAAO,UAAU,UAAU,YAAY,UAAU,UAAU,OACtD,UAAU,QACX;CACN,OAAO,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;AAC7C"}
1
+ {"version":3,"file":"oauth-logging.js","names":[],"sources":["../src/oauth-logging.ts"],"sourcesContent":["import type { OAuthConfig } from './oauth-config';\nimport type { RefreshFailureCause } from './session-events';\n\n/**\n * Returns a structured, log-safe context describing an `OAuthConfig`.\n *\n * Excludes credentials (`clientSecret`, `privateKey`). Use this to attach\n * provider configuration to log lines for debugging OAuth/OIDC flows without\n * leaking credentials.\n */\nexport function getOAuthConfigLogContext(config: OAuthConfig) {\n return {\n clientId: config.clientId,\n issuer: config.issuer,\n redirectUri: config.redirect_uri,\n scope: Array.isArray(config.scope) ? config.scope.join(' ') : config.scope,\n usePar: config.usePar ?? false,\n };\n}\n\n/**\n * Returns a structured log context for an unknown OAuth-related error.\n *\n * `oauth4webapi` (used via `openid-client`) wraps OAuth response errors. The\n * original OAuth response (`{error, error_description, ...}`) usually lives\n * either on the error itself or on `cause`.\n *\n * @see https://github.com/panva/oauth4webapi/blob/main/docs/classes/ResponseBodyError.md\n */\nexport function getOAuthErrorLogContext(error: unknown) {\n if (typeof error !== 'object' || error === null) {\n return { value: error };\n }\n\n const candidate = error as Record<string, unknown>;\n const cause =\n typeof candidate.cause === 'object' && candidate.cause !== null\n ? (candidate.cause as Record<string, unknown>)\n : null;\n\n // OAuth-specific fields (code/error/error_description) may live either on\n // the error itself or on `cause` depending on how oauth4webapi wraps the\n // response. Prefer the top-level value but fall back to `cause`.\n return {\n code: stringProp(candidate, 'code') ?? (cause ? stringProp(cause, 'code') : undefined),\n error: stringProp(candidate, 'error') ?? (cause ? stringProp(cause, 'error') : undefined),\n errorDescription:\n stringProp(candidate, 'error_description') ??\n (cause ? stringProp(cause, 'error_description') : undefined),\n message: stringProp(candidate, 'message'),\n name: stringProp(candidate, 'name'),\n status: numberProp(candidate, 'status'),\n causeMessage: cause ? stringProp(cause, 'message') : undefined,\n causeName: cause ? stringProp(cause, 'name') : undefined,\n };\n}\n\nfunction stringProp(value: Record<string, unknown>, key: string): string | undefined {\n return typeof value[key] === 'string' ? (value[key] as string) : undefined;\n}\n\nfunction numberProp(value: Record<string, unknown>, key: string): number | undefined {\n return typeof value[key] === 'number' ? (value[key] as number) : undefined;\n}\n\n/**\n * Node/undici and browser fetch surface connectivity failures as syscall codes on\n * `cause`. Any of these means the request never reached the provider.\n */\nconst TRANSPORT_CODES = new Set([\n 'ECONNREFUSED',\n 'ECONNRESET',\n 'ENOTFOUND',\n 'EAI_AGAIN',\n 'ETIMEDOUT',\n 'EHOSTUNREACH',\n 'ENETUNREACH',\n 'EPIPE',\n 'EPROTO',\n 'CERT_HAS_EXPIRED',\n 'DEPTH_ZERO_SELF_SIGNED_CERT',\n 'UND_ERR_CONNECT_TIMEOUT',\n 'UND_ERR_HEADERS_TIMEOUT',\n 'UND_ERR_BODY_TIMEOUT',\n 'UND_ERR_SOCKET',\n]);\n\n/** `fetch` rejects with a bare TypeError on network failure; the wording is runtime-specific. */\nconst TRANSPORT_MESSAGE = /fetch failed|failed to fetch|network(?:\\s|error)|load failed|socket hang up/i;\n\n/**\n * Classifies a refresh failure into the three causes that call for different\n * responses.\n *\n * The distinction is not cosmetic: `invalid_grant` is the provider stating the\n * refresh token is dead, and the user genuinely has to log in again. A transport\n * fault or a provider 5xx says nothing about the session — treating those the\n * same way logs out a user whose session was perfectly valid, which is exactly\n * the bug that makes \"logged out every few minutes\" so hard to diagnose.\n *\n * Unknown errors fall back to `idp_error` rather than `invalid_grant`, so an\n * unrecognised failure never silently ends a session.\n */\nexport function classifyRefreshFailure(error: unknown): RefreshFailureCause {\n const context = getOAuthErrorLogContext(error);\n\n if (context.code === 'OAUTH_RESPONSE_BODY_ERROR' && context.error === 'invalid_grant') {\n return 'invalid_grant';\n }\n\n if (isTransportFailure(error, context)) {\n return 'transport';\n }\n\n return 'idp_error';\n}\n\nfunction isTransportFailure(\n error: unknown,\n context: ReturnType<typeof getOAuthErrorLogContext>,\n): boolean {\n // An aborted or timed-out request never got an answer.\n if (context.name === 'AbortError' || context.name === 'TimeoutError') return true;\n if (context.causeName === 'AbortError' || context.causeName === 'TimeoutError') return true;\n\n const code = syscallCode(error);\n if (code !== undefined && TRANSPORT_CODES.has(code)) return true;\n\n // Only trust the message when the error is a bare TypeError — the shape fetch\n // uses for connectivity failures. A provider error that happens to mention a\n // network is still a provider error.\n if (context.name === 'TypeError') {\n if (context.message && TRANSPORT_MESSAGE.test(context.message)) return true;\n if (context.causeMessage && TRANSPORT_MESSAGE.test(context.causeMessage)) return true;\n }\n\n return false;\n}\n\n/** Reads a syscall code off the error or its `cause`, where fetch buries connectivity failures. */\nfunction syscallCode(error: unknown): string | undefined {\n if (typeof error !== 'object' || error === null) return undefined;\n const candidate = error as Record<string, unknown>;\n\n const own = stringProp(candidate, 'code');\n if (own !== undefined && own !== 'OAUTH_RESPONSE_BODY_ERROR') return own;\n\n const cause =\n typeof candidate.cause === 'object' && candidate.cause !== null\n ? (candidate.cause as Record<string, unknown>)\n : null;\n return cause ? stringProp(cause, 'code') : undefined;\n}\n"],"mappings":";;;;;;;;AAUA,SAAgB,yBAAyB,QAAqB;CAC5D,OAAO;EACL,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,aAAa,OAAO;EACpB,OAAO,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,MAAM,KAAK,GAAG,IAAI,OAAO;EACrE,QAAQ,OAAO,UAAU;CAC3B;AACF;;;;;;;;;;AAWA,SAAgB,wBAAwB,OAAgB;CACtD,IAAI,OAAO,UAAU,YAAY,UAAU,MACzC,OAAO,EAAE,OAAO,MAAM;CAGxB,MAAM,YAAY;CAClB,MAAM,QACJ,OAAO,UAAU,UAAU,YAAY,UAAU,UAAU,OACtD,UAAU,QACX;CAKN,OAAO;EACL,MAAM,WAAW,WAAW,MAAM,MAAM,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;EAC5E,OAAO,WAAW,WAAW,OAAO,MAAM,QAAQ,WAAW,OAAO,OAAO,IAAI,KAAA;EAC/E,kBACE,WAAW,WAAW,mBAAmB,MACxC,QAAQ,WAAW,OAAO,mBAAmB,IAAI,KAAA;EACpD,SAAS,WAAW,WAAW,SAAS;EACxC,MAAM,WAAW,WAAW,MAAM;EAClC,QAAQ,WAAW,WAAW,QAAQ;EACtC,cAAc,QAAQ,WAAW,OAAO,SAAS,IAAI,KAAA;EACrD,WAAW,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;CACjD;AACF;AAEA,SAAS,WAAW,OAAgC,KAAiC;CACnF,OAAO,OAAO,MAAM,SAAS,WAAY,MAAM,OAAkB,KAAA;AACnE;AAEA,SAAS,WAAW,OAAgC,KAAiC;CACnF,OAAO,OAAO,MAAM,SAAS,WAAY,MAAM,OAAkB,KAAA;AACnE;;;;;AAMA,IAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,IAAM,oBAAoB;;;;;;;;;;;;;;AAe1B,SAAgB,uBAAuB,OAAqC;CAC1E,MAAM,UAAU,wBAAwB,KAAK;CAE7C,IAAI,QAAQ,SAAS,+BAA+B,QAAQ,UAAU,iBACpE,OAAO;CAGT,IAAI,mBAAmB,OAAO,OAAO,GACnC,OAAO;CAGT,OAAO;AACT;AAEA,SAAS,mBACP,OACA,SACS;CAET,IAAI,QAAQ,SAAS,gBAAgB,QAAQ,SAAS,gBAAgB,OAAO;CAC7E,IAAI,QAAQ,cAAc,gBAAgB,QAAQ,cAAc,gBAAgB,OAAO;CAEvF,MAAM,OAAO,YAAY,KAAK;CAC9B,IAAI,SAAS,KAAA,KAAa,gBAAgB,IAAI,IAAI,GAAG,OAAO;CAK5D,IAAI,QAAQ,SAAS,aAAa;EAChC,IAAI,QAAQ,WAAW,kBAAkB,KAAK,QAAQ,OAAO,GAAG,OAAO;EACvE,IAAI,QAAQ,gBAAgB,kBAAkB,KAAK,QAAQ,YAAY,GAAG,OAAO;CACnF;CAEA,OAAO;AACT;;AAGA,SAAS,YAAY,OAAoC;CACvD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO,KAAA;CACxD,MAAM,YAAY;CAElB,MAAM,MAAM,WAAW,WAAW,MAAM;CACxC,IAAI,QAAQ,KAAA,KAAa,QAAQ,6BAA6B,OAAO;CAErE,MAAM,QACJ,OAAO,UAAU,UAAU,YAAY,UAAU,UAAU,OACtD,UAAU,QACX;CACN,OAAO,QAAQ,WAAW,OAAO,MAAM,IAAI,KAAA;AAC7C"}
package/dist/oauth.d.ts CHANGED
@@ -1,24 +1,13 @@
1
+ import { type JWTPayload } from 'jose';
1
2
  import * as openid from 'openid-client';
3
+ import type { ClientAuthConfig, ClientCredentialsConfig, OAuthConfig } from './oauth-config.js';
2
4
  import type { Session } from './types';
3
5
  export type { Configuration, TokenEndpointResponse } from 'openid-client';
6
+ export type { ClientAuthConfig, ClientCredentialsConfig, OAuthConfig };
4
7
  /** Default OIDC scope requesting user identity, profile, email, and offline access. */
5
8
  export declare const defaultScope = "openid profile email offline_access";
6
- /** Configuration for server-side OAuth / OIDC flows. */
7
- export type OAuthConfig = {
8
- issuer: string;
9
- clientId: string;
10
- clientSecret: string;
11
- redirect_uri: string;
12
- scope: string;
13
- /**
14
- * Opt in to Pushed Authorization Requests (RFC 9126) when the provider
15
- * supports it. When true, authorization parameters are posted to the
16
- * provider's PAR endpoint ahead of the redirect, and only `client_id` +
17
- * `request_uri` travel through the user-agent. The provider must advertise
18
- * `pushed_authorization_request_endpoint` in its discovery metadata.
19
- */
20
- usePar?: boolean;
21
- };
9
+ /** Returns a scope as the space-separated string sent on the wire. */
10
+ export declare function joinScope(scope: string | string[]): string;
22
11
  /**
23
12
  * Refreshes an access token using a refresh token grant.
24
13
  *
@@ -34,17 +23,26 @@ export interface PKCEOptions {
34
23
  state: string;
35
24
  parameters: Record<string, string>;
36
25
  }
26
+ /** Per-request options for {@link buildPKCEOptions}. */
27
+ export interface AuthorizationRequestOptions {
28
+ /** Extra authorization parameters, merged over `OAuthConfig.authorizationParameters`. */
29
+ parameters?: Record<string, string>;
30
+ }
37
31
  /**
38
32
  * Creates PKCE parameters for an OAuth flow.
33
+ * For the verifier, challenge or state alone, use `createPkcePair` / `createState` from `/pkce`.
34
+ * Adds `config.authorizationParameters` and `options.parameters`; a `scope` among them wins.
39
35
  *
40
36
  * @param config - OAuthConfig configuration including the redirect URI and scope.
37
+ * @param options - Per-request extra parameters.
41
38
  * @returns A promise that resolves to an object containing:
42
39
  * - code_verifier: The randomly generated PKCE code verifier.
43
40
  * - code_challenge: The derived PKCE code challenge.
44
41
  * - state: A random state to protect against CSRF.
45
42
  * - parameters: A Record<string,string> with the assembled parameters.
43
+ * @throws Error if an extra parameter is a reserved key (see `OAuthConfig.authorizationParameters`).
46
44
  */
47
- export declare function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions>;
45
+ export declare function buildPKCEOptions(config: OAuthConfig, options?: AuthorizationRequestOptions): Promise<PKCEOptions>;
48
46
  /**
49
47
  * Builds an authorization URL with PKCE parameters.
50
48
  *
@@ -81,29 +79,6 @@ export declare function buildAuthorizationUrlWithPAR(config: openid.Configuratio
81
79
  * @returns A Promise that resolves to the authorization URL
82
80
  */
83
81
  export declare function discoverAndBuildAuthorizationUrl(oauthConfig: OAuthConfig, pkceOptions: PKCEOptions): Promise<URL>;
84
- /**
85
- * Configuration for OAuth client credentials flow (machine-to-machine authentication)
86
- */
87
- export type ClientCredentialsConfig = {
88
- /** OAuth token endpoint URL. Must be https. */
89
- tokenEndpoint: string;
90
- /** OAuth client ID */
91
- clientId: string;
92
- /** OAuth client secret */
93
- clientSecret: string;
94
- /** Optional scope for the access token */
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;
106
- };
107
82
  /**
108
83
  * Exchanges an authorization code for tokens using OIDC discovery and PKCE.
109
84
  *
@@ -127,19 +102,46 @@ export interface OidcUserInfo {
127
102
  * Extracts user information from an OIDC token response.
128
103
  * Decodes the ID token and extracts standard claims plus roles from a configurable claim.
129
104
  *
105
+ * Decodes only: the signature, `iss`, `aud` and `exp` are not checked. Safe on tokens
106
+ * straight from `exchangeAuthorizationCode`; verify any others first (e.g. jose `jwtVerify`).
107
+ *
130
108
  * @param tokens - Token response from the OIDC provider
131
109
  * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
132
110
  * @returns Extracted user information
133
111
  */
134
112
  export declare function extractUserFromTokens(tokens: openid.TokenEndpointResponse, rolesClaim?: string): OidcUserInfo;
113
+ /**
114
+ * Changes the session built from a token response, e.g. to add `data` or custom user
115
+ * fields. `claims` are decoded from the ID token, not verified, and undefined without one.
116
+ */
117
+ export type MapSession = (tokens: openid.TokenEndpointResponse, claims: JWTPayload | undefined, session: Session) => Session;
118
+ /** Options for {@link buildSessionFromTokens}. */
119
+ export interface BuildSessionOptions {
120
+ /**
121
+ * Name of the ID token claim that contains user roles.
122
+ * @default 'roles'
123
+ */
124
+ rolesClaim?: string;
125
+ /** Changes the session before it is returned. */
126
+ mapSession?: MapSession;
127
+ }
135
128
  /**
136
129
  * Builds a Session object from an OIDC token response.
137
- * Maps provider tokens and user claims into the internal Session format.
130
+ * Maps provider tokens and user claims into the internal Session format. Without an
131
+ * `id_token` the session has no `user`.
132
+ *
133
+ * Decodes the ID token like `extractUserFromTokens`, without verifying it. Pass tokens
134
+ * straight from `exchangeAuthorizationCode`, or verify them first.
138
135
  *
139
136
  * @param tokens - Token response from the OIDC provider
140
- * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
137
+ * @param options - `rolesClaim` and `mapSession`
141
138
  * @returns A Session object ready to be encrypted and stored
142
139
  */
140
+ export declare function buildSessionFromTokens(tokens: openid.TokenEndpointResponse, options?: BuildSessionOptions): Session;
141
+ /**
142
+ * @param tokens - Token response from the OIDC provider
143
+ * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
144
+ */
143
145
  export declare function buildSessionFromTokens(tokens: openid.TokenEndpointResponse, rolesClaim?: string): Session;
144
146
  /**
145
147
  * Validates a return URL against open redirect attacks.
@@ -197,10 +199,10 @@ export declare function buildOidcLogoutUrl(oauthConfig: OAuthConfig, options: Oi
197
199
  /**
198
200
  * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
199
201
  *
200
- * Uses client_secret_post against the given token endpoint. The endpoint must be
201
- * https — the request carries the client secret.
202
+ * Authenticates with `config.clientAuth` (default client_secret_post). The
203
+ * endpoint must be https — the request carries the client credentials.
202
204
  *
203
- * @param config - Client credentials configuration including token endpoint, client ID, and secret
205
+ * @param config - Client credentials configuration: token endpoint, client ID and credentials
204
206
  * @returns Token response from the OAuth provider
205
207
  * @throws Error if the endpoint is not https, the request times out, or the grant fails
206
208
  *
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
package/dist/oauth.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import { createLogger } from "./logger.js";
2
2
  import { getOAuthErrorLogContext } from "./oauth-logging.js";
3
3
  import { newSessionId } from "./session-events.js";
4
- import { discoverConfiguration } from "./oidc-discovery.js";
4
+ import { discoverConfiguration, resolveClientAuth } from "./oidc-discovery.js";
5
+ import { createPkcePair, createState } from "./pkce.js";
5
6
  import { decodeJwt } from "jose";
6
7
  import * as openid from "openid-client";
7
- //#region src/oauth.ts
8
+ // src/oauth.ts
8
9
  var logger = createLogger({ namespace: "fides-auth:oauth" });
9
10
  /** Default OIDC scope requesting user identity, profile, email, and offline access. */
10
11
  var defaultScope = "openid profile email offline_access";
12
+ /** Returns a scope as the space-separated string sent on the wire. */
13
+ function joinScope(scope) {
14
+ return Array.isArray(scope) ? scope.join(" ") : scope;
15
+ }
11
16
  /**
12
17
  * Refreshes an access token using a refresh token grant.
13
18
  *
@@ -19,7 +24,7 @@ async function refreshAccessToken(oAuthConfig, refreshToken) {
19
24
  logger.debug({ issuer: oAuthConfig.issuer }, "Starting token refresh");
20
25
  try {
21
26
  const config = await discoverConfiguration(oAuthConfig);
22
- const tokens = await openid.refreshTokenGrant(config, refreshToken, { scope: oAuthConfig.scope });
27
+ const tokens = await openid.refreshTokenGrant(config, refreshToken, { scope: joinScope(oAuthConfig.scope) });
23
28
  logger.info({
24
29
  hasAccessToken: !!tokens.access_token,
25
30
  hasRefreshToken: !!tokens.refresh_token,
@@ -39,30 +44,53 @@ async function refreshAccessToken(oAuthConfig, refreshToken) {
39
44
  throw error;
40
45
  }
41
46
  }
47
+ /** Extra parameters can't set these. */
48
+ var RESERVED_AUTHORIZATION_PARAMETERS = /* @__PURE__ */ new Set([
49
+ "state",
50
+ "code_challenge",
51
+ "code_challenge_method",
52
+ "redirect_uri",
53
+ "client_id",
54
+ "response_type",
55
+ "request",
56
+ "request_uri",
57
+ "nonce"
58
+ ]);
42
59
  /**
43
60
  * Creates PKCE parameters for an OAuth flow.
61
+ * For the verifier, challenge or state alone, use `createPkcePair` / `createState` from `/pkce`.
62
+ * Adds `config.authorizationParameters` and `options.parameters`; a `scope` among them wins.
44
63
  *
45
64
  * @param config - OAuthConfig configuration including the redirect URI and scope.
65
+ * @param options - Per-request extra parameters.
46
66
  * @returns A promise that resolves to an object containing:
47
67
  * - code_verifier: The randomly generated PKCE code verifier.
48
68
  * - code_challenge: The derived PKCE code challenge.
49
69
  * - state: A random state to protect against CSRF.
50
70
  * - parameters: A Record<string,string> with the assembled parameters.
71
+ * @throws Error if an extra parameter is a reserved key (see `OAuthConfig.authorizationParameters`).
51
72
  */
52
- async function buildPKCEOptions(config) {
53
- const code_verifier = openid.randomPKCECodeVerifier();
54
- const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);
55
- const state = openid.randomState();
73
+ async function buildPKCEOptions(config, options = {}) {
74
+ const extra = {
75
+ ...config.authorizationParameters,
76
+ ...options.parameters
77
+ };
78
+ const reserved = Object.keys(extra).filter((key) => RESERVED_AUTHORIZATION_PARAMETERS.has(key));
79
+ if (reserved.length > 0) throw new Error(`Reserved authorization parameters can't be overridden: ${reserved.join(", ")}`);
80
+ const { verifier: code_verifier, challenge: code_challenge } = await createPkcePair();
81
+ const state = createState();
56
82
  const parameters = {
57
83
  redirect_uri: config.redirect_uri,
58
- scope: config.scope ?? "openid profile email offline_access",
84
+ scope: joinScope(config.scope ?? "openid profile email offline_access"),
85
+ ...extra,
59
86
  code_challenge,
60
87
  code_challenge_method: "S256",
61
88
  state
62
89
  };
63
- logger.info({
90
+ logger.debug({
64
91
  redirectUri: config.redirect_uri,
65
- scope: parameters.scope
92
+ scope: parameters.scope,
93
+ extraParameters: Object.keys(extra)
66
94
  }, "PKCE parameters generated");
67
95
  return {
68
96
  code_verifier,
@@ -177,13 +205,18 @@ async function exchangeAuthorizationCode(oauthConfig, callbackUrl, codeVerifier,
177
205
  * Extracts user information from an OIDC token response.
178
206
  * Decodes the ID token and extracts standard claims plus roles from a configurable claim.
179
207
  *
208
+ * Decodes only: the signature, `iss`, `aud` and `exp` are not checked. Safe on tokens
209
+ * straight from `exchangeAuthorizationCode`; verify any others first (e.g. jose `jwtVerify`).
210
+ *
180
211
  * @param tokens - Token response from the OIDC provider
181
212
  * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
182
213
  * @returns Extracted user information
183
214
  */
184
215
  function extractUserFromTokens(tokens, rolesClaim = "roles") {
185
216
  if (!tokens.id_token) throw new Error("OIDC token response is missing id_token");
186
- const idToken = decodeJwt(tokens.id_token);
217
+ return userFromClaims(decodeJwt(tokens.id_token), rolesClaim);
218
+ }
219
+ function userFromClaims(idToken, rolesClaim) {
187
220
  const rawRoles = idToken[rolesClaim];
188
221
  const nonArrayRoles = typeof rawRoles === "string" ? [rawRoles] : [];
189
222
  const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;
@@ -199,23 +232,17 @@ function extractUserFromTokens(tokens, rolesClaim = "roles") {
199
232
  roles
200
233
  };
201
234
  }
202
- /**
203
- * Builds a Session object from an OIDC token response.
204
- * Maps provider tokens and user claims into the internal Session format.
205
- *
206
- * @param tokens - Token response from the OIDC provider
207
- * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
208
- * @returns A Session object ready to be encrypted and stored
209
- */
210
- function buildSessionFromTokens(tokens, rolesClaim = "roles") {
235
+ function buildSessionFromTokens(tokens, options = {}) {
236
+ const { rolesClaim = "roles", mapSession } = typeof options === "string" ? { rolesClaim: options } : options;
211
237
  if (!tokens.access_token) throw new Error("OIDC token response is missing access_token");
212
- const userInfo = extractUserFromTokens(tokens, rolesClaim);
238
+ const claims = tokens.id_token ? decodeJwt(tokens.id_token) : void 0;
239
+ const userInfo = claims && userFromClaims(claims, rolesClaim);
213
240
  let scopes;
214
241
  if (typeof tokens.scope === "string") {
215
242
  const parsed = tokens.scope.split(" ").filter((s) => s.length > 0);
216
243
  if (parsed.length > 0) scopes = parsed;
217
244
  }
218
- return {
245
+ const session = {
219
246
  sid: newSessionId(),
220
247
  tokens: {
221
248
  accessToken: tokens.access_token,
@@ -223,13 +250,15 @@ function buildSessionFromTokens(tokens, rolesClaim = "roles") {
223
250
  refreshToken: tokens.refresh_token,
224
251
  idToken: tokens.id_token
225
252
  },
226
- user: {
253
+ user: userInfo && {
254
+ sub: userInfo.sub,
227
255
  name: userInfo.name,
228
256
  email: userInfo.email,
229
257
  roles: userInfo.roles
230
258
  },
231
259
  scopes
232
260
  };
261
+ return mapSession ? mapSession(tokens, claims, session) : session;
233
262
  }
234
263
  /**
235
264
  * Validates a return URL against open redirect attacks.
@@ -328,10 +357,10 @@ async function buildOidcLogoutUrl(oauthConfig, options) {
328
357
  /**
329
358
  * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
330
359
  *
331
- * Uses client_secret_post against the given token endpoint. The endpoint must be
332
- * https — the request carries the client secret.
360
+ * Authenticates with `config.clientAuth` (default client_secret_post). The
361
+ * endpoint must be https — the request carries the client credentials.
333
362
  *
334
- * @param config - Client credentials configuration including token endpoint, client ID, and secret
363
+ * @param config - Client credentials configuration: token endpoint, client ID and credentials
335
364
  * @returns Token response from the OAuth provider
336
365
  * @throws Error if the endpoint is not https, the request times out, or the grant fails
337
366
  *
@@ -347,10 +376,11 @@ async function buildOidcLogoutUrl(oauthConfig, options) {
347
376
  */
348
377
  async function clientCredentialsGrant(config) {
349
378
  try {
379
+ const { clientSecret, clientAuthentication } = resolveClientAuth(config);
350
380
  const openidConfig = new openid.Configuration({
351
381
  issuer: config.issuer ?? new URL(config.tokenEndpoint).origin,
352
382
  token_endpoint: config.tokenEndpoint
353
- }, config.clientId, config.clientSecret, openid.ClientSecretPost(config.clientSecret));
383
+ }, config.clientId, clientSecret, clientAuthentication);
354
384
  openidConfig.timeout = config.timeout ?? 30;
355
385
  const tokens = await openid.clientCredentialsGrant(openidConfig, config.scope ? { scope: config.scope } : void 0);
356
386
  logger.debug({ tokenEndpoint: config.tokenEndpoint }, "Client credentials grant successful");
@@ -363,7 +393,6 @@ async function clientCredentialsGrant(config) {
363
393
  throw error;
364
394
  }
365
395
  }
366
- //#endregion
367
- export { buildAuthorizationUrl, buildAuthorizationUrlWithPAR, buildOidcLogoutUrl, buildPKCEOptions, buildSessionFromTokens, clientCredentialsGrant, defaultScope, discoverAndBuildAuthorizationUrl, exchangeAuthorizationCode, extractUserFromTokens, refreshAccessToken, validateReturnUrl };
396
+ export { buildAuthorizationUrl, buildAuthorizationUrlWithPAR, buildOidcLogoutUrl, buildPKCEOptions, buildSessionFromTokens, clientCredentialsGrant, defaultScope, discoverAndBuildAuthorizationUrl, exchangeAuthorizationCode, extractUserFromTokens, joinScope, refreshAccessToken, validateReturnUrl };
368
397
 
369
398
  //# sourceMappingURL=oauth.js.map