@eventuras/fides-auth 0.14.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -33
- package/dist/activity-tracker.js +1 -2
- package/dist/cookies.d.ts +6 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/cookies.js +8 -3
- package/dist/cookies.js.map +1 -1
- package/dist/heartbeat.js +1 -2
- package/dist/id-token.d.ts +31 -0
- package/dist/id-token.d.ts.map +1 -0
- package/dist/id-token.js +60 -0
- package/dist/id-token.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/logger.js +1 -2
- package/dist/oauth-browser.d.ts +13 -10
- package/dist/oauth-browser.d.ts.map +1 -1
- package/dist/oauth-browser.js +4 -2
- package/dist/oauth-browser.js.map +1 -1
- package/dist/oauth-config.d.ts +70 -0
- package/dist/oauth-config.d.ts.map +1 -0
- package/dist/oauth-logging.d.ts +4 -3
- package/dist/oauth-logging.d.ts.map +1 -1
- package/dist/oauth-logging.js +5 -5
- package/dist/oauth-logging.js.map +1 -1
- package/dist/oauth.d.ts +47 -45
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +58 -29
- package/dist/oauth.js.map +1 -1
- package/dist/oidc-discovery.d.ts +13 -4
- package/dist/oidc-discovery.d.ts.map +1 -1
- package/dist/oidc-discovery.js +82 -15
- package/dist/oidc-discovery.js.map +1 -1
- package/dist/pkce.d.ts +10 -0
- package/dist/pkce.d.ts.map +1 -0
- package/dist/pkce.js +23 -0
- package/dist/pkce.js.map +1 -0
- package/dist/providers/vipps/client.d.ts +3 -2
- package/dist/providers/vipps/client.d.ts.map +1 -1
- package/dist/providers/vipps/index.js +10 -10
- package/dist/providers/vipps/index.js.map +1 -1
- package/dist/providers/vipps/types.d.ts +2 -2
- package/dist/providers/vipps/types.d.ts.map +1 -1
- package/dist/rate-limit.js +1 -2
- package/dist/server/authorized-fetch.d.ts +78 -0
- package/dist/server/authorized-fetch.d.ts.map +1 -0
- package/dist/server/heartbeat-handler.d.ts +6 -4
- package/dist/server/heartbeat-handler.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +744 -94
- package/dist/server/index.js.map +1 -1
- package/dist/server/login-transaction.d.ts +55 -0
- package/dist/server/login-transaction.d.ts.map +1 -0
- package/dist/server/oidc-callback.d.ts +13 -4
- package/dist/server/oidc-callback.d.ts.map +1 -1
- package/dist/server/oidc-login.d.ts +14 -5
- package/dist/server/oidc-login.d.ts.map +1 -1
- package/dist/server/oidc-logout.d.ts +6 -4
- package/dist/server/oidc-logout.d.ts.map +1 -1
- package/dist/server/session-persistence.d.ts +102 -0
- package/dist/server/session-persistence.d.ts.map +1 -0
- package/dist/server/session-store.d.ts +39 -0
- package/dist/server/session-store.d.ts.map +1 -0
- package/dist/server/session.d.ts +35 -28
- package/dist/server/session.d.ts.map +1 -1
- package/dist/session-cookies.d.ts +1 -0
- package/dist/session-cookies.d.ts.map +1 -1
- package/dist/session-cookies.js +4 -3
- package/dist/session-cookies.js.map +1 -1
- package/dist/session-events.d.ts +10 -3
- package/dist/session-events.d.ts.map +1 -1
- package/dist/session-events.js +4 -3
- package/dist/session-events.js.map +1 -1
- package/dist/session-refresh.d.ts +1 -1
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +1 -2
- package/dist/session-refresh.js.map +1 -1
- package/dist/session-validation.d.ts.map +1 -1
- package/dist/session-validation.js +9 -6
- package/dist/session-validation.js.map +1 -1
- package/dist/silent-login.d.ts +1 -1
- package/dist/silent-login.d.ts.map +1 -1
- package/dist/silent-login.js +3 -3
- package/dist/silent-login.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +0 -14
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -24
- package/dist/utils.js.map +1 -1
- package/docs/discovery-cache.md +7 -3
- package/docs/rp-initiated-logout.md +6 -3
- package/docs/server-sessions.md +154 -0
- package/docs/session-events.md +17 -16
- package/package.json +14 -6
package/dist/oauth.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 { 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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"}
|
|
1
|
+
{"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt, type JWTPayload } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\n// `.js` keeps the re-exported types resolvable under node16/nodenext.\nimport type { ClientAuthConfig, ClientCredentialsConfig, OAuthConfig } from './oauth-config.js';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { discoverConfiguration, resolveClientAuth } from './oidc-discovery';\nimport { createPkcePair, createState } from './pkce';\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\nexport type { ClientAuthConfig, ClientCredentialsConfig, OAuthConfig };\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Returns a scope as the space-separated string sent on the wire. */\nexport function joinScope(scope: string | string[]): string {\n return Array.isArray(scope) ? scope.join(' ') : scope;\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: joinScope(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/** Per-request options for {@link buildPKCEOptions}. */\nexport interface AuthorizationRequestOptions {\n /** Extra authorization parameters, merged over `OAuthConfig.authorizationParameters`. */\n parameters?: Record<string, string>;\n}\n\n/** Extra parameters can't set these. */\nconst RESERVED_AUTHORIZATION_PARAMETERS = new Set([\n // Set by buildPKCEOptions\n 'state',\n 'code_challenge',\n 'code_challenge_method',\n 'redirect_uri',\n // openid-client fills these in only when absent\n 'client_id',\n 'response_type',\n // Would bypass the PKCE and state parameters above\n 'request',\n 'request_uri',\n // The callback expects no nonce, so an ID token carrying one fails the login\n 'nonce',\n]);\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n * For the verifier, challenge or state alone, use `createPkcePair` / `createState` from `/pkce`.\n * Adds `config.authorizationParameters` and `options.parameters`; a `scope` among them wins.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @param options - Per-request extra parameters.\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 * @throws Error if an extra parameter is a reserved key (see `OAuthConfig.authorizationParameters`).\n */\nexport async function buildPKCEOptions(\n config: OAuthConfig,\n options: AuthorizationRequestOptions = {},\n): Promise<PKCEOptions> {\n const extra = { ...config.authorizationParameters, ...options.parameters };\n const reserved = Object.keys(extra).filter((key) => RESERVED_AUTHORIZATION_PARAMETERS.has(key));\n if (reserved.length > 0) {\n throw new Error(`Reserved authorization parameters can't be overridden: ${reserved.join(', ')}`);\n }\n\n const { verifier: code_verifier, challenge: code_challenge } = await createPkcePair();\n const state = createState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: joinScope(config.scope ?? defaultScope),\n ...extra,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n // Keys only: values such as login_hint can be personal data.\n logger.debug(\n { redirectUri: config.redirect_uri, scope: parameters.scope, extraParameters: Object.keys(extra) },\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 * 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 * Decodes only: the signature, `iss`, `aud` and `exp` are not checked. Safe on tokens\n * straight from `exchangeAuthorizationCode`; verify any others first (e.g. jose `jwtVerify`).\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 return userFromClaims(decodeJwt(tokens.id_token), rolesClaim);\n}\n\nfunction userFromClaims(idToken: JWTPayload, rolesClaim: string): OidcUserInfo {\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 * Changes the session built from a token response, e.g. to add `data` or custom user\n * fields. `claims` are decoded from the ID token, not verified, and undefined without one.\n */\nexport type MapSession = (\n tokens: openid.TokenEndpointResponse,\n claims: JWTPayload | undefined,\n session: Session,\n) => Session;\n\n/** Options for {@link buildSessionFromTokens}. */\nexport interface BuildSessionOptions {\n /**\n * Name of the ID token claim that contains user roles.\n * @default 'roles'\n */\n rolesClaim?: string;\n\n /** Changes the session before it is returned. */\n mapSession?: MapSession;\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. Without an\n * `id_token` the session has no `user`.\n *\n * Decodes the ID token like `extractUserFromTokens`, without verifying it. Pass tokens\n * straight from `exchangeAuthorizationCode`, or verify them first.\n *\n * @param tokens - Token response from the OIDC provider\n * @param options - `rolesClaim` and `mapSession`\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n options?: BuildSessionOptions,\n): Session;\n/**\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim?: string,\n): Session;\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n options: BuildSessionOptions | string = {},\n): Session {\n const { rolesClaim = 'roles', mapSession } =\n typeof options === 'string' ? { rolesClaim: options } : options;\n\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n // OAuth-only responses (no `openid` scope) carry no ID token, so no user.\n const claims = tokens.id_token ? decodeJwt(tokens.id_token) : undefined;\n const userInfo = claims && userFromClaims(claims, 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 const session: Session = {\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: userInfo && {\n sub: userInfo.sub,\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n\n return mapSession ? mapSession(tokens, claims, session) : session;\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 * Authenticates with `config.clientAuth` (default client_secret_post). The\n * endpoint must be https — the request carries the client credentials.\n *\n * @param config - Client credentials configuration: token endpoint, client ID and credentials\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 { clientSecret, clientAuthentication } = resolveClientAuth(config);\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 clientSecret,\n clientAuthentication,\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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"}
|
package/dist/oidc-discovery.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as openid from 'openid-client';
|
|
2
|
-
import type { OAuthConfig } from './oauth';
|
|
2
|
+
import type { ClientAuthConfig, OAuthConfig } from './oauth-config';
|
|
3
3
|
/** Options for {@link configureDiscoveryCache}. */
|
|
4
4
|
export interface DiscoveryCacheOptions {
|
|
5
5
|
/**
|
|
@@ -30,10 +30,19 @@ export declare function configureDiscoveryCache(options: DiscoveryCacheOptions):
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function clearDiscoveryCache(): void;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Maps `clientAuth` to openid-client's client authentication. Every
|
|
34
|
+
* `Configuration` fides-auth builds gets its client authentication from here.
|
|
34
35
|
*
|
|
35
|
-
*
|
|
36
|
-
|
|
36
|
+
* @throws TypeError for an unknown `clientAuth`, rather than falling back to another method.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveClientAuth(config: ClientAuthConfig): {
|
|
39
|
+
/** Client metadata shorthand; set only for the secret methods. */
|
|
40
|
+
clientSecret: string | undefined;
|
|
41
|
+
clientAuthentication: openid.ClientAuth;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Discovers the provider's OpenID configuration, or builds it from
|
|
45
|
+
* `config.serverMetadata`, reusing a recent result.
|
|
37
46
|
*
|
|
38
47
|
* @param config - OAuth configuration identifying the provider and client.
|
|
39
48
|
* @returns The openid-client `Configuration` for this provider and client.
|
|
@@ -1 +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,
|
|
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,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAkCpE,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;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,gBAAgB,GAAG;IAC3D,kEAAkE;IAClE,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,oBAAoB,EAAE,MAAM,CAAC,UAAU,CAAC;CACzC,CAyBA;AA+DD;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,CA+B/B"}
|
package/dist/oidc-discovery.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createLogger } from "./logger.js";
|
|
2
2
|
import * as openid from "openid-client";
|
|
3
|
-
|
|
3
|
+
// src/oidc-discovery.ts
|
|
4
4
|
var logger = createLogger({ namespace: "fides-auth:oidc-discovery" });
|
|
5
5
|
/**
|
|
6
6
|
* Default lifetime of a cached configuration.
|
|
@@ -41,34 +41,102 @@ function clearDiscoveryCache() {
|
|
|
41
41
|
cache.clear();
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* Maps `clientAuth` to openid-client's client authentication. Every
|
|
45
|
+
* `Configuration` fides-auth builds gets its client authentication from here.
|
|
46
|
+
*
|
|
47
|
+
* @throws TypeError for an unknown `clientAuth`, rather than falling back to another method.
|
|
48
|
+
*/
|
|
49
|
+
function resolveClientAuth(config) {
|
|
50
|
+
switch (config.clientAuth) {
|
|
51
|
+
case void 0:
|
|
52
|
+
case "client_secret_post": return {
|
|
53
|
+
clientSecret: config.clientSecret,
|
|
54
|
+
clientAuthentication: openid.ClientSecretPost(config.clientSecret)
|
|
55
|
+
};
|
|
56
|
+
case "client_secret_basic": return {
|
|
57
|
+
clientSecret: config.clientSecret,
|
|
58
|
+
clientAuthentication: openid.ClientSecretBasic(config.clientSecret)
|
|
59
|
+
};
|
|
60
|
+
case "none": return {
|
|
61
|
+
clientSecret: void 0,
|
|
62
|
+
clientAuthentication: openid.None()
|
|
63
|
+
};
|
|
64
|
+
case "private_key_jwt": return {
|
|
65
|
+
clientSecret: void 0,
|
|
66
|
+
clientAuthentication: openid.PrivateKeyJwt(config.privateKey)
|
|
67
|
+
};
|
|
68
|
+
default: throw new TypeError(`Unsupported clientAuth: ${String(config.clientAuth)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Issuer, client and auth method identify a configuration; the credential is
|
|
73
|
+
* checked separately. JSON-encoded rather than concatenated so no separator can
|
|
74
|
+
* be forged by a value that happens to contain it.
|
|
47
75
|
*/
|
|
48
76
|
function cacheKey(config) {
|
|
49
|
-
|
|
77
|
+
const method = config.clientAuth ?? "client_secret_post";
|
|
78
|
+
return JSON.stringify([
|
|
79
|
+
config.issuer,
|
|
80
|
+
config.clientId,
|
|
81
|
+
method
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
/** The values an entry must have been built from, compared by identity. */
|
|
85
|
+
function cacheInputs(config) {
|
|
86
|
+
const metadata = config.serverMetadata;
|
|
87
|
+
switch (config.clientAuth) {
|
|
88
|
+
case "none": return [metadata];
|
|
89
|
+
case "private_key_jwt": {
|
|
90
|
+
const pk = config.privateKey;
|
|
91
|
+
return "key" in pk ? [
|
|
92
|
+
metadata,
|
|
93
|
+
pk.key,
|
|
94
|
+
pk.kid
|
|
95
|
+
] : [
|
|
96
|
+
metadata,
|
|
97
|
+
pk,
|
|
98
|
+
void 0
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
default: return [metadata, config.clientSecret];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function sameInputs(a, b) {
|
|
105
|
+
return a.length === b.length && a.every((value, i) => Object.is(value, b[i]));
|
|
106
|
+
}
|
|
107
|
+
/** Unset scope means `defaultScope`, which includes `openid`. */
|
|
108
|
+
function requestsOpenId(scope) {
|
|
109
|
+
if (scope === void 0) return true;
|
|
110
|
+
return (Array.isArray(scope) ? scope.join(" ") : scope).split(/\s+/).includes("openid");
|
|
111
|
+
}
|
|
112
|
+
/** Async so a bad config rejects like a failed discovery and is never cached. */
|
|
113
|
+
async function buildConfiguration(config) {
|
|
114
|
+
const { clientSecret, clientAuthentication } = resolveClientAuth(config);
|
|
115
|
+
const metadata = config.serverMetadata;
|
|
116
|
+
if (!metadata) return openid.discovery(new URL(config.issuer), config.clientId, clientSecret, clientAuthentication);
|
|
117
|
+
if (!metadata.issuer && requestsOpenId(config.scope)) throw new TypeError("serverMetadata.issuer is required when scope includes \"openid\", to check the ID token");
|
|
118
|
+
return new openid.Configuration(metadata, config.clientId, clientSecret, clientAuthentication);
|
|
50
119
|
}
|
|
51
120
|
/**
|
|
52
|
-
* Discovers the provider's OpenID configuration,
|
|
53
|
-
*
|
|
54
|
-
* Every fides-auth flow goes through here, so this is also the single place
|
|
55
|
-
* where the client authentication method is decided.
|
|
121
|
+
* Discovers the provider's OpenID configuration, or builds it from
|
|
122
|
+
* `config.serverMetadata`, reusing a recent result.
|
|
56
123
|
*
|
|
57
124
|
* @param config - OAuth configuration identifying the provider and client.
|
|
58
125
|
* @returns The openid-client `Configuration` for this provider and client.
|
|
59
126
|
*/
|
|
60
127
|
async function discoverConfiguration(config) {
|
|
61
128
|
const key = cacheKey(config);
|
|
129
|
+
const inputs = cacheInputs(config);
|
|
62
130
|
const now = Date.now();
|
|
63
131
|
const cached = cache.get(key);
|
|
64
|
-
if (cached && cached.expiresAt > now && cached.
|
|
65
|
-
logger.debug({ issuer: config.issuer }, "Discovering OpenID configuration");
|
|
66
|
-
const configuration =
|
|
132
|
+
if (cached && cached.expiresAt > now && sameInputs(cached.inputs, inputs)) return cached.configuration;
|
|
133
|
+
logger.debug({ issuer: config.issuer }, config.serverMetadata ? "Building configuration from server metadata" : "Discovering OpenID configuration");
|
|
134
|
+
const configuration = buildConfiguration(config);
|
|
67
135
|
if (ttlMs > 0) {
|
|
68
136
|
cache.set(key, {
|
|
69
137
|
configuration,
|
|
70
138
|
expiresAt: now + ttlMs,
|
|
71
|
-
|
|
139
|
+
inputs
|
|
72
140
|
});
|
|
73
141
|
configuration.catch(() => {
|
|
74
142
|
if (cache.get(key)?.configuration === configuration) cache.delete(key);
|
|
@@ -76,7 +144,6 @@ async function discoverConfiguration(config) {
|
|
|
76
144
|
}
|
|
77
145
|
return configuration;
|
|
78
146
|
}
|
|
79
|
-
|
|
80
|
-
export { clearDiscoveryCache, configureDiscoveryCache, discoverConfiguration };
|
|
147
|
+
export { clearDiscoveryCache, configureDiscoveryCache, discoverConfiguration, resolveClientAuth };
|
|
81
148
|
|
|
82
149
|
//# sourceMappingURL=oidc-discovery.js.map
|
|
@@ -1 +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
|
+
{"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 { ClientAuthConfig, OAuthConfig } from './oauth-config';\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 server metadata and secret or private key this entry was built with,\n * compared on lookup so a change invalidates the entry without the credential\n * going into the cache key.\n */\n inputs: readonly unknown[];\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 * Maps `clientAuth` to openid-client's client authentication. Every\n * `Configuration` fides-auth builds gets its client authentication from here.\n *\n * @throws TypeError for an unknown `clientAuth`, rather than falling back to another method.\n */\nexport function resolveClientAuth(config: ClientAuthConfig): {\n /** Client metadata shorthand; set only for the secret methods. */\n clientSecret: string | undefined;\n clientAuthentication: openid.ClientAuth;\n} {\n switch (config.clientAuth) {\n case undefined:\n case 'client_secret_post':\n return {\n clientSecret: config.clientSecret,\n clientAuthentication: openid.ClientSecretPost(config.clientSecret),\n };\n case 'client_secret_basic':\n return {\n clientSecret: config.clientSecret,\n clientAuthentication: openid.ClientSecretBasic(config.clientSecret),\n };\n case 'none':\n return { clientSecret: undefined, clientAuthentication: openid.None() };\n case 'private_key_jwt':\n return {\n clientSecret: undefined,\n clientAuthentication: openid.PrivateKeyJwt(config.privateKey),\n };\n default:\n throw new TypeError(\n `Unsupported clientAuth: ${String((config as { clientAuth: unknown }).clientAuth)}`,\n );\n }\n}\n\n/**\n * Issuer, client and auth method identify a configuration; the credential is\n * checked separately. JSON-encoded rather than concatenated so no separator can\n * be forged by a value that happens to contain it.\n */\nfunction cacheKey(config: OAuthConfig): string {\n const method = config.clientAuth ?? 'client_secret_post';\n return JSON.stringify([config.issuer, config.clientId, method]);\n}\n\n/** The values an entry must have been built from, compared by identity. */\nfunction cacheInputs(config: OAuthConfig): readonly unknown[] {\n const metadata = config.serverMetadata;\n switch (config.clientAuth) {\n case 'none':\n return [metadata];\n case 'private_key_jwt': {\n // A `{ key, kid }` wrapper may be rebuilt per call; compare what it holds.\n const pk = config.privateKey;\n return 'key' in pk ? [metadata, pk.key, pk.kid] : [metadata, pk, undefined];\n }\n default:\n return [metadata, config.clientSecret];\n }\n}\n\nfunction sameInputs(a: readonly unknown[], b: readonly unknown[]): boolean {\n return a.length === b.length && a.every((value, i) => Object.is(value, b[i]));\n}\n\n/** Unset scope means `defaultScope`, which includes `openid`. */\nfunction requestsOpenId(scope: string | string[] | undefined): boolean {\n if (scope === undefined) return true;\n const joined = Array.isArray(scope) ? scope.join(' ') : scope;\n return joined.split(/\\s+/).includes('openid');\n}\n\n/** Async so a bad config rejects like a failed discovery and is never cached. */\nasync function buildConfiguration(config: OAuthConfig): Promise<openid.Configuration> {\n const { clientSecret, clientAuthentication } = resolveClientAuth(config);\n const metadata = config.serverMetadata;\n\n if (!metadata) {\n return openid.discovery(\n new URL(config.issuer),\n config.clientId,\n clientSecret,\n clientAuthentication,\n );\n }\n\n // Don't fall back to `config.issuer`: the ID token's `iss` must match the server's own.\n if (!metadata.issuer && requestsOpenId(config.scope)) {\n throw new TypeError(\n 'serverMetadata.issuer is required when scope includes \"openid\", to check the ID token',\n );\n }\n\n return new openid.Configuration(metadata, config.clientId, clientSecret, clientAuthentication);\n}\n\n/**\n * Discovers the provider's OpenID configuration, or builds it from\n * `config.serverMetadata`, reusing a recent result.\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 inputs = cacheInputs(config);\n const now = Date.now();\n const cached = cache.get(key);\n\n if (cached && cached.expiresAt > now && sameInputs(cached.inputs, inputs)) {\n return cached.configuration;\n }\n\n logger.debug(\n { issuer: config.issuer },\n config.serverMetadata\n ? 'Building configuration from server metadata'\n : 'Discovering OpenID configuration',\n );\n\n const configuration = buildConfiguration(config);\n\n if (ttlMs > 0) {\n cache.set(key, { configuration, expiresAt: now + ttlMs, inputs });\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":"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"}
|
package/dist/pkce.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A PKCE code verifier and its S256 code challenge. */
|
|
2
|
+
export interface PkcePair {
|
|
3
|
+
verifier: string;
|
|
4
|
+
challenge: string;
|
|
5
|
+
}
|
|
6
|
+
/** Creates a code verifier (32 random bytes, base64url) and its S256 challenge. */
|
|
7
|
+
export declare function createPkcePair(): Promise<PkcePair>;
|
|
8
|
+
/** Creates a random `state` value (32 random bytes, base64url). */
|
|
9
|
+
export declare function createState(): string;
|
|
10
|
+
//# sourceMappingURL=pkce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../src/pkce.ts"],"names":[],"mappings":"AAKA,wDAAwD;AACxD,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAaD,mFAAmF;AACnF,wBAAsB,cAAc,IAAI,OAAO,CAAC,QAAQ,CAAC,CAIxD;AAED,mEAAmE;AACnE,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
|
package/dist/pkce.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/pkce.ts
|
|
2
|
+
function base64url(bytes) {
|
|
3
|
+
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
4
|
+
}
|
|
5
|
+
function randomBase64url(byteLength) {
|
|
6
|
+
return base64url(crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
7
|
+
}
|
|
8
|
+
/** Creates a code verifier (32 random bytes, base64url) and its S256 challenge. */
|
|
9
|
+
async function createPkcePair() {
|
|
10
|
+
const verifier = randomBase64url(32);
|
|
11
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
12
|
+
return {
|
|
13
|
+
verifier,
|
|
14
|
+
challenge: base64url(new Uint8Array(digest))
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Creates a random `state` value (32 random bytes, base64url). */
|
|
18
|
+
function createState() {
|
|
19
|
+
return randomBase64url(32);
|
|
20
|
+
}
|
|
21
|
+
export { createPkcePair, createState };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=pkce.js.map
|
package/dist/pkce.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","names":[],"sources":["../src/pkce.ts"],"sourcesContent":["// pkce.ts\n//\n// PKCE (RFC 7636) and `state` primitives on Web Crypto. No imports, so it runs\n// in Node, browsers and edge runtimes.\n\n/** A PKCE code verifier and its S256 code challenge. */\nexport interface PkcePair {\n verifier: string;\n challenge: string;\n}\n\nfunction base64url(bytes: Uint8Array): string {\n return btoa(String.fromCharCode(...bytes))\n .replaceAll('+', '-')\n .replaceAll('/', '_')\n .replace(/=+$/, '');\n}\n\nfunction randomBase64url(byteLength: number): string {\n return base64url(crypto.getRandomValues(new Uint8Array(byteLength)));\n}\n\n/** Creates a code verifier (32 random bytes, base64url) and its S256 challenge. */\nexport async function createPkcePair(): Promise<PkcePair> {\n const verifier = randomBase64url(32);\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));\n return { verifier, challenge: base64url(new Uint8Array(digest)) };\n}\n\n/** Creates a random `state` value (32 random bytes, base64url). */\nexport function createState(): string {\n return randomBase64url(32);\n}\n"],"mappings":";AAWA,SAAS,UAAU,OAA2B;CAC5C,OAAO,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC,CAAC,CACvC,WAAW,KAAK,GAAG,CAAC,CACpB,WAAW,KAAK,GAAG,CAAC,CACpB,QAAQ,OAAO,EAAE;AACtB;AAEA,SAAS,gBAAgB,YAA4B;CACnD,OAAO,UAAU,OAAO,gBAAgB,IAAI,WAAW,UAAU,CAAC,CAAC;AACrE;;AAGA,eAAsB,iBAAoC;CACxD,MAAM,WAAW,gBAAgB,EAAE;CACnC,MAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,CAAC,CAAC,OAAO,QAAQ,CAAC;CACvF,OAAO;EAAE;EAAU,WAAW,UAAU,IAAI,WAAW,MAAM,CAAC;CAAE;AAClE;;AAGA,SAAgB,cAAsB;CACpC,OAAO,gBAAgB,EAAE;AAC3B"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Implements OAuth/OIDC operations with PKCE for Vipps.
|
|
6
6
|
*/
|
|
7
7
|
import * as openid from 'openid-client';
|
|
8
|
-
import { type PKCEOptions } from '../../oauth';
|
|
8
|
+
import { type AuthorizationRequestOptions, type PKCEOptions } from '../../oauth';
|
|
9
9
|
import { type VippsLoginConfig, type VippsUserInfo } from './types';
|
|
10
10
|
/**
|
|
11
11
|
* Vipps Login Client
|
|
@@ -28,9 +28,10 @@ export declare class VippsLoginClient {
|
|
|
28
28
|
/**
|
|
29
29
|
* Build PKCE options for authorization flow
|
|
30
30
|
*
|
|
31
|
+
* @param options - Per-request extra authorization parameters
|
|
31
32
|
* @returns PKCE options including code_verifier, code_challenge, and state
|
|
32
33
|
*/
|
|
33
|
-
buildPKCEOptions(): Promise<PKCEOptions>;
|
|
34
|
+
buildPKCEOptions(options?: AuthorizationRequestOptions): Promise<PKCEOptions>;
|
|
34
35
|
/**
|
|
35
36
|
* Build authorization URL for Vipps Login
|
|
36
37
|
*
|
|
@@ -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,2BAA2B,EAChC,KAAK,WAAW,EACjB,MAAM,aAAa,CAAC;AAGrB,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,EAoBnC;IAED;;;;;;;OAOG;YACW,SAAS;IAIvB;;;;;OAKG;IACG,gBAAgB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,CAAC,CAGlF;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,8 +1,8 @@
|
|
|
1
1
|
import { createLogger } from "../../logger.js";
|
|
2
2
|
import { discoverConfiguration } from "../../oidc-discovery.js";
|
|
3
|
-
import { buildAuthorizationUrl, buildPKCEOptions } from "../../oauth.js";
|
|
3
|
+
import { buildAuthorizationUrl, buildPKCEOptions, joinScope } from "../../oauth.js";
|
|
4
4
|
import * as openid from "openid-client";
|
|
5
|
-
|
|
5
|
+
// src/providers/vipps/types.ts
|
|
6
6
|
/**
|
|
7
7
|
* Available Vipps Login scopes
|
|
8
8
|
*/
|
|
@@ -45,8 +45,7 @@ var VippsEnvironments = {
|
|
|
45
45
|
function getVippsIssuer(environment) {
|
|
46
46
|
return `${environment}/access-management-1.0/access/`;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
//#region src/providers/vipps/client.ts
|
|
48
|
+
// src/providers/vipps/client.ts
|
|
50
49
|
/**
|
|
51
50
|
* Vipps Login Client
|
|
52
51
|
*
|
|
@@ -66,21 +65,22 @@ var VippsLoginClient = class {
|
|
|
66
65
|
config;
|
|
67
66
|
oauthConfig;
|
|
68
67
|
constructor(config) {
|
|
68
|
+
const scope = joinScope(config.scope ?? defaultVippsLoginScope);
|
|
69
69
|
this.config = {
|
|
70
70
|
...config,
|
|
71
|
-
scope
|
|
71
|
+
scope
|
|
72
72
|
};
|
|
73
73
|
this.oauthConfig = {
|
|
74
74
|
issuer: config.issuer,
|
|
75
75
|
clientId: config.clientId,
|
|
76
76
|
clientSecret: config.clientSecret,
|
|
77
77
|
redirect_uri: config.redirectUri,
|
|
78
|
-
scope
|
|
78
|
+
scope
|
|
79
79
|
};
|
|
80
80
|
logger.info({
|
|
81
81
|
issuer: config.issuer,
|
|
82
82
|
redirectUri: config.redirectUri,
|
|
83
|
-
scope
|
|
83
|
+
scope
|
|
84
84
|
}, "VippsLoginClient initialized");
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
@@ -97,11 +97,12 @@ var VippsLoginClient = class {
|
|
|
97
97
|
/**
|
|
98
98
|
* Build PKCE options for authorization flow
|
|
99
99
|
*
|
|
100
|
+
* @param options - Per-request extra authorization parameters
|
|
100
101
|
* @returns PKCE options including code_verifier, code_challenge, and state
|
|
101
102
|
*/
|
|
102
|
-
async buildPKCEOptions() {
|
|
103
|
+
async buildPKCEOptions(options) {
|
|
103
104
|
logger.debug("Building PKCE options");
|
|
104
|
-
return buildPKCEOptions(this.oauthConfig);
|
|
105
|
+
return buildPKCEOptions(this.oauthConfig, options);
|
|
105
106
|
}
|
|
106
107
|
/**
|
|
107
108
|
* Build authorization URL for Vipps Login
|
|
@@ -204,7 +205,6 @@ var VippsLoginClient = class {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
};
|
|
207
|
-
//#endregion
|
|
208
208
|
export { VippsEnvironments, VippsLoginClient, VippsLoginScopes, defaultVippsLoginScope, getVippsIssuer };
|
|
209
209
|
|
|
210
210
|
//# sourceMappingURL=index.js.map
|
|
@@ -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 { 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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"}
|
|
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, as a string or an array (default: 'openid name phoneNumber address email') */\n scope?: string | 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 joinScope,\n type AuthorizationRequestOptions,\n type PKCEOptions,\n} from '../../oauth';\nimport type { OAuthConfig } from '../../oauth-config';\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 const scope = joinScope(config.scope ?? defaultVippsLoginScope);\n this.config = { ...config, scope };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n 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 * @param options - Per-request extra authorization parameters\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(options?: AuthorizationRequestOptions): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig, options);\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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"}
|
|
@@ -17,8 +17,8 @@ export interface VippsLoginConfig {
|
|
|
17
17
|
clientSecret: string;
|
|
18
18
|
/** OAuth redirect URI (callback URL) */
|
|
19
19
|
redirectUri: string;
|
|
20
|
-
/** OpenID Connect scopes (default: 'openid name phoneNumber address email') */
|
|
21
|
-
scope?: string;
|
|
20
|
+
/** OpenID Connect scopes, as a string or an array (default: 'openid name phoneNumber address email') */
|
|
21
|
+
scope?: string | string[];
|
|
22
22
|
/** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */
|
|
23
23
|
subscriptionKey?: string;
|
|
24
24
|
/** Merchant serial number - required for userinfo endpoint */
|
|
@@ -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
|
|
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,wGAAwG;IACxG,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/rate-limit.ts
|
|
2
2
|
/**
|
|
3
3
|
* Token bucket rate limiter for protecting against brute-force attacks.
|
|
4
4
|
*
|
|
@@ -58,7 +58,6 @@ var TokenBucket = class {
|
|
|
58
58
|
return true;
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
|
-
//#endregion
|
|
62
61
|
export { TokenBucket };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=rate-limit.js.map
|