@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.
- package/README.md +225 -33
- package/dist/activity-tracker.js +1 -2
- package/dist/cookies.d.ts +6 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/cookies.js +8 -3
- package/dist/cookies.js.map +1 -1
- package/dist/heartbeat.js +1 -2
- package/dist/id-token.d.ts +31 -0
- package/dist/id-token.d.ts.map +1 -0
- package/dist/id-token.js +60 -0
- package/dist/id-token.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/logger.js +1 -2
- package/dist/oauth-browser.d.ts +13 -10
- package/dist/oauth-browser.d.ts.map +1 -1
- package/dist/oauth-browser.js +4 -2
- package/dist/oauth-browser.js.map +1 -1
- package/dist/oauth-config.d.ts +70 -0
- package/dist/oauth-config.d.ts.map +1 -0
- package/dist/oauth-logging.d.ts +4 -3
- package/dist/oauth-logging.d.ts.map +1 -1
- package/dist/oauth-logging.js +5 -5
- package/dist/oauth-logging.js.map +1 -1
- package/dist/oauth.d.ts +47 -45
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +58 -29
- package/dist/oauth.js.map +1 -1
- package/dist/oidc-discovery.d.ts +13 -4
- package/dist/oidc-discovery.d.ts.map +1 -1
- package/dist/oidc-discovery.js +82 -15
- package/dist/oidc-discovery.js.map +1 -1
- package/dist/pkce.d.ts +10 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +23 -0
- package/dist/pkce.js.map +1 -0
- package/dist/providers/vipps/client.d.ts +3 -2
- package/dist/providers/vipps/client.d.ts.map +1 -1
- package/dist/providers/vipps/index.js +10 -10
- package/dist/providers/vipps/index.js.map +1 -1
- package/dist/providers/vipps/types.d.ts +2 -2
- package/dist/providers/vipps/types.d.ts.map +1 -1
- package/dist/rate-limit.js +1 -2
- package/dist/server/authorized-fetch.d.ts +78 -0
- package/dist/server/authorized-fetch.d.ts.map +1 -0
- package/dist/server/heartbeat-handler.d.ts +6 -4
- package/dist/server/heartbeat-handler.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +744 -94
- package/dist/server/index.js.map +1 -1
- package/dist/server/login-transaction.d.ts +55 -0
- package/dist/server/login-transaction.d.ts.map +1 -0
- package/dist/server/oidc-callback.d.ts +13 -4
- package/dist/server/oidc-callback.d.ts.map +1 -1
- package/dist/server/oidc-login.d.ts +14 -5
- package/dist/server/oidc-login.d.ts.map +1 -1
- package/dist/server/oidc-logout.d.ts +6 -4
- package/dist/server/oidc-logout.d.ts.map +1 -1
- package/dist/server/session-persistence.d.ts +102 -0
- package/dist/server/session-persistence.d.ts.map +1 -0
- package/dist/server/session-store.d.ts +39 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session.d.ts +35 -28
- package/dist/server/session.d.ts.map +1 -1
- package/dist/session-cookies.d.ts +1 -0
- package/dist/session-cookies.d.ts.map +1 -1
- package/dist/session-cookies.js +4 -3
- package/dist/session-cookies.js.map +1 -1
- package/dist/session-events.d.ts +10 -3
- package/dist/session-events.d.ts.map +1 -1
- package/dist/session-events.js +4 -3
- package/dist/session-events.js.map +1 -1
- package/dist/session-refresh.d.ts +1 -1
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +1 -2
- package/dist/session-refresh.js.map +1 -1
- package/dist/session-validation.d.ts.map +1 -1
- package/dist/session-validation.js +9 -6
- package/dist/session-validation.js.map +1 -1
- package/dist/silent-login.d.ts +1 -1
- package/dist/silent-login.d.ts.map +1 -1
- package/dist/silent-login.js +3 -3
- package/dist/silent-login.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +0 -14
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -24
- package/dist/utils.js.map +1 -1
- package/docs/discovery-cache.md +7 -3
- package/docs/rp-initiated-logout.md +6 -3
- package/docs/server-sessions.md +154 -0
- package/docs/session-events.md +17 -16
- package/package.json +14 -6
package/dist/oauth-browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 *
|
|
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"}
|
package/dist/oauth-logging.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
* lines for debugging OAuth/OIDC flows without
|
|
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,
|
|
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"}
|
package/dist/oauth-logging.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
// src/oauth-logging.ts
|
|
2
2
|
/**
|
|
3
3
|
* Returns a structured, log-safe context describing an `OAuthConfig`.
|
|
4
4
|
*
|
|
5
|
-
* Excludes `clientSecret
|
|
6
|
-
* lines for debugging OAuth/OIDC flows without
|
|
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
|
|
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
|
-
/**
|
|
7
|
-
export
|
|
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
|
|
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
|
-
*
|
|
201
|
-
* https — the request carries the client
|
|
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
|
|
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
|
*
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
332
|
-
* https — the request carries the client
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|