@eventuras/fides-auth 0.13.0 → 0.14.1
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/dist/activity-tracker.d.ts +1 -1
- package/dist/activity-tracker.d.ts.map +1 -1
- package/dist/cookies.d.ts.map +1 -1
- package/dist/heartbeat.d.ts +1 -1
- package/dist/oauth-logging.d.ts +3 -3
- package/dist/oauth-logging.d.ts.map +1 -1
- package/dist/oauth.d.ts +3 -3
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +7 -6
- package/dist/oauth.js.map +1 -1
- package/dist/oidc-discovery.d.ts +42 -0
- package/dist/oidc-discovery.d.ts.map +1 -0
- package/dist/oidc-discovery.js +82 -0
- package/dist/oidc-discovery.js.map +1 -0
- package/dist/providers/vipps/client.d.ts +8 -5
- package/dist/providers/vipps/client.d.ts.map +1 -1
- package/dist/providers/vipps/index.js +8 -5
- package/dist/providers/vipps/index.js.map +1 -1
- package/dist/providers/vipps/types.d.ts +8 -8
- package/dist/providers/vipps/types.d.ts.map +1 -1
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/server/cookie-store.d.ts +1 -1
- package/dist/server/heartbeat-handler.d.ts +2 -2
- package/dist/server/oidc-callback.d.ts +2 -2
- package/dist/server/oidc-login.d.ts +2 -2
- package/dist/server/oidc-logout.d.ts +2 -2
- package/dist/server/session.d.ts +4 -4
- package/dist/session-cookies.d.ts +1 -1
- package/dist/session-events.d.ts +6 -6
- package/dist/session-events.d.ts.map +1 -1
- package/dist/session-refresh.d.ts +2 -2
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/silent-login.d.ts +13 -4
- package/dist/silent-login.d.ts.map +1 -1
- package/dist/silent-login.js +2 -1
- package/dist/silent-login.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/docs/discovery-cache.md +53 -0
- package/docs/session-events.md +18 -0
- package/package.json +12 -8
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* Uses `focusin` rather than `focus` because `focus` does not bubble — focusing
|
|
30
30
|
* a nested input would otherwise be missed when listening on `window`.
|
|
31
31
|
*/
|
|
32
|
-
export declare const DEFAULT_ACTIVITY_EVENTS: readonly [
|
|
32
|
+
export declare const DEFAULT_ACTIVITY_EVENTS: readonly ['pointerdown', 'keydown', 'touchstart', 'focusin'];
|
|
33
33
|
export interface ActivityTrackerOptions {
|
|
34
34
|
/**
|
|
35
35
|
* Target to attach listeners to. Defaults to `globalThis.window` in the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-tracker.d.ts","sourceRoot":"","sources":["../src/activity-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,
|
|
1
|
+
{"version":3,"file":"activity-tracker.d.ts","sourceRoot":"","sources":["../src/activity-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,YAClC,aAAa,EACb,SAAS,EACT,YAAY,EACZ,SAAS,CACD,CAAC;AAEX,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uFAAuF;IACvF,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,eAAe,IAAI,MAAM,CAAC;IAC1B,uEAAuE;IACvE,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,6EAA6E;IAC7E,cAAc,IAAI,IAAI,CAAC;IACvB,oCAAoC;IACpC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAyC3F"}
|
package/dist/cookies.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,MAAM;aACZ,KAAK,EAAE,MAAM;
|
|
1
|
+
{"version":3,"file":"cookies.d.ts","sourceRoot":"","sources":["../src/cookies.ts"],"names":[],"mappings":"AAUA;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAE1B,UAAU,EAAE,MAAM;aAClB,IAAI,EAAE,MAAM;aACZ,KAAK,EAAE,MAAM;IAH/B,YACkB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,GAAE,MAAyB,EAMjD;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yBAAyB;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;IACrC,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B,EAAE,aAMzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,aAMvC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,eAAe,CAAC;AAErD;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAGlE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3E"}
|
package/dist/heartbeat.d.ts
CHANGED
package/dist/oauth-logging.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from './oauth';
|
|
2
|
-
import { RefreshFailureCause } from './session-events';
|
|
1
|
+
import type { OAuthConfig } from './oauth';
|
|
2
|
+
import type { RefreshFailureCause } from './session-events';
|
|
3
3
|
/**
|
|
4
4
|
* Returns a structured, log-safe context describing an `OAuthConfig`.
|
|
5
5
|
*
|
|
@@ -33,6 +33,7 @@ export declare function getOAuthErrorLogContext(error: unknown): {
|
|
|
33
33
|
causeMessage?: undefined;
|
|
34
34
|
causeName?: undefined;
|
|
35
35
|
} | {
|
|
36
|
+
value?: undefined;
|
|
36
37
|
code: string | undefined;
|
|
37
38
|
error: string | undefined;
|
|
38
39
|
errorDescription: string | undefined;
|
|
@@ -41,7 +42,6 @@ export declare function getOAuthErrorLogContext(error: unknown): {
|
|
|
41
42
|
status: number | undefined;
|
|
42
43
|
causeMessage: string | undefined;
|
|
43
44
|
causeName: string | undefined;
|
|
44
|
-
value?: undefined;
|
|
45
45
|
};
|
|
46
46
|
/**
|
|
47
47
|
* Classifies a refresh failure into the three causes that call for different
|
|
@@ -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
|
|
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"}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Session } from './types';
|
|
2
1
|
import * as openid from 'openid-client';
|
|
2
|
+
import type { Session } from './types';
|
|
3
3
|
export type { Configuration, TokenEndpointResponse } from 'openid-client';
|
|
4
4
|
/** Default OIDC scope requesting user identity, profile, email, and offline access. */
|
|
5
5
|
export declare const defaultScope = "openid profile email offline_access";
|
|
@@ -48,7 +48,7 @@ export declare function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptio
|
|
|
48
48
|
/**
|
|
49
49
|
* Builds an authorization URL with PKCE parameters.
|
|
50
50
|
*
|
|
51
|
-
* @param config - Discovered OpenID Configuration (use
|
|
51
|
+
* @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)
|
|
52
52
|
* @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
|
|
53
53
|
* @returns A Promise that resolves to the authorization URL
|
|
54
54
|
*/
|
|
@@ -60,7 +60,7 @@ export declare function buildAuthorizationUrl(config: openid.Configuration, pkce
|
|
|
60
60
|
* containing only `client_id` and `request_uri`. The provider's discovery
|
|
61
61
|
* metadata must include `pushed_authorization_request_endpoint`.
|
|
62
62
|
*
|
|
63
|
-
* @param config - Discovered OpenID Configuration (use
|
|
63
|
+
* @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)
|
|
64
64
|
* @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
|
|
65
65
|
* @returns A Promise that resolves to the PAR-based authorization URL
|
|
66
66
|
*/
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
5
|
import { decodeJwt } from "jose";
|
|
5
6
|
import * as openid from "openid-client";
|
|
6
7
|
//#region src/oauth.ts
|
|
@@ -17,7 +18,7 @@ var defaultScope = "openid profile email offline_access";
|
|
|
17
18
|
async function refreshAccessToken(oAuthConfig, refreshToken) {
|
|
18
19
|
logger.debug({ issuer: oAuthConfig.issuer }, "Starting token refresh");
|
|
19
20
|
try {
|
|
20
|
-
const config = await
|
|
21
|
+
const config = await discoverConfiguration(oAuthConfig);
|
|
21
22
|
const tokens = await openid.refreshTokenGrant(config, refreshToken, { scope: oAuthConfig.scope });
|
|
22
23
|
logger.info({
|
|
23
24
|
hasAccessToken: !!tokens.access_token,
|
|
@@ -73,7 +74,7 @@ async function buildPKCEOptions(config) {
|
|
|
73
74
|
/**
|
|
74
75
|
* Builds an authorization URL with PKCE parameters.
|
|
75
76
|
*
|
|
76
|
-
* @param config - Discovered OpenID Configuration (use
|
|
77
|
+
* @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)
|
|
77
78
|
* @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
|
|
78
79
|
* @returns A Promise that resolves to the authorization URL
|
|
79
80
|
*/
|
|
@@ -94,7 +95,7 @@ async function buildAuthorizationUrl(config, pkceOptions) {
|
|
|
94
95
|
* containing only `client_id` and `request_uri`. The provider's discovery
|
|
95
96
|
* metadata must include `pushed_authorization_request_endpoint`.
|
|
96
97
|
*
|
|
97
|
-
* @param config - Discovered OpenID Configuration (use
|
|
98
|
+
* @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)
|
|
98
99
|
* @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
|
|
99
100
|
* @returns A Promise that resolves to the PAR-based authorization URL
|
|
100
101
|
*/
|
|
@@ -126,7 +127,7 @@ async function buildAuthorizationUrlWithPAR(config, pkceOptions) {
|
|
|
126
127
|
async function discoverAndBuildAuthorizationUrl(oauthConfig, pkceOptions) {
|
|
127
128
|
logger.debug({ issuer: oauthConfig.issuer }, "Discovering OpenID configuration");
|
|
128
129
|
try {
|
|
129
|
-
const config = await
|
|
130
|
+
const config = await discoverConfiguration(oauthConfig);
|
|
130
131
|
const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;
|
|
131
132
|
if (oauthConfig.usePar) {
|
|
132
133
|
if (!parEndpoint) throw new Error("PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint");
|
|
@@ -156,7 +157,7 @@ async function exchangeAuthorizationCode(oauthConfig, callbackUrl, codeVerifier,
|
|
|
156
157
|
issuer: oauthConfig.issuer,
|
|
157
158
|
clientId: oauthConfig.clientId
|
|
158
159
|
}, "Exchanging authorization code for tokens");
|
|
159
|
-
const config = await
|
|
160
|
+
const config = await discoverConfiguration(oauthConfig);
|
|
160
161
|
const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);
|
|
161
162
|
normalizedCallbackUrl.pathname = callbackUrl.pathname;
|
|
162
163
|
normalizedCallbackUrl.search = callbackUrl.search;
|
|
@@ -286,7 +287,7 @@ function validateReturnUrl(returnTo, applicationOrigin, defaultPath = "/", valid
|
|
|
286
287
|
async function buildOidcLogoutUrl(oauthConfig, options) {
|
|
287
288
|
const { postLogoutRedirectUri, idTokenHint, state, logoutHint, includeClientId = true } = typeof options === "string" ? { postLogoutRedirectUri: options } : options;
|
|
288
289
|
try {
|
|
289
|
-
const config = await
|
|
290
|
+
const config = await discoverConfiguration(oauthConfig);
|
|
290
291
|
const endSessionEndpoint = config.serverMetadata().end_session_endpoint;
|
|
291
292
|
if (!endSessionEndpoint) {
|
|
292
293
|
logger.info({ issuer: oauthConfig.issuer }, "No end_session_endpoint found");
|
package/dist/oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { newSessionId } from './session-events';\nimport type { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:oauth' });\n\n// Re-export commonly used types from openid-client for convenience\nexport type { Configuration, TokenEndpointResponse } from 'openid-client';\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Configuration for server-side OAuth / OIDC flows. */\nexport type OAuthConfig = {\n issuer: string;\n clientId: string;\n clientSecret: string;\n redirect_uri: string;\n scope: string;\n /**\n * Opt in to Pushed Authorization Requests (RFC 9126) when the provider\n * supports it. When true, authorization parameters are posted to the\n * provider's PAR endpoint ahead of the redirect, and only `client_id` +\n * `request_uri` travel through the user-agent. The provider must advertise\n * `pushed_authorization_request_endpoint` in its discovery metadata.\n */\n usePar?: boolean;\n};\n\n/**\n * Refreshes an access token using a refresh token grant.\n *\n * @param oAuthConfig - OAuth configuration for the identity provider\n * @param refreshToken - The refresh token to exchange\n * @returns Token endpoint response containing new access and refresh tokens\n */\nexport async function refreshAccessToken(\n oAuthConfig: OAuthConfig,\n refreshToken: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug({ issuer: oAuthConfig.issuer }, 'Starting token refresh');\n\n try {\n const config = await openid.discovery(\n new URL(oAuthConfig.issuer),\n oAuthConfig.clientId,\n oAuthConfig.clientSecret,\n openid.ClientSecretPost(oAuthConfig.clientSecret),\n );\n\n const tokens = await openid.refreshTokenGrant(\n config,\n refreshToken,\n {\n scope: oAuthConfig.scope,\n },\n );\n\n logger.info({\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in\n }, 'Token refresh successful');\n\n return tokens;\n } catch (error) {\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Most often the refresh token has expired - log at info level\n logger.info(\n { ...errorContext, issuer: oAuthConfig.issuer },\n 'Token refresh failed - refresh token expired or invalid',\n );\n } else {\n // Unexpected error - log at error level\n logger.error({ error, issuer: oAuthConfig.issuer }, 'Token refresh failed');\n }\n\n throw error;\n }\n}\n\n/** PKCE parameters generated for an authorization request. */\nexport interface PKCEOptions {\n code_verifier: string;\n code_challenge: string;\n state: string;\n parameters: Record<string, string>;\n}\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @returns A promise that resolves to an object containing:\n * - code_verifier: The randomly generated PKCE code verifier.\n * - code_challenge: The derived PKCE code challenge.\n * - state: A random state to protect against CSRF.\n * - parameters: A Record<string,string> with the assembled parameters.\n */\nexport async function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions> {\n const code_verifier = openid.randomPKCECodeVerifier();\n const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);\n const state = openid.randomState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope ?? defaultScope,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n logger.info(\n { redirectUri: config.redirect_uri, scope: parameters.scope },\n 'PKCE parameters generated'\n );\n\n return { code_verifier, code_challenge, state, parameters };\n}\n\n/**\n * Builds an authorization URL with PKCE parameters.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function buildAuthorizationUrl(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);\n logger.info('Authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build authorization URL');\n throw error;\n }\n}\n\n/**\n * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).\n *\n * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL\n * containing only `client_id` and `request_uri`. The provider's discovery\n * metadata must include `pushed_authorization_request_endpoint`.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the PAR-based authorization URL\n */\nexport async function buildAuthorizationUrlWithPAR(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = await openid.buildAuthorizationUrlWithPAR(\n config,\n pkceOptions.parameters,\n );\n logger.info('PAR authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build PAR authorization URL');\n throw error;\n }\n}\n\n/**\n * Convenience function to discover OpenID configuration and build an\n * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and\n * the provider advertises `pushed_authorization_request_endpoint`.\n *\n * - `usePar === true` + provider advertises PAR → uses PAR.\n * - `usePar === true` + provider does **not** advertise PAR → throws.\n * - `usePar` unset/false + provider advertises PAR → standard flow + one-line\n * `info` advisory that PAR is available but not enabled.\n * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.\n *\n * @param oauthConfig - Your OAuth configuration\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function discoverAndBuildAuthorizationUrl(\n oauthConfig: OAuthConfig,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n logger.debug({ issuer: oauthConfig.issuer }, 'Discovering OpenID configuration');\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret)\n );\n\n const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;\n\n if (oauthConfig.usePar) {\n if (!parEndpoint) {\n throw new Error(\n 'PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint',\n );\n }\n return buildAuthorizationUrlWithPAR(config, pkceOptions);\n }\n\n if (parEndpoint) {\n logger.info(\n { issuer: oauthConfig.issuer },\n 'Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it',\n );\n }\n\n return buildAuthorizationUrl(config, pkceOptions);\n } catch (error) {\n logger.error({ error, issuer: oauthConfig.issuer }, 'Failed to discover config or build URL');\n throw error;\n }\n}\n\n/**\n * Configuration for OAuth client credentials flow (machine-to-machine authentication)\n */\nexport type ClientCredentialsConfig = {\n /** OAuth token endpoint URL. Must be https. */\n tokenEndpoint: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Optional scope for the access token */\n scope?: string;\n /**\n * Issuer identifier. Only used to satisfy the server metadata; defaults to the\n * token endpoint's origin, which is right for the common case.\n */\n issuer?: string;\n /**\n * Request timeout in seconds.\n * @default 30\n */\n timeout?: number;\n};\n\n/**\n * Exchanges an authorization code for tokens using OIDC discovery and PKCE.\n *\n * @param oauthConfig - OAuth configuration\n * @param callbackUrl - The full callback URL with query parameters from the OIDC provider\n * @param codeVerifier - The PKCE code verifier stored during login initiation\n * @param expectedState - The state parameter stored during login initiation\n * @returns Token response from the OIDC provider\n */\nexport async function exchangeAuthorizationCode(\n oauthConfig: OAuthConfig,\n callbackUrl: URL,\n codeVerifier: string,\n expectedState: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug(\n { issuer: oauthConfig.issuer, clientId: oauthConfig.clientId },\n 'Exchanging authorization code for tokens',\n );\n\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n\n // openid-client overrides the explicit redirect_uri option with\n // stripParams(callbackUrl). Anchor to oauthConfig.redirect_uri so PAR and\n // token-exchange stay consistent behind reverse proxies that don't forward\n // the original scheme. Build from the trusted base and then copy pathname/\n // search by assignment — `new URL(callbackUrl.pathname, base)` would let a\n // `//evil.com/...` pathname swap the host.\n const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);\n normalizedCallbackUrl.pathname = callbackUrl.pathname;\n normalizedCallbackUrl.search = callbackUrl.search;\n normalizedCallbackUrl.hash = '';\n\n const tokens = await openid.authorizationCodeGrant(\n config,\n normalizedCallbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState,\n },\n { redirect_uri: oauthConfig.redirect_uri },\n );\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Authorization code exchange successful',\n );\n\n return tokens;\n}\n\n/**\n * Result of extracting user information from an OIDC token response.\n */\nexport interface OidcUserInfo {\n sub: string;\n name: string;\n email: string;\n roles: string[];\n}\n\n/**\n * Extracts user information from an OIDC token response.\n * Decodes the ID token and extracts standard claims plus roles from a configurable claim.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns Extracted user information\n */\nexport function extractUserFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): OidcUserInfo {\n if (!tokens.id_token) {\n throw new Error('OIDC token response is missing id_token');\n }\n\n const idToken = decodeJwt(tokens.id_token);\n\n // Normalize roles: some providers send a single string instead of an array\n const rawRoles = idToken[rolesClaim];\n const nonArrayRoles = typeof rawRoles === 'string' ? [rawRoles] : [];\n const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;\n\n logger.debug({ sub: idToken.sub, rolesClaim, rolesCount: roles.length }, 'Extracted user from ID token');\n\n return {\n sub: idToken.sub ?? '',\n name: idToken.name as string,\n email: idToken.email as string,\n roles,\n };\n}\n\n/**\n * Builds a Session object from an OIDC token response.\n * Maps provider tokens and user claims into the internal Session format.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): Session {\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n const userInfo = extractUserFromTokens(tokens, rolesClaim);\n\n let scopes: string[] | undefined;\n if (typeof tokens.scope === 'string') {\n const parsed = tokens.scope.split(' ').filter(s => s.length > 0);\n if (parsed.length > 0) scopes = parsed;\n }\n\n return {\n // Minted once here and carried across refreshes, so every log line for this\n // session shares one correlation id.\n sid: newSessionId(),\n tokens: {\n accessToken: tokens.access_token,\n accessTokenExpiresAt: tokens.expires_in\n ? new Date(Date.now() + tokens.expires_in * 1000).toISOString()\n : undefined,\n refreshToken: tokens.refresh_token,\n // Kept for `id_token_hint` on RP-initiated logout.\n idToken: tokens.id_token,\n },\n user: {\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n}\n\n/**\n * Validates a return URL against open redirect attacks.\n * Ensures the URL is same-origin and passes optional custom validation.\n *\n * @param returnTo - The raw returnTo path/URL to validate\n * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')\n * @param defaultPath - Fallback path if validation fails (default: '/')\n * @param validate - Optional custom validation function for the pathname\n * @returns A safe, validated URL\n */\nexport function validateReturnUrl(\n returnTo: string | null | undefined,\n applicationOrigin: string,\n defaultPath: string = '/',\n validate?: (pathname: string) => boolean,\n): URL {\n const redirectPath = returnTo ?? defaultPath;\n let redirectUrl: URL;\n\n try {\n redirectUrl = new URL(redirectPath, applicationOrigin);\n } catch {\n logger.warn({ returnTo: redirectPath }, 'Invalid returnTo URL, using default');\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Enforce same-origin — cross-origin returnTo is a potential open redirect attack\n const originUrl = new URL(applicationOrigin);\n if (redirectUrl.origin !== originUrl.origin) {\n logger.error(\n { returnTo: redirectPath, redirectOrigin: redirectUrl.origin, applicationOrigin: originUrl.origin },\n 'Possible open redirect attack: returnTo points outside application origin',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Custom validation\n if (validate && !validate(redirectUrl.pathname)) {\n logger.warn(\n { pathname: redirectUrl.pathname },\n 'returnTo failed custom validation, falling back to default',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n return redirectUrl;\n}\n\n/** Parameters for an RP-initiated logout request (OIDC RP-Initiated Logout 1.0 §2). */\nexport interface OidcLogoutOptions {\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri?: string;\n\n /**\n * Raw ID token from `session.tokens.idToken`, sent as `id_token_hint`. Tells the\n * OP which session to end, and is usually what lets it skip the confirmation\n * interstitial.\n */\n idTokenHint?: string;\n\n /** Opaque value echoed back on the post-logout redirect. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /**\n * Whether to send `client_id`. Default true, also alongside `id_token_hint`: the\n * spec allows both (the OP must then verify they match), and `client_id` is often\n * what lets the OP accept `post_logout_redirect_uri` when the hint is stale or\n * absent. Set false only for a provider that documents rejecting the combination.\n */\n includeClientId?: boolean;\n}\n\n/**\n * Discovers the OIDC provider's end_session_endpoint and builds an RP-initiated\n * logout URL. Returns null when the provider exposes none, so callers can fall\n * back to clearing the local session only.\n *\n * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)\n * @param options - Logout parameters, or a string as shorthand for\n * `postLogoutRedirectUri`\n * @returns Logout URL or null if not supported\n *\n * @example\n * ```typescript\n * const session = await readSession(cookies, secret);\n * const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n * postLogoutRedirectUri: 'https://app.example.com/',\n * idTokenHint: session?.tokens?.idToken,\n * });\n * ```\n */\nexport async function buildOidcLogoutUrl(\n oauthConfig: OAuthConfig,\n options: OidcLogoutOptions | string,\n): Promise<URL | null> {\n const {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId = true,\n } = typeof options === 'string' ? { postLogoutRedirectUri: options } : options;\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n const endSessionEndpoint = config.serverMetadata().end_session_endpoint;\n\n if (!endSessionEndpoint) {\n logger.info({ issuer: oauthConfig.issuer }, 'No end_session_endpoint found');\n return null;\n }\n\n const parameters: Record<string, string> = {};\n if (postLogoutRedirectUri) parameters.post_logout_redirect_uri = postLogoutRedirectUri;\n if (idTokenHint) parameters.id_token_hint = idTokenHint;\n if (state) parameters.state = state;\n if (logoutHint) parameters.logout_hint = logoutHint;\n\n // buildEndSessionUrl always adds client_id, and deleting it afterwards would\n // also drop one the provider baked into its own endpoint — so when it isn't\n // wanted, assemble the URL directly instead.\n let logoutUrl: URL;\n if (includeClientId) {\n logoutUrl = openid.buildEndSessionUrl(config, parameters);\n } else {\n logoutUrl = new URL(endSessionEndpoint);\n\n // openid-client rejects a non-https endpoint on the path above; the manual\n // one must too. The URL carries a raw id_token_hint, so a discovery document\n // that downgrades the scheme would put it on the wire in the clear.\n if (logoutUrl.protocol !== 'https:') {\n logger.error(\n { issuer: oauthConfig.issuer, protocol: logoutUrl.protocol },\n 'end_session_endpoint is not https — refusing to build a logout URL',\n );\n return null;\n }\n\n for (const [key, value] of Object.entries(parameters)) {\n logoutUrl.searchParams.append(key, value);\n }\n }\n\n logger.debug(\n { endSessionEndpoint, hasIdTokenHint: !!idTokenHint, hasState: !!state },\n 'Built OIDC logout URL',\n );\n return logoutUrl;\n } catch (error) {\n logger.warn({ error, issuer: oauthConfig.issuer }, 'OIDC discovery failed for logout');\n return null;\n }\n}\n\n/**\n * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.\n *\n * Uses client_secret_post against the given token endpoint. The endpoint must be\n * https — the request carries the client secret.\n *\n * @param config - Client credentials configuration including token endpoint, client ID, and secret\n * @returns Token response from the OAuth provider\n * @throws Error if the endpoint is not https, the request times out, or the grant fails\n *\n * @example\n * ```typescript\n * const tokens = await clientCredentialsGrant({\n * tokenEndpoint: 'https://api.example.com/oauth2/token',\n * clientId: 'my-client-id',\n * clientSecret: 'my-client-secret',\n * scope: 'api:read api:write',\n * });\n * ```\n */\nexport async function clientCredentialsGrant(\n config: ClientCredentialsConfig,\n): Promise<openid.TokenEndpointResponse> {\n try {\n // Build the configuration from metadata rather than discovering it: callers\n // give us a token endpoint, not an issuer. Going through openid-client anyway\n // buys the https guard and the timeout, which a raw fetch of a caller-supplied\n // URL carrying a client secret would not have.\n const openidConfig = new openid.Configuration(\n {\n issuer: config.issuer ?? new URL(config.tokenEndpoint).origin,\n token_endpoint: config.tokenEndpoint,\n },\n config.clientId,\n config.clientSecret,\n // Explicit rather than relying on openid-client's default, matching the\n // discovery calls above: the JSDoc promises client_secret_post.\n openid.ClientSecretPost(config.clientSecret),\n );\n openidConfig.timeout = config.timeout ?? 30;\n\n const tokens = await openid.clientCredentialsGrant(\n openidConfig,\n config.scope ? { scope: config.scope } : undefined,\n );\n\n logger.debug({ tokenEndpoint: config.tokenEndpoint }, 'Client credentials grant successful');\n return tokens;\n } catch (error) {\n logger.error(\n { ...getOAuthErrorLogContext(error), tokenEndpoint: config.tokenEndpoint },\n 'Client credentials grant failed',\n );\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { discoverConfiguration } from './oidc-discovery';\nimport { newSessionId } from './session-events';\nimport type { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:oauth' });\n\n// Re-export commonly used types from openid-client for convenience\nexport type { Configuration, TokenEndpointResponse } from 'openid-client';\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Configuration for server-side OAuth / OIDC flows. */\nexport type OAuthConfig = {\n issuer: string;\n clientId: string;\n clientSecret: string;\n redirect_uri: string;\n scope: string;\n /**\n * Opt in to Pushed Authorization Requests (RFC 9126) when the provider\n * supports it. When true, authorization parameters are posted to the\n * provider's PAR endpoint ahead of the redirect, and only `client_id` +\n * `request_uri` travel through the user-agent. The provider must advertise\n * `pushed_authorization_request_endpoint` in its discovery metadata.\n */\n usePar?: boolean;\n};\n\n/**\n * Refreshes an access token using a refresh token grant.\n *\n * @param oAuthConfig - OAuth configuration for the identity provider\n * @param refreshToken - The refresh token to exchange\n * @returns Token endpoint response containing new access and refresh tokens\n */\nexport async function refreshAccessToken(\n oAuthConfig: OAuthConfig,\n refreshToken: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug({ issuer: oAuthConfig.issuer }, 'Starting token refresh');\n\n try {\n const config = await discoverConfiguration(oAuthConfig);\n\n const tokens = await openid.refreshTokenGrant(\n config,\n refreshToken,\n {\n scope: oAuthConfig.scope,\n },\n );\n\n logger.info({\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in\n }, 'Token refresh successful');\n\n return tokens;\n } catch (error) {\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Most often the refresh token has expired - log at info level\n logger.info(\n { ...errorContext, issuer: oAuthConfig.issuer },\n 'Token refresh failed - refresh token expired or invalid',\n );\n } else {\n // Unexpected error - log at error level\n logger.error({ error, issuer: oAuthConfig.issuer }, 'Token refresh failed');\n }\n\n throw error;\n }\n}\n\n/** PKCE parameters generated for an authorization request. */\nexport interface PKCEOptions {\n code_verifier: string;\n code_challenge: string;\n state: string;\n parameters: Record<string, string>;\n}\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @returns A promise that resolves to an object containing:\n * - code_verifier: The randomly generated PKCE code verifier.\n * - code_challenge: The derived PKCE code challenge.\n * - state: A random state to protect against CSRF.\n * - parameters: A Record<string,string> with the assembled parameters.\n */\nexport async function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions> {\n const code_verifier = openid.randomPKCECodeVerifier();\n const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);\n const state = openid.randomState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope ?? defaultScope,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n logger.info(\n { redirectUri: config.redirect_uri, scope: parameters.scope },\n 'PKCE parameters generated'\n );\n\n return { code_verifier, code_challenge, state, parameters };\n}\n\n/**\n * Builds an authorization URL with PKCE parameters.\n *\n * @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function buildAuthorizationUrl(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);\n logger.info('Authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build authorization URL');\n throw error;\n }\n}\n\n/**\n * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).\n *\n * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL\n * containing only `client_id` and `request_uri`. The provider's discovery\n * metadata must include `pushed_authorization_request_endpoint`.\n *\n * @param config - Discovered OpenID Configuration (use discoverConfiguration() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the PAR-based authorization URL\n */\nexport async function buildAuthorizationUrlWithPAR(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = await openid.buildAuthorizationUrlWithPAR(\n config,\n pkceOptions.parameters,\n );\n logger.info('PAR authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build PAR authorization URL');\n throw error;\n }\n}\n\n/**\n * Convenience function to discover OpenID configuration and build an\n * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and\n * the provider advertises `pushed_authorization_request_endpoint`.\n *\n * - `usePar === true` + provider advertises PAR → uses PAR.\n * - `usePar === true` + provider does **not** advertise PAR → throws.\n * - `usePar` unset/false + provider advertises PAR → standard flow + one-line\n * `info` advisory that PAR is available but not enabled.\n * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.\n *\n * @param oauthConfig - Your OAuth configuration\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function discoverAndBuildAuthorizationUrl(\n oauthConfig: OAuthConfig,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n logger.debug({ issuer: oauthConfig.issuer }, 'Discovering OpenID configuration');\n\n try {\n const config = await discoverConfiguration(oauthConfig);\n\n const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;\n\n if (oauthConfig.usePar) {\n if (!parEndpoint) {\n throw new Error(\n 'PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint',\n );\n }\n return buildAuthorizationUrlWithPAR(config, pkceOptions);\n }\n\n if (parEndpoint) {\n logger.info(\n { issuer: oauthConfig.issuer },\n 'Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it',\n );\n }\n\n return buildAuthorizationUrl(config, pkceOptions);\n } catch (error) {\n logger.error({ error, issuer: oauthConfig.issuer }, 'Failed to discover config or build URL');\n throw error;\n }\n}\n\n/**\n * Configuration for OAuth client credentials flow (machine-to-machine authentication)\n */\nexport type ClientCredentialsConfig = {\n /** OAuth token endpoint URL. Must be https. */\n tokenEndpoint: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Optional scope for the access token */\n scope?: string;\n /**\n * Issuer identifier. Only used to satisfy the server metadata; defaults to the\n * token endpoint's origin, which is right for the common case.\n */\n issuer?: string;\n /**\n * Request timeout in seconds.\n * @default 30\n */\n timeout?: number;\n};\n\n/**\n * Exchanges an authorization code for tokens using OIDC discovery and PKCE.\n *\n * @param oauthConfig - OAuth configuration\n * @param callbackUrl - The full callback URL with query parameters from the OIDC provider\n * @param codeVerifier - The PKCE code verifier stored during login initiation\n * @param expectedState - The state parameter stored during login initiation\n * @returns Token response from the OIDC provider\n */\nexport async function exchangeAuthorizationCode(\n oauthConfig: OAuthConfig,\n callbackUrl: URL,\n codeVerifier: string,\n expectedState: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug(\n { issuer: oauthConfig.issuer, clientId: oauthConfig.clientId },\n 'Exchanging authorization code for tokens',\n );\n\n const config = await discoverConfiguration(oauthConfig);\n\n // openid-client overrides the explicit redirect_uri option with\n // stripParams(callbackUrl). Anchor to oauthConfig.redirect_uri so PAR and\n // token-exchange stay consistent behind reverse proxies that don't forward\n // the original scheme. Build from the trusted base and then copy pathname/\n // search by assignment — `new URL(callbackUrl.pathname, base)` would let a\n // `//evil.com/...` pathname swap the host.\n const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);\n normalizedCallbackUrl.pathname = callbackUrl.pathname;\n normalizedCallbackUrl.search = callbackUrl.search;\n normalizedCallbackUrl.hash = '';\n\n const tokens = await openid.authorizationCodeGrant(\n config,\n normalizedCallbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState,\n },\n { redirect_uri: oauthConfig.redirect_uri },\n );\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Authorization code exchange successful',\n );\n\n return tokens;\n}\n\n/**\n * Result of extracting user information from an OIDC token response.\n */\nexport interface OidcUserInfo {\n sub: string;\n name: string;\n email: string;\n roles: string[];\n}\n\n/**\n * Extracts user information from an OIDC token response.\n * Decodes the ID token and extracts standard claims plus roles from a configurable claim.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns Extracted user information\n */\nexport function extractUserFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): OidcUserInfo {\n if (!tokens.id_token) {\n throw new Error('OIDC token response is missing id_token');\n }\n\n const idToken = decodeJwt(tokens.id_token);\n\n // Normalize roles: some providers send a single string instead of an array\n const rawRoles = idToken[rolesClaim];\n const nonArrayRoles = typeof rawRoles === 'string' ? [rawRoles] : [];\n const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;\n\n logger.debug({ sub: idToken.sub, rolesClaim, rolesCount: roles.length }, 'Extracted user from ID token');\n\n return {\n sub: idToken.sub ?? '',\n name: idToken.name as string,\n email: idToken.email as string,\n roles,\n };\n}\n\n/**\n * Builds a Session object from an OIDC token response.\n * Maps provider tokens and user claims into the internal Session format.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): Session {\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n const userInfo = extractUserFromTokens(tokens, rolesClaim);\n\n let scopes: string[] | undefined;\n if (typeof tokens.scope === 'string') {\n const parsed = tokens.scope.split(' ').filter(s => s.length > 0);\n if (parsed.length > 0) scopes = parsed;\n }\n\n return {\n // Minted once here and carried across refreshes, so every log line for this\n // session shares one correlation id.\n sid: newSessionId(),\n tokens: {\n accessToken: tokens.access_token,\n accessTokenExpiresAt: tokens.expires_in\n ? new Date(Date.now() + tokens.expires_in * 1000).toISOString()\n : undefined,\n refreshToken: tokens.refresh_token,\n // Kept for `id_token_hint` on RP-initiated logout.\n idToken: tokens.id_token,\n },\n user: {\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n}\n\n/**\n * Validates a return URL against open redirect attacks.\n * Ensures the URL is same-origin and passes optional custom validation.\n *\n * @param returnTo - The raw returnTo path/URL to validate\n * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')\n * @param defaultPath - Fallback path if validation fails (default: '/')\n * @param validate - Optional custom validation function for the pathname\n * @returns A safe, validated URL\n */\nexport function validateReturnUrl(\n returnTo: string | null | undefined,\n applicationOrigin: string,\n defaultPath: string = '/',\n validate?: (pathname: string) => boolean,\n): URL {\n const redirectPath = returnTo ?? defaultPath;\n let redirectUrl: URL;\n\n try {\n redirectUrl = new URL(redirectPath, applicationOrigin);\n } catch {\n logger.warn({ returnTo: redirectPath }, 'Invalid returnTo URL, using default');\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Enforce same-origin — cross-origin returnTo is a potential open redirect attack\n const originUrl = new URL(applicationOrigin);\n if (redirectUrl.origin !== originUrl.origin) {\n logger.error(\n { returnTo: redirectPath, redirectOrigin: redirectUrl.origin, applicationOrigin: originUrl.origin },\n 'Possible open redirect attack: returnTo points outside application origin',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Custom validation\n if (validate && !validate(redirectUrl.pathname)) {\n logger.warn(\n { pathname: redirectUrl.pathname },\n 'returnTo failed custom validation, falling back to default',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n return redirectUrl;\n}\n\n/** Parameters for an RP-initiated logout request (OIDC RP-Initiated Logout 1.0 §2). */\nexport interface OidcLogoutOptions {\n /** Post-logout redirect URI. Must be registered with the provider. */\n postLogoutRedirectUri?: string;\n\n /**\n * Raw ID token from `session.tokens.idToken`, sent as `id_token_hint`. Tells the\n * OP which session to end, and is usually what lets it skip the confirmation\n * interstitial.\n */\n idTokenHint?: string;\n\n /** Opaque value echoed back on the post-logout redirect. */\n state?: string;\n\n /** `logout_hint`, when the provider documents one. */\n logoutHint?: string;\n\n /**\n * Whether to send `client_id`. Default true, also alongside `id_token_hint`: the\n * spec allows both (the OP must then verify they match), and `client_id` is often\n * what lets the OP accept `post_logout_redirect_uri` when the hint is stale or\n * absent. Set false only for a provider that documents rejecting the combination.\n */\n includeClientId?: boolean;\n}\n\n/**\n * Discovers the OIDC provider's end_session_endpoint and builds an RP-initiated\n * logout URL. Returns null when the provider exposes none, so callers can fall\n * back to clearing the local session only.\n *\n * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)\n * @param options - Logout parameters, or a string as shorthand for\n * `postLogoutRedirectUri`\n * @returns Logout URL or null if not supported\n *\n * @example\n * ```typescript\n * const session = await readSession(cookies, secret);\n * const logoutUrl = await buildOidcLogoutUrl(oauthConfig, {\n * postLogoutRedirectUri: 'https://app.example.com/',\n * idTokenHint: session?.tokens?.idToken,\n * });\n * ```\n */\nexport async function buildOidcLogoutUrl(\n oauthConfig: OAuthConfig,\n options: OidcLogoutOptions | string,\n): Promise<URL | null> {\n const {\n postLogoutRedirectUri,\n idTokenHint,\n state,\n logoutHint,\n includeClientId = true,\n } = typeof options === 'string' ? { postLogoutRedirectUri: options } : options;\n\n try {\n const config = await discoverConfiguration(oauthConfig);\n const endSessionEndpoint = config.serverMetadata().end_session_endpoint;\n\n if (!endSessionEndpoint) {\n logger.info({ issuer: oauthConfig.issuer }, 'No end_session_endpoint found');\n return null;\n }\n\n const parameters: Record<string, string> = {};\n if (postLogoutRedirectUri) parameters.post_logout_redirect_uri = postLogoutRedirectUri;\n if (idTokenHint) parameters.id_token_hint = idTokenHint;\n if (state) parameters.state = state;\n if (logoutHint) parameters.logout_hint = logoutHint;\n\n // buildEndSessionUrl always adds client_id, and deleting it afterwards would\n // also drop one the provider baked into its own endpoint — so when it isn't\n // wanted, assemble the URL directly instead.\n let logoutUrl: URL;\n if (includeClientId) {\n logoutUrl = openid.buildEndSessionUrl(config, parameters);\n } else {\n logoutUrl = new URL(endSessionEndpoint);\n\n // openid-client rejects a non-https endpoint on the path above; the manual\n // one must too. The URL carries a raw id_token_hint, so a discovery document\n // that downgrades the scheme would put it on the wire in the clear.\n if (logoutUrl.protocol !== 'https:') {\n logger.error(\n { issuer: oauthConfig.issuer, protocol: logoutUrl.protocol },\n 'end_session_endpoint is not https — refusing to build a logout URL',\n );\n return null;\n }\n\n for (const [key, value] of Object.entries(parameters)) {\n logoutUrl.searchParams.append(key, value);\n }\n }\n\n logger.debug(\n { endSessionEndpoint, hasIdTokenHint: !!idTokenHint, hasState: !!state },\n 'Built OIDC logout URL',\n );\n return logoutUrl;\n } catch (error) {\n logger.warn({ error, issuer: oauthConfig.issuer }, 'OIDC discovery failed for logout');\n return null;\n }\n}\n\n/**\n * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.\n *\n * Uses client_secret_post against the given token endpoint. The endpoint must be\n * https — the request carries the client secret.\n *\n * @param config - Client credentials configuration including token endpoint, client ID, and secret\n * @returns Token response from the OAuth provider\n * @throws Error if the endpoint is not https, the request times out, or the grant fails\n *\n * @example\n * ```typescript\n * const tokens = await clientCredentialsGrant({\n * tokenEndpoint: 'https://api.example.com/oauth2/token',\n * clientId: 'my-client-id',\n * clientSecret: 'my-client-secret',\n * scope: 'api:read api:write',\n * });\n * ```\n */\nexport async function clientCredentialsGrant(\n config: ClientCredentialsConfig,\n): Promise<openid.TokenEndpointResponse> {\n try {\n // Build the configuration from metadata rather than discovering it: callers\n // give us a token endpoint, not an issuer. Going through openid-client anyway\n // buys the https guard and the timeout, which a raw fetch of a caller-supplied\n // URL carrying a client secret would not have.\n const openidConfig = new openid.Configuration(\n {\n issuer: config.issuer ?? new URL(config.tokenEndpoint).origin,\n token_endpoint: config.tokenEndpoint,\n },\n config.clientId,\n config.clientSecret,\n // Explicit rather than relying on openid-client's default, matching the\n // discovery calls above: the JSDoc promises client_secret_post.\n openid.ClientSecretPost(config.clientSecret),\n );\n openidConfig.timeout = config.timeout ?? 30;\n\n const tokens = await openid.clientCredentialsGrant(\n openidConfig,\n config.scope ? { scope: config.scope } : undefined,\n );\n\n logger.debug({ tokenEndpoint: config.tokenEndpoint }, 'Client credentials grant successful');\n return tokens;\n } catch (error) {\n logger.error(\n { ...getOAuthErrorLogContext(error), tokenEndpoint: config.tokenEndpoint },\n 'Client credentials grant failed',\n );\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as openid from 'openid-client';
|
|
2
|
+
import type { OAuthConfig } from './oauth';
|
|
3
|
+
/** Options for {@link configureDiscoveryCache}. */
|
|
4
|
+
export interface DiscoveryCacheOptions {
|
|
5
|
+
/**
|
|
6
|
+
* How long a discovered configuration stays reusable, in milliseconds.
|
|
7
|
+
* Pass `0` to disable caching and discover on every call. Must be finite and
|
|
8
|
+
* non-negative; anything else throws rather than quietly disabling the cache.
|
|
9
|
+
* @default 300_000 (5 minutes)
|
|
10
|
+
*/
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Adjusts the discovery cache. Existing entries are dropped so the new setting
|
|
15
|
+
* takes effect immediately.
|
|
16
|
+
*
|
|
17
|
+
* @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting
|
|
18
|
+
* loudly matters here because the alternative is silent: `NaN` — what
|
|
19
|
+
* `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would
|
|
20
|
+
* otherwise turn the cache off and restore a discovery round-trip on every
|
|
21
|
+
* token operation, with nothing to show for it in the logs.
|
|
22
|
+
*/
|
|
23
|
+
export declare function configureDiscoveryCache(options: DiscoveryCacheOptions): void;
|
|
24
|
+
/**
|
|
25
|
+
* Drops every cached configuration.
|
|
26
|
+
*
|
|
27
|
+
* Call this after changing provider configuration that discovery would not
|
|
28
|
+
* otherwise pick up until the TTL expires. Tests should call it between cases,
|
|
29
|
+
* since the cache is module state.
|
|
30
|
+
*/
|
|
31
|
+
export declare function clearDiscoveryCache(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Discovers the provider's OpenID configuration, reusing a recent result.
|
|
34
|
+
*
|
|
35
|
+
* Every fides-auth flow goes through here, so this is also the single place
|
|
36
|
+
* where the client authentication method is decided.
|
|
37
|
+
*
|
|
38
|
+
* @param config - OAuth configuration identifying the provider and client.
|
|
39
|
+
* @returns The openid-client `Configuration` for this provider and client.
|
|
40
|
+
*/
|
|
41
|
+
export declare function discoverConfiguration(config: OAuthConfig): Promise<openid.Configuration>;
|
|
42
|
+
//# sourceMappingURL=oidc-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-discovery.d.ts","sourceRoot":"","sources":["../src/oidc-discovery.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAiC3C,mDAAmD;AACnD,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAU5E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C;AAWD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CAgC/B"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
2
|
+
import * as openid from "openid-client";
|
|
3
|
+
//#region src/oidc-discovery.ts
|
|
4
|
+
var logger = createLogger({ namespace: "fides-auth:oidc-discovery" });
|
|
5
|
+
/**
|
|
6
|
+
* Default lifetime of a cached configuration.
|
|
7
|
+
*
|
|
8
|
+
* Discovery documents change rarely — a new endpoint, a signing-key rollover —
|
|
9
|
+
* and openid-client refetches JWKS on its own schedule, so this only bounds how
|
|
10
|
+
* long a *metadata* change takes to be noticed. Five minutes keeps that window
|
|
11
|
+
* short while removing essentially every discovery request from the hot path.
|
|
12
|
+
*/
|
|
13
|
+
var DEFAULT_TTL_MS = 3e5;
|
|
14
|
+
var cache = /* @__PURE__ */ new Map();
|
|
15
|
+
var ttlMs = DEFAULT_TTL_MS;
|
|
16
|
+
/**
|
|
17
|
+
* Adjusts the discovery cache. Existing entries are dropped so the new setting
|
|
18
|
+
* takes effect immediately.
|
|
19
|
+
*
|
|
20
|
+
* @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting
|
|
21
|
+
* loudly matters here because the alternative is silent: `NaN` — what
|
|
22
|
+
* `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would
|
|
23
|
+
* otherwise turn the cache off and restore a discovery round-trip on every
|
|
24
|
+
* token operation, with nothing to show for it in the logs.
|
|
25
|
+
*/
|
|
26
|
+
function configureDiscoveryCache(options) {
|
|
27
|
+
if (options.ttlMs !== void 0) {
|
|
28
|
+
if (!Number.isFinite(options.ttlMs) || options.ttlMs < 0) throw new TypeError(`configureDiscoveryCache: ttlMs must be a finite number >= 0, got ${options.ttlMs}`);
|
|
29
|
+
ttlMs = options.ttlMs;
|
|
30
|
+
}
|
|
31
|
+
cache.clear();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Drops every cached configuration.
|
|
35
|
+
*
|
|
36
|
+
* Call this after changing provider configuration that discovery would not
|
|
37
|
+
* otherwise pick up until the TTL expires. Tests should call it between cases,
|
|
38
|
+
* since the cache is module state.
|
|
39
|
+
*/
|
|
40
|
+
function clearDiscoveryCache() {
|
|
41
|
+
cache.clear();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Issuer and client identify a configuration; the secret is checked separately.
|
|
45
|
+
* JSON-encoded rather than concatenated so no separator can be forged by a
|
|
46
|
+
* value that happens to contain it.
|
|
47
|
+
*/
|
|
48
|
+
function cacheKey(config) {
|
|
49
|
+
return JSON.stringify([config.issuer, config.clientId]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Discovers the provider's OpenID configuration, reusing a recent result.
|
|
53
|
+
*
|
|
54
|
+
* Every fides-auth flow goes through here, so this is also the single place
|
|
55
|
+
* where the client authentication method is decided.
|
|
56
|
+
*
|
|
57
|
+
* @param config - OAuth configuration identifying the provider and client.
|
|
58
|
+
* @returns The openid-client `Configuration` for this provider and client.
|
|
59
|
+
*/
|
|
60
|
+
async function discoverConfiguration(config) {
|
|
61
|
+
const key = cacheKey(config);
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
const cached = cache.get(key);
|
|
64
|
+
if (cached && cached.expiresAt > now && cached.clientSecret === config.clientSecret) return cached.configuration;
|
|
65
|
+
logger.debug({ issuer: config.issuer }, "Discovering OpenID configuration");
|
|
66
|
+
const configuration = openid.discovery(new URL(config.issuer), config.clientId, config.clientSecret, openid.ClientSecretPost(config.clientSecret));
|
|
67
|
+
if (ttlMs > 0) {
|
|
68
|
+
cache.set(key, {
|
|
69
|
+
configuration,
|
|
70
|
+
expiresAt: now + ttlMs,
|
|
71
|
+
clientSecret: config.clientSecret
|
|
72
|
+
});
|
|
73
|
+
configuration.catch(() => {
|
|
74
|
+
if (cache.get(key)?.configuration === configuration) cache.delete(key);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return configuration;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { clearDiscoveryCache, configureDiscoveryCache, discoverConfiguration };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=oidc-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-discovery.js","names":[],"sources":["../src/oidc-discovery.ts"],"sourcesContent":["// oidc-discovery.ts\n//\n// One place where fides-auth turns an OAuthConfig into an openid-client\n// Configuration, and one cache in front of it.\n//\n// Discovery was previously performed inline at every call site, which meant an\n// HTTP GET to the provider's /.well-known/openid-configuration before *every*\n// token operation. On the heartbeat path that is one extra round-trip per user\n// every few minutes: it doubles the latency of a refresh, doubles the load on\n// the provider, and doubles the surface on which a refresh can fail — a\n// discovery blip is indistinguishable from a token-endpoint blip to the caller.\n//\n// Reusing a Configuration is openid-client's intended usage; its own examples\n// build one at startup and keep it.\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport type { OAuthConfig } from './oauth';\n\nconst logger = createLogger({ namespace: 'fides-auth:oidc-discovery' });\n\n/**\n * Default lifetime of a cached configuration.\n *\n * Discovery documents change rarely — a new endpoint, a signing-key rollover —\n * and openid-client refetches JWKS on its own schedule, so this only bounds how\n * long a *metadata* change takes to be noticed. Five minutes keeps that window\n * short while removing essentially every discovery request from the hot path.\n */\nconst DEFAULT_TTL_MS = 5 * 60_000;\n\ninterface CacheEntry {\n /**\n * Held as the promise, not the resolved value, so concurrent callers share one\n * round-trip. Caching only the result would let a burst of requests each start\n * their own discovery before the first finished — precisely what a fleet of\n * heartbeats produces.\n */\n configuration: Promise<openid.Configuration>;\n expiresAt: number;\n /**\n * The secret this entry was built with, compared on lookup so a rotated client\n * secret invalidates the entry without the secret going into the cache key.\n */\n clientSecret: string;\n}\n\nconst cache = new Map<string, CacheEntry>();\nlet ttlMs = DEFAULT_TTL_MS;\n\n/** Options for {@link configureDiscoveryCache}. */\nexport interface DiscoveryCacheOptions {\n /**\n * How long a discovered configuration stays reusable, in milliseconds.\n * Pass `0` to disable caching and discover on every call. Must be finite and\n * non-negative; anything else throws rather than quietly disabling the cache.\n * @default 300_000 (5 minutes)\n */\n ttlMs?: number;\n}\n\n/**\n * Adjusts the discovery cache. Existing entries are dropped so the new setting\n * takes effect immediately.\n *\n * @throws TypeError if `ttlMs` is not a finite, non-negative number. Rejecting\n * loudly matters here because the alternative is silent: `NaN` — what\n * `Number(process.env.DISCOVERY_TTL)` yields when the variable is unset — would\n * otherwise turn the cache off and restore a discovery round-trip on every\n * token operation, with nothing to show for it in the logs.\n */\nexport function configureDiscoveryCache(options: DiscoveryCacheOptions): void {\n if (options.ttlMs !== undefined) {\n if (!Number.isFinite(options.ttlMs) || options.ttlMs < 0) {\n throw new TypeError(\n `configureDiscoveryCache: ttlMs must be a finite number >= 0, got ${options.ttlMs}`,\n );\n }\n ttlMs = options.ttlMs;\n }\n cache.clear();\n}\n\n/**\n * Drops every cached configuration.\n *\n * Call this after changing provider configuration that discovery would not\n * otherwise pick up until the TTL expires. Tests should call it between cases,\n * since the cache is module state.\n */\nexport function clearDiscoveryCache(): void {\n cache.clear();\n}\n\n/**\n * Issuer and client identify a configuration; the secret is checked separately.\n * JSON-encoded rather than concatenated so no separator can be forged by a\n * value that happens to contain it.\n */\nfunction cacheKey(config: OAuthConfig): string {\n return JSON.stringify([config.issuer, config.clientId]);\n}\n\n/**\n * Discovers the provider's OpenID configuration, reusing a recent result.\n *\n * Every fides-auth flow goes through here, so this is also the single place\n * where the client authentication method is decided.\n *\n * @param config - OAuth configuration identifying the provider and client.\n * @returns The openid-client `Configuration` for this provider and client.\n */\nexport async function discoverConfiguration(\n config: OAuthConfig,\n): Promise<openid.Configuration> {\n const key = cacheKey(config);\n const now = Date.now();\n const cached = cache.get(key);\n\n if (cached && cached.expiresAt > now && cached.clientSecret === config.clientSecret) {\n return cached.configuration;\n }\n\n logger.debug({ issuer: config.issuer }, 'Discovering OpenID configuration');\n\n const configuration = openid.discovery(\n new URL(config.issuer),\n config.clientId,\n config.clientSecret,\n // openid-client's own default is client_secret_post; stating it keeps the\n // method visible at the one place that would grow a `clientAuth` option.\n openid.ClientSecretPost(config.clientSecret),\n );\n\n if (ttlMs > 0) {\n cache.set(key, { configuration, expiresAt: now + ttlMs, clientSecret: config.clientSecret });\n\n // Never cache a failure: a provider that was briefly unreachable must be\n // retried on the next call, not remembered as broken for the whole TTL.\n // Guarded so a slow failure cannot evict a newer entry that replaced it.\n configuration.catch(() => {\n if (cache.get(key)?.configuration === configuration) cache.delete(key);\n });\n }\n\n return configuration;\n}\n"],"mappings":";;;AAoBA,IAAM,SAAS,aAAa,EAAE,WAAW,4BAA4B,CAAC;;;;;;;;;AAUtE,IAAM,iBAAiB;AAkBvB,IAAM,wBAAQ,IAAI,IAAwB;AAC1C,IAAI,QAAQ;;;;;;;;;;;AAuBZ,SAAgB,wBAAwB,SAAsC;CAC5E,IAAI,QAAQ,UAAU,KAAA,GAAW;EAC/B,IAAI,CAAC,OAAO,SAAS,QAAQ,KAAK,KAAK,QAAQ,QAAQ,GACrD,MAAM,IAAI,UACR,oEAAoE,QAAQ,OAC9E;EAEF,QAAQ,QAAQ;CAClB;CACA,MAAM,MAAM;AACd;;;;;;;;AASA,SAAgB,sBAA4B;CAC1C,MAAM,MAAM;AACd;;;;;;AAOA,SAAS,SAAS,QAA6B;CAC7C,OAAO,KAAK,UAAU,CAAC,OAAO,QAAQ,OAAO,QAAQ,CAAC;AACxD;;;;;;;;;;AAWA,eAAsB,sBACpB,QAC+B;CAC/B,MAAM,MAAM,SAAS,MAAM;CAC3B,MAAM,MAAM,KAAK,IAAI;CACrB,MAAM,SAAS,MAAM,IAAI,GAAG;CAE5B,IAAI,UAAU,OAAO,YAAY,OAAO,OAAO,iBAAiB,OAAO,cACrE,OAAO,OAAO;CAGhB,OAAO,MAAM,EAAE,QAAQ,OAAO,OAAO,GAAG,kCAAkC;CAE1E,MAAM,gBAAgB,OAAO,UAC3B,IAAI,IAAI,OAAO,MAAM,GACrB,OAAO,UACP,OAAO,cAGP,OAAO,iBAAiB,OAAO,YAAY,CAC7C;CAEA,IAAI,QAAQ,GAAG;EACb,MAAM,IAAI,KAAK;GAAE;GAAe,WAAW,MAAM;GAAO,cAAc,OAAO;EAAa,CAAC;EAK3F,cAAc,YAAY;GACxB,IAAI,MAAM,IAAI,GAAG,CAAC,EAAE,kBAAkB,eAAe,MAAM,OAAO,GAAG;EACvE,CAAC;CACH;CAEA,OAAO;AACT"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { PKCEOptions } from '../../oauth';
|
|
2
|
-
import { VippsLoginConfig, VippsUserInfo } from './types';
|
|
3
1
|
/**
|
|
4
2
|
* Vipps Login Client
|
|
5
3
|
*
|
|
@@ -7,6 +5,8 @@ import { VippsLoginConfig, VippsUserInfo } from './types';
|
|
|
7
5
|
* Implements OAuth/OIDC operations with PKCE for Vipps.
|
|
8
6
|
*/
|
|
9
7
|
import * as openid from 'openid-client';
|
|
8
|
+
import { type PKCEOptions } from '../../oauth';
|
|
9
|
+
import { type VippsLoginConfig, type VippsUserInfo } from './types';
|
|
10
10
|
/**
|
|
11
11
|
* Vipps Login Client
|
|
12
12
|
*
|
|
@@ -15,11 +15,14 @@ import * as openid from 'openid-client';
|
|
|
15
15
|
export declare class VippsLoginClient {
|
|
16
16
|
private readonly config;
|
|
17
17
|
private readonly oauthConfig;
|
|
18
|
-
private configPromise;
|
|
19
18
|
constructor(config: VippsLoginConfig);
|
|
20
19
|
/**
|
|
21
|
-
* Get OpenID Configuration
|
|
22
|
-
*
|
|
20
|
+
* Get the OpenID Configuration, shared with the rest of fides-auth.
|
|
21
|
+
*
|
|
22
|
+
* Previously memoized on the instance, which cached rejections too: one failed
|
|
23
|
+
* discovery — a restart during a provider blip — left every later call
|
|
24
|
+
* rejecting for the lifetime of the client. {@link discoverConfiguration}
|
|
25
|
+
* caches successes with a TTL and drops failures.
|
|
23
26
|
*/
|
|
24
27
|
private getConfig;
|
|
25
28
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,SAAS,CAAC;AAOjB;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAE1C,YAAY,MAAM,EAAE,gBAAgB,EAsBnC;IAED;;;;;;;OAOG;YACW,SAAS;IAIvB;;;;OAIG;IACG,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC,CAG7C;IAED;;;;;OAKG;IACG,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAIlE;IAED;;;;;;;OAOG;IACG,qBAAqB,CACzB,WAAW,EAAE,MAAM,GAAG,GAAG,EACzB,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CA8BvC;IAED;;;;;OAKG;IACG,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAoD7D;IAED;;;;;OAKG;IACG,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAqB/E;CACF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "../../logger.js";
|
|
2
|
+
import { discoverConfiguration } from "../../oidc-discovery.js";
|
|
2
3
|
import { buildAuthorizationUrl, buildPKCEOptions } from "../../oauth.js";
|
|
3
4
|
import * as openid from "openid-client";
|
|
4
5
|
//#region src/providers/vipps/types.ts
|
|
@@ -64,7 +65,6 @@ var logger = createLogger({
|
|
|
64
65
|
var VippsLoginClient = class {
|
|
65
66
|
config;
|
|
66
67
|
oauthConfig;
|
|
67
|
-
configPromise = null;
|
|
68
68
|
constructor(config) {
|
|
69
69
|
this.config = {
|
|
70
70
|
...config,
|
|
@@ -84,12 +84,15 @@ var VippsLoginClient = class {
|
|
|
84
84
|
}, "VippsLoginClient initialized");
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* Get OpenID Configuration
|
|
88
|
-
*
|
|
87
|
+
* Get the OpenID Configuration, shared with the rest of fides-auth.
|
|
88
|
+
*
|
|
89
|
+
* Previously memoized on the instance, which cached rejections too: one failed
|
|
90
|
+
* discovery — a restart during a provider blip — left every later call
|
|
91
|
+
* rejecting for the lifetime of the client. {@link discoverConfiguration}
|
|
92
|
+
* caches successes with a TTL and drops failures.
|
|
89
93
|
*/
|
|
90
94
|
async getConfig() {
|
|
91
|
-
|
|
92
|
-
return this.configPromise;
|
|
95
|
+
return discoverConfiguration(this.oauthConfig);
|
|
93
96
|
}
|
|
94
97
|
/**
|
|
95
98
|
* Build PKCE options for authorization flow
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n private configPromise: Promise<openid.Configuration> | null = null;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get OpenID Configuration (cached)\n * This ensures we use the same Configuration instance across all operations\n */\n private async getConfig(): Promise<openid.Configuration> {\n if (!this.configPromise) {\n this.configPromise = openid.discovery(\n new URL(this.oauthConfig.issuer),\n this.oauthConfig.clientId,\n this.oauthConfig.clientSecret,\n openid.ClientSecretPost(this.oauthConfig.clientSecret)\n );\n }\n return this.configPromise;\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport { discoverConfiguration } from '../../oidc-discovery';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get the OpenID Configuration, shared with the rest of fides-auth.\n *\n * Previously memoized on the instance, which cached rejections too: one failed\n * discovery — a restart during a provider blip — left every later call\n * rejecting for the lifetime of the client. {@link discoverConfiguration}\n * caches successes with a TTL and drops failures.\n */\n private async getConfig(): Promise<openid.Configuration> {\n return discoverConfiguration(this.oauthConfig);\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -75,17 +75,17 @@ export interface VippsUserInfo {
|
|
|
75
75
|
*/
|
|
76
76
|
export declare const VippsLoginScopes: {
|
|
77
77
|
/** Required: OpenID Connect base scope */
|
|
78
|
-
readonly OpenId:
|
|
78
|
+
readonly OpenId: 'openid';
|
|
79
79
|
/** User's name (given_name, family_name, name) */
|
|
80
|
-
readonly Name:
|
|
80
|
+
readonly Name: 'name';
|
|
81
81
|
/** Phone number */
|
|
82
|
-
readonly PhoneNumber:
|
|
82
|
+
readonly PhoneNumber: 'phoneNumber';
|
|
83
83
|
/** Physical address */
|
|
84
|
-
readonly Address:
|
|
84
|
+
readonly Address: 'address';
|
|
85
85
|
/** Birth date */
|
|
86
|
-
readonly BirthDate:
|
|
86
|
+
readonly BirthDate: 'birthDate';
|
|
87
87
|
/** Email address */
|
|
88
|
-
readonly Email:
|
|
88
|
+
readonly Email: 'email';
|
|
89
89
|
};
|
|
90
90
|
/**
|
|
91
91
|
* Default scopes for Vipps Login
|
|
@@ -96,9 +96,9 @@ export declare const defaultVippsLoginScope: string;
|
|
|
96
96
|
*/
|
|
97
97
|
export declare const VippsEnvironments: {
|
|
98
98
|
/** Test environment */
|
|
99
|
-
readonly Test:
|
|
99
|
+
readonly Test: 'https://apitest.vipps.no';
|
|
100
100
|
/** Production environment */
|
|
101
|
-
readonly Production:
|
|
101
|
+
readonly Production: 'https://api.vipps.no';
|
|
102
102
|
};
|
|
103
103
|
/**
|
|
104
104
|
* Get Vipps OIDC issuer URL for environment
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,gCAAgC;IAChC,eAAe,CAAC,EAAE,YAAY,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B,0CAA0C
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/providers/vipps/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4FAA4F;IAC5F,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8BAA8B;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,gCAAgC;IAChC,eAAe,CAAC,EAAE,YAAY,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB;IAC3B,0CAA0C;aAC1C,MAAM,EAAE,QAAQ;IAChB,kDAAkD;aAClD,IAAI,EAAE,MAAM;IACZ,mBAAmB;aACnB,WAAW,EAAE,aAAa;IAC1B,uBAAuB;aACvB,OAAO,EAAE,SAAS;IAClB,iBAAiB;aACjB,SAAS,EAAE,WAAW;IACtB,oBAAoB;aACpB,KAAK,EAAE,OAAO;CACN,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,sBAAsB,QAMxB,CAAC;AAEZ;;GAEG;AACH,eAAO,MAAM,iBAAiB;IAC5B,uBAAuB;aACvB,IAAI,EAAE,0BAA0B;IAChC,6BAA6B;aAC7B,UAAU,EAAE,sBAAsB;CAC1B,CAAC;AAEX;;GAEG;AACH,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG1D"}
|
package/dist/rate-limit.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,WAAW,CAAC,IAAI;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,WAAW,CAAC,IAAI;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,qBAAqB,EAAE,MAAM,CAAC;IAErC,YAAY,GAAG,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,EAGrD;IAED,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IAEnD,wEAAwE;IACjE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAW7C;IAED,0FAA0F;IACnF,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAuB/C;CACF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CookieOptions } from '../cookies';
|
|
1
|
+
import type { CookieOptions } from '../cookies';
|
|
2
2
|
export interface CookieStore {
|
|
3
3
|
/** Returns the cookie's value, or null/undefined when it is not set. */
|
|
4
4
|
get(name: string): string | null | undefined | Promise<string | null | undefined>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from '../oauth';
|
|
2
|
-
import { CookieStore } from './cookie-store';
|
|
1
|
+
import type { OAuthConfig } from '../oauth';
|
|
2
|
+
import type { CookieStore } from './cookie-store';
|
|
3
3
|
export interface HeartbeatHandlerConfig {
|
|
4
4
|
/** OAuth/OIDC configuration used to refresh the access token. */
|
|
5
5
|
oauthConfig: OAuthConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from '../oauth';
|
|
2
|
-
import { CookieStore } from './cookie-store';
|
|
1
|
+
import type { OAuthConfig } from '../oauth';
|
|
2
|
+
import type { CookieStore } from './cookie-store';
|
|
3
3
|
export interface OidcCallbackConfig {
|
|
4
4
|
/** OAuth/OIDC configuration. */
|
|
5
5
|
oauthConfig: OAuthConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from '../oauth';
|
|
2
|
-
import { CookieStore } from './cookie-store';
|
|
1
|
+
import type { OAuthConfig } from '../oauth';
|
|
2
|
+
import type { CookieStore } from './cookie-store';
|
|
3
3
|
export interface OidcLoginConfig {
|
|
4
4
|
/** OAuth/OIDC configuration. */
|
|
5
5
|
oauthConfig: OAuthConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from '../oauth';
|
|
2
|
-
import { CookieStore } from './cookie-store';
|
|
1
|
+
import type { OAuthConfig } from '../oauth';
|
|
2
|
+
import type { CookieStore } from './cookie-store';
|
|
3
3
|
export interface OidcLogoutConfig {
|
|
4
4
|
/** OAuth/OIDC configuration. */
|
|
5
5
|
oauthConfig: OAuthConfig;
|
package/dist/server/session.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { OAuthConfig } from '../oauth';
|
|
2
|
-
import { SESSION_EVENT, RefreshFailureCause, SessionClearedTrigger, SessionRejectedReason } from '../session-events';
|
|
3
|
-
import { CreateSessionOptions, Session } from '../types';
|
|
4
|
-
import { CookieStore } from './cookie-store';
|
|
1
|
+
import type { OAuthConfig } from '../oauth';
|
|
2
|
+
import { SESSION_EVENT, type RefreshFailureCause, type SessionClearedTrigger, type SessionRejectedReason } from '../session-events';
|
|
3
|
+
import type { CreateSessionOptions, Session } from '../types';
|
|
4
|
+
import type { CookieStore } from './cookie-store';
|
|
5
5
|
type Secret = string | Uint8Array;
|
|
6
6
|
/** How {@link persistSession} should report what it just wrote. */
|
|
7
7
|
export interface PersistSessionOptions {
|
package/dist/session-events.d.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { FidesLogger } from './logger';
|
|
1
|
+
import type { FidesLogger } from './logger';
|
|
2
2
|
/**
|
|
3
3
|
* Event names. Use these constants rather than string literals so a rename is a
|
|
4
4
|
* compile error at every call site instead of a silently broken dashboard.
|
|
5
5
|
*/
|
|
6
6
|
export declare const SESSION_EVENT: {
|
|
7
7
|
/** A session was created from a token response (login, or client-credentials grant). */
|
|
8
|
-
readonly CREATED:
|
|
8
|
+
readonly CREATED: 'session.created';
|
|
9
9
|
/** An existing session was renewed with a fresh access token. */
|
|
10
|
-
readonly REFRESHED:
|
|
10
|
+
readonly REFRESHED: 'session.refreshed';
|
|
11
11
|
/** A request could not proceed with the session it had. Carries a {@link SessionRejectedReason}. */
|
|
12
|
-
readonly REJECTED:
|
|
12
|
+
readonly REJECTED: 'session.rejected';
|
|
13
13
|
/** A refresh attempt failed. Carries a {@link RefreshFailureCause}. */
|
|
14
|
-
readonly REFRESH_FAILED:
|
|
14
|
+
readonly REFRESH_FAILED: 'session.refresh_failed';
|
|
15
15
|
/** Session cookies were deleted. Carries a {@link SessionClearedTrigger}. */
|
|
16
|
-
readonly CLEARED:
|
|
16
|
+
readonly CLEARED: 'session.cleared';
|
|
17
17
|
};
|
|
18
18
|
export type SessionEventName = (typeof SESSION_EVENT)[keyof typeof SESSION_EVENT];
|
|
19
19
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,aAAa;IACxB,wFAAwF
|
|
1
|
+
{"version":3,"file":"session-events.d.ts","sourceRoot":"","sources":["../src/session-events.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { OAuthConfig } from
|
|
2
|
-
import { CreateSessionOptions, Session } from
|
|
1
|
+
import { OAuthConfig } from "./oauth";
|
|
2
|
+
import { CreateSessionOptions, Session } from "./types";
|
|
3
3
|
/**
|
|
4
4
|
* Refreshes an existing session by exchanging its refresh token for new tokens.
|
|
5
5
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,oBACR,OAAO,UAChB,WAAW,YACV,oBAAoB,KAC5B,OAAO,CAAC,OAAO,GAAG,IAAI,CAgDxB,CAAC"}
|
package/dist/silent-login.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Silent login functionality for OAuth flows.
|
|
3
|
+
*
|
|
4
|
+
* Silent login (also called "prompt=none") attempts to authenticate the user
|
|
5
|
+
* without any user interaction. This is useful for:
|
|
6
|
+
* - Checking if a user has an active SSO session
|
|
7
|
+
* - Refreshing authentication silently in the background
|
|
8
|
+
* - Implementing "keep me logged in" features
|
|
9
|
+
*/
|
|
1
10
|
import { OAuthConfig } from './oauth';
|
|
2
11
|
export interface SilentLoginOptions {
|
|
3
12
|
/** Optional hint about the user's identity (e.g., email or user ID) */
|
|
@@ -78,10 +87,10 @@ export declare function checkSilentLoginResult(callbackUrl: URL): SilentLoginRes
|
|
|
78
87
|
* - account_selection_required: User must select an account
|
|
79
88
|
*/
|
|
80
89
|
export declare const SilentLoginErrors: {
|
|
81
|
-
readonly LOGIN_REQUIRED:
|
|
82
|
-
readonly INTERACTION_REQUIRED:
|
|
83
|
-
readonly CONSENT_REQUIRED:
|
|
84
|
-
readonly ACCOUNT_SELECTION_REQUIRED:
|
|
90
|
+
readonly LOGIN_REQUIRED: 'login_required';
|
|
91
|
+
readonly INTERACTION_REQUIRED: 'interaction_required';
|
|
92
|
+
readonly CONSENT_REQUIRED: 'consent_required';
|
|
93
|
+
readonly ACCOUNT_SELECTION_REQUIRED: 'account_selection_required';
|
|
85
94
|
};
|
|
86
95
|
export type SilentLoginError = typeof SilentLoginErrors[keyof typeof SilentLoginErrors];
|
|
87
96
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"silent-login.d.ts","sourceRoot":"","sources":["../src/silent-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"silent-login.d.ts","sourceRoot":"","sources":["../src/silent-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAKtC,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IAChC,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,GAAG,CAAC;IACvB,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,WAAW,EACnB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,GAAG,CAAC,CA+Bd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,GAAG,GAAG,iBAAiB,CA4B1E;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;aAC5B,cAAc,EAAE,gBAAgB;aAChC,oBAAoB,EAAE,sBAAsB;aAC5C,gBAAgB,EAAE,kBAAkB;aACpC,0BAA0B,EAAE,4BAA4B;CAChD,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,OAAO,iBAAiB,CAAC,MAAM,OAAO,iBAAiB,CAAC,CAAC;AAExF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAI3E"}
|
package/dist/silent-login.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
|
+
import { discoverConfiguration } from "./oidc-discovery.js";
|
|
2
3
|
import * as openid from "openid-client";
|
|
3
4
|
//#region src/silent-login.ts
|
|
4
5
|
/**
|
|
@@ -35,7 +36,7 @@ var logger = createLogger({ namespace: "fides-auth:silent-login" });
|
|
|
35
36
|
async function buildSilentLoginUrl(config, options = {}) {
|
|
36
37
|
logger.debug({ issuer: config.issuer }, "Building silent login URL");
|
|
37
38
|
try {
|
|
38
|
-
const server = await
|
|
39
|
+
const server = await discoverConfiguration(config);
|
|
39
40
|
const parameters = {
|
|
40
41
|
redirect_uri: config.redirect_uri,
|
|
41
42
|
scope: config.scope,
|
package/dist/silent-login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"silent-login.js","names":[],"sources":["../src/silent-login.ts"],"sourcesContent":["/**\n * Silent login functionality for OAuth flows.\n *\n * Silent login (also called \"prompt=none\") attempts to authenticate the user\n * without any user interaction. This is useful for:\n * - Checking if a user has an active SSO session\n * - Refreshing authentication silently in the background\n * - Implementing \"keep me logged in\" features\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { OAuthConfig } from './oauth';\n\nconst logger = createLogger({ namespace: 'fides-auth:silent-login' });\n\nexport interface SilentLoginOptions {\n /** Optional hint about the user's identity (e.g., email or user ID) */\n loginHint?: string;\n /** Maximum age of authentication in seconds */\n maxAge?: number;\n /** Additional parameters to pass to the authorization endpoint */\n additionalParams?: Record<string, string>;\n}\n\nexport interface SilentLoginResult {\n /** Whether silent login was successful */\n success: boolean;\n /** The authorization URL to use (only if success is true) */\n authorizationUrl?: URL;\n /** Error message if silent login failed */\n error?: string;\n /** Detailed error description from OAuth provider */\n errorDescription?: string;\n}\n\n/**\n * Builds an authorization URL for silent authentication.\n *\n * This function creates an authorization URL with prompt=none,\n * which tells the OAuth provider to authenticate without user interaction.\n * If the user doesn't have an active session, the provider will return an error.\n *\n * @param config - OAuth configuration\n * @param options - Silent login options\n * @returns Authorization URL for silent authentication\n *\n * @example\n * ```ts\n * const silentAuthUrl = await buildSilentLoginUrl(oauthConfig, {\n * loginHint: 'user@example.com',\n * maxAge: 3600, // Require authentication within last hour\n * });\n *\n * // Use this URL in an iframe or hidden window\n * ```\n */\nexport async function buildSilentLoginUrl(\n config: OAuthConfig,\n options: SilentLoginOptions = {}\n): Promise<URL> {\n logger.debug({ issuer: config.issuer }, 'Building silent login URL');\n\n try {\n const server = await
|
|
1
|
+
{"version":3,"file":"silent-login.js","names":[],"sources":["../src/silent-login.ts"],"sourcesContent":["/**\n * Silent login functionality for OAuth flows.\n *\n * Silent login (also called \"prompt=none\") attempts to authenticate the user\n * without any user interaction. This is useful for:\n * - Checking if a user has an active SSO session\n * - Refreshing authentication silently in the background\n * - Implementing \"keep me logged in\" features\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { OAuthConfig } from './oauth';\nimport { discoverConfiguration } from './oidc-discovery';\n\nconst logger = createLogger({ namespace: 'fides-auth:silent-login' });\n\nexport interface SilentLoginOptions {\n /** Optional hint about the user's identity (e.g., email or user ID) */\n loginHint?: string;\n /** Maximum age of authentication in seconds */\n maxAge?: number;\n /** Additional parameters to pass to the authorization endpoint */\n additionalParams?: Record<string, string>;\n}\n\nexport interface SilentLoginResult {\n /** Whether silent login was successful */\n success: boolean;\n /** The authorization URL to use (only if success is true) */\n authorizationUrl?: URL;\n /** Error message if silent login failed */\n error?: string;\n /** Detailed error description from OAuth provider */\n errorDescription?: string;\n}\n\n/**\n * Builds an authorization URL for silent authentication.\n *\n * This function creates an authorization URL with prompt=none,\n * which tells the OAuth provider to authenticate without user interaction.\n * If the user doesn't have an active session, the provider will return an error.\n *\n * @param config - OAuth configuration\n * @param options - Silent login options\n * @returns Authorization URL for silent authentication\n *\n * @example\n * ```ts\n * const silentAuthUrl = await buildSilentLoginUrl(oauthConfig, {\n * loginHint: 'user@example.com',\n * maxAge: 3600, // Require authentication within last hour\n * });\n *\n * // Use this URL in an iframe or hidden window\n * ```\n */\nexport async function buildSilentLoginUrl(\n config: OAuthConfig,\n options: SilentLoginOptions = {}\n): Promise<URL> {\n logger.debug({ issuer: config.issuer }, 'Building silent login URL');\n\n try {\n const server = await discoverConfiguration(config);\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope,\n prompt: 'none', // Critical: tells provider to not show any UI\n response_type: 'code',\n ...options.additionalParams,\n };\n\n // Add optional parameters\n if (options.loginHint) {\n parameters.login_hint = options.loginHint;\n }\n\n if (options.maxAge !== undefined) {\n parameters.max_age = options.maxAge.toString();\n }\n\n const authUrl = openid.buildAuthorizationUrl(server, parameters);\n\n logger.info('Silent login URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error, issuer: config.issuer }, 'Failed to build silent login URL');\n throw error;\n }\n}\n\n/**\n * Checks if silent login is possible by examining the callback URL.\n *\n * After redirecting to the silent login URL, the OAuth provider will redirect back\n * with either a code (success) or an error (failure). This function parses the\n * callback URL to determine the result.\n *\n * @param callbackUrl - The URL the OAuth provider redirected to\n * @returns Result indicating success or failure with details\n *\n * @example\n * ```ts\n * // In your OAuth callback handler\n * const result = checkSilentLoginResult(new URL(request.url));\n *\n * if (result.success) {\n * // User has active SSO session, proceed with token exchange\n * const code = new URL(request.url).searchParams.get('code');\n * // ... exchange code for tokens\n * } else if (result.error === 'login_required') {\n * // User needs to log in interactively\n * redirect('/login');\n * } else {\n * // Other error\n * console.error(result.errorDescription);\n * }\n * ```\n */\nexport function checkSilentLoginResult(callbackUrl: URL): SilentLoginResult {\n const error = callbackUrl.searchParams.get('error');\n const errorDescription = callbackUrl.searchParams.get('error_description');\n const code = callbackUrl.searchParams.get('code');\n\n if (error) {\n logger.info({ error, errorDescription }, 'Silent login failed with error');\n\n return {\n success: false,\n error,\n errorDescription: errorDescription ?? undefined,\n };\n }\n\n if (code) {\n logger.info('Silent login successful');\n return {\n success: true,\n };\n }\n\n logger.warn('Silent login callback missing both code and error');\n return {\n success: false,\n error: 'invalid_callback',\n errorDescription: 'Callback URL missing both code and error parameters',\n };\n}\n\n/**\n * Common OAuth errors returned from silent login attempts.\n *\n * These can be used to handle specific error cases:\n * - login_required: User must log in interactively\n * - interaction_required: User interaction is needed (e.g., consent)\n * - consent_required: User must grant consent\n * - account_selection_required: User must select an account\n */\nexport const SilentLoginErrors = {\n LOGIN_REQUIRED: 'login_required',\n INTERACTION_REQUIRED: 'interaction_required',\n CONSENT_REQUIRED: 'consent_required',\n ACCOUNT_SELECTION_REQUIRED: 'account_selection_required',\n} as const;\n\nexport type SilentLoginError = typeof SilentLoginErrors[keyof typeof SilentLoginErrors];\n\n/**\n * Helper to check if an error requires interactive login.\n *\n * @param error - The error code from the OAuth provider\n * @returns True if the user must log in interactively\n *\n * @example\n * ```ts\n * const result = checkSilentLoginResult(callbackUrl);\n * if (!result.success && requiresInteractiveLogin(result.error)) {\n * // Redirect to full login page\n * redirect('/login');\n * }\n * ```\n */\nexport function requiresInteractiveLogin(error: string | undefined): boolean {\n if (!error) return false;\n\n return Object.values(SilentLoginErrors).includes(error as SilentLoginError);\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAM,SAAS,aAAa,EAAE,WAAW,0BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;;AA2CpE,eAAsB,oBACpB,QACA,UAA8B,CAAC,GACjB;CACd,OAAO,MAAM,EAAE,QAAQ,OAAO,OAAO,GAAG,2BAA2B;CAEnE,IAAI;EACF,MAAM,SAAS,MAAM,sBAAsB,MAAM;EAEjD,MAAM,aAAqC;GACzC,cAAc,OAAO;GACrB,OAAO,OAAO;GACd,QAAQ;GACR,eAAe;GACf,GAAG,QAAQ;EACb;EAGA,IAAI,QAAQ,WACV,WAAW,aAAa,QAAQ;EAGlC,IAAI,QAAQ,WAAW,KAAA,GACrB,WAAW,UAAU,QAAQ,OAAO,SAAS;EAG/C,MAAM,UAAU,OAAO,sBAAsB,QAAQ,UAAU;EAE/D,OAAO,KAAK,qCAAqC;EACjD,OAAO;CACT,SAAS,OAAO;EACd,OAAO,MAAM;GAAE;GAAO,QAAQ,OAAO;EAAO,GAAG,kCAAkC;EACjF,MAAM;CACR;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAgB,uBAAuB,aAAqC;CAC1E,MAAM,QAAQ,YAAY,aAAa,IAAI,OAAO;CAClD,MAAM,mBAAmB,YAAY,aAAa,IAAI,mBAAmB;CACzE,MAAM,OAAO,YAAY,aAAa,IAAI,MAAM;CAEhD,IAAI,OAAO;EACT,OAAO,KAAK;GAAE;GAAO;EAAiB,GAAG,gCAAgC;EAEzE,OAAO;GACL,SAAS;GACT;GACA,kBAAkB,oBAAoB,KAAA;EACxC;CACF;CAEA,IAAI,MAAM;EACR,OAAO,KAAK,yBAAyB;EACrC,OAAO,EACL,SAAS,KACX;CACF;CAEA,OAAO,KAAK,mDAAmD;CAC/D,OAAO;EACL,SAAS;EACT,OAAO;EACP,kBAAkB;CACpB;AACF;;;;;;;;;;AAWA,IAAa,oBAAoB;CAC/B,gBAAgB;CAChB,sBAAsB;CACtB,kBAAkB;CAClB,4BAA4B;AAC9B;;;;;;;;;;;;;;;;AAmBA,SAAgB,yBAAyB,OAAoC;CAC3E,IAAI,CAAC,OAAO,OAAO;CAEnB,OAAO,OAAO,OAAO,iBAAiB,CAAC,CAAC,SAAS,KAAyB;AAC5E"}
|
package/dist/utils.d.ts
CHANGED
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAYvD;AAaD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAG1B;AAGD;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCxF;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA4CxF;AAcD;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,IAAI,UAAU,CAKvD;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI/C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,WAAW,GAAE,MAAW,GAAG,MAAM,CAI9D;AAGD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAKvC;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAYvD;AAaD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GAAG,UAAU,GAC1B,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAG1B;AAGD;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCxF;AAED;;;;;;;GAOG;AACH,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA4CxF;AAcD;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAIvE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAMzC;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,IAAI,UAAU,CAKvD;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAI/C;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,WAAW,GAAE,MAAW,GAAG,MAAM,CAI9D;AAGD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,gBAChB,MAAM,YACV,MAAM,KACd,OAsBF,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjE"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Discovery cache
|
|
2
|
+
|
|
3
|
+
Every OIDC flow needs the provider's metadata — token endpoint, authorization
|
|
4
|
+
endpoint, end-session endpoint. fides-auth fetches it once per provider and
|
|
5
|
+
reuses it for five minutes.
|
|
6
|
+
|
|
7
|
+
## Why it matters
|
|
8
|
+
|
|
9
|
+
Without a cache, a refresh is two round-trips: a GET to
|
|
10
|
+
`/.well-known/openid-configuration`, then the token request. On the heartbeat
|
|
11
|
+
path that happens per user every few minutes. It doubles refresh latency,
|
|
12
|
+
doubles the load your provider sees, and doubles the surface on which a refresh
|
|
13
|
+
can fail — a discovery blip and a token-endpoint blip are indistinguishable to
|
|
14
|
+
the caller, and both end up as a `session.refresh_failed` your operators have to
|
|
15
|
+
explain.
|
|
16
|
+
|
|
17
|
+
Concurrent callers share one round-trip. A burst of heartbeats arriving together
|
|
18
|
+
does not become a burst of discovery requests, because the cache holds the
|
|
19
|
+
in-flight promise rather than waiting for a result to store.
|
|
20
|
+
|
|
21
|
+
## What is cached, and for how long
|
|
22
|
+
|
|
23
|
+
Keyed by issuer and client id, for five minutes. The client secret is checked on
|
|
24
|
+
every lookup: rotate it and the next call rediscovers rather than authenticating
|
|
25
|
+
with a credential the provider has already retired.
|
|
26
|
+
|
|
27
|
+
Failures are never cached. A provider that was briefly unreachable is retried on
|
|
28
|
+
the next call — caching the rejection would turn one blip into five minutes of
|
|
29
|
+
downtime.
|
|
30
|
+
|
|
31
|
+
The TTL bounds only how long a *metadata* change takes to be noticed. Signing
|
|
32
|
+
keys are not covered by it: openid-client fetches JWKS on its own schedule, so a
|
|
33
|
+
key rollover does not wait for this cache.
|
|
34
|
+
|
|
35
|
+
## Changing it
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { configureDiscoveryCache, clearDiscoveryCache } from "@eventuras/fides-auth/oidc-discovery";
|
|
39
|
+
|
|
40
|
+
// Shorten the window, or pass 0 to discover on every call.
|
|
41
|
+
configureDiscoveryCache({ ttlMs: 60_000 });
|
|
42
|
+
|
|
43
|
+
// Drop everything now — after changing provider configuration you need picked
|
|
44
|
+
// up immediately.
|
|
45
|
+
clearDiscoveryCache();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Both are process-local: the cache lives in memory, so each instance of your app
|
|
49
|
+
holds its own, and a deploy starts empty.
|
|
50
|
+
|
|
51
|
+
If you write tests that mock discovery, call `clearDiscoveryCache()` between
|
|
52
|
+
cases. The cache is module state and will otherwise serve one test's provider
|
|
53
|
+
metadata to the next.
|
package/docs/session-events.md
CHANGED
|
@@ -187,3 +187,21 @@ if (!session) {
|
|
|
187
187
|
|
|
188
188
|
`logSessionEvent` picks the level from the policy above, so every caller stays
|
|
189
189
|
consistent without repeating it.
|
|
190
|
+
|
|
191
|
+
Next.js apps get the same thing without wiring a cookie store, and the read is
|
|
192
|
+
cached per render alongside `getCurrentSession`, so asking for the reason costs
|
|
193
|
+
no extra decrypt:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import {
|
|
197
|
+
tryGetCurrentSession,
|
|
198
|
+
SESSION_EVENT,
|
|
199
|
+
logSessionEvent,
|
|
200
|
+
} from "@eventuras/fides-auth-next";
|
|
201
|
+
|
|
202
|
+
const { session, reason } = await tryGetCurrentSession();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`tryRefreshCurrentSession` is the matching refresh, returning `cause` so a route
|
|
206
|
+
can answer 503 rather than ending a session over a provider blip. If you need the
|
|
207
|
+
framework-agnostic helpers directly, `nextCookieStore` is exported too.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eventuras/fides-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Framework-agnostic OAuth/OIDC authentication library with PKCE, session management, and pluggable logging",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oauth",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"types": "./dist/oauth.d.ts",
|
|
68
68
|
"import": "./dist/oauth.js"
|
|
69
69
|
},
|
|
70
|
+
"./oidc-discovery": {
|
|
71
|
+
"types": "./dist/oidc-discovery.d.ts",
|
|
72
|
+
"import": "./dist/oidc-discovery.js"
|
|
73
|
+
},
|
|
70
74
|
"./oauth-logging": {
|
|
71
75
|
"types": "./dist/oauth-logging.d.ts",
|
|
72
76
|
"import": "./dist/oauth-logging.js"
|
|
@@ -109,23 +113,23 @@
|
|
|
109
113
|
"README.md"
|
|
110
114
|
],
|
|
111
115
|
"dependencies": {
|
|
112
|
-
"jose": "^6.2.
|
|
113
|
-
"openid-client": "^6.8.
|
|
116
|
+
"jose": "^6.2.9",
|
|
117
|
+
"openid-client": "^6.8.7"
|
|
114
118
|
},
|
|
115
119
|
"devDependencies": {
|
|
116
120
|
"@types/node": "^22.20.1",
|
|
117
|
-
"jsdom": "
|
|
118
|
-
"typescript": "^
|
|
119
|
-
"vite": "^8.2.
|
|
121
|
+
"jsdom": "30.0.1",
|
|
122
|
+
"typescript": "^7.0.2",
|
|
123
|
+
"vite": "^8.2.1",
|
|
120
124
|
"vitest": "^4.1.10",
|
|
121
125
|
"@eventuras/typescript-config": "1.0.0",
|
|
122
|
-
"@eventuras/vite-config": "0.
|
|
126
|
+
"@eventuras/vite-config": "0.3.0"
|
|
123
127
|
},
|
|
124
128
|
"engines": {
|
|
125
129
|
"node": ">=18"
|
|
126
130
|
},
|
|
127
131
|
"scripts": {
|
|
128
|
-
"build": "vite build",
|
|
132
|
+
"build": "vite build && tsc --emitDeclarationOnly",
|
|
129
133
|
"typecheck": "tsc --noEmit",
|
|
130
134
|
"dev": "vite build --watch",
|
|
131
135
|
"test": "vitest run",
|