@eventuras/fides-auth 0.5.0 → 0.7.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/dist/oauth.js CHANGED
@@ -1,178 +1,338 @@
1
- import { createLogger as e } from "./logger.js";
2
- import { t } from "./decode_jwt-1J26fl4I.js";
3
- import { a as n, c as r, i, l as a, n as o, o as s, r as c, s as l, t as u } from "./build-nL2--iMi.js";
1
+ import { createLogger } from "./logger.js";
2
+ import { getOAuthErrorLogContext } from "./oauth-logging.js";
3
+ import { decodeJwt } from "jose";
4
+ import * as openid from "openid-client";
4
5
  //#region src/oauth.ts
5
- var d = e({ namespace: "fides-auth:oauth" }), f = "openid profile email offline_access";
6
- async function p(e, t) {
7
- d.debug({ issuer: e.issuer }, "Starting token refresh");
6
+ var logger = createLogger({ namespace: "fides-auth:oauth" });
7
+ /** Default OIDC scope requesting user identity, profile, email, and offline access. */
8
+ var defaultScope = "openid profile email offline_access";
9
+ /**
10
+ * Refreshes an access token using a refresh token grant.
11
+ *
12
+ * @param oAuthConfig - OAuth configuration for the identity provider
13
+ * @param refreshToken - The refresh token to exchange
14
+ * @returns Token endpoint response containing new access and refresh tokens
15
+ */
16
+ async function refreshAccessToken(oAuthConfig, refreshToken) {
17
+ logger.debug({ issuer: oAuthConfig.issuer }, "Starting token refresh");
8
18
  try {
9
- let n = await a(await s(new URL(e.issuer), e.clientId, e.clientSecret, u(e.clientSecret)), t, { scope: e.scope });
10
- return d.info({
11
- hasAccessToken: !!n.access_token,
12
- hasRefreshToken: !!n.refresh_token,
13
- expiresIn: n.expires_in
14
- }, "Token refresh successful"), n;
15
- } catch (t) {
16
- let n = t;
17
- throw n?.code === "OAUTH_RESPONSE_BODY_ERROR" && n?.error === "invalid_grant" ? d.info({
18
- error: n.error,
19
- error_description: n.error_description,
20
- issuer: e.issuer
21
- }, "Token refresh failed - refresh token expired or invalid") : d.error({
22
- error: t,
23
- issuer: e.issuer
24
- }, "Token refresh failed"), t;
19
+ const config = await openid.discovery(new URL(oAuthConfig.issuer), oAuthConfig.clientId, oAuthConfig.clientSecret, openid.ClientSecretPost(oAuthConfig.clientSecret));
20
+ const tokens = await openid.refreshTokenGrant(config, refreshToken, { scope: oAuthConfig.scope });
21
+ logger.info({
22
+ hasAccessToken: !!tokens.access_token,
23
+ hasRefreshToken: !!tokens.refresh_token,
24
+ expiresIn: tokens.expires_in
25
+ }, "Token refresh successful");
26
+ return tokens;
27
+ } catch (error) {
28
+ const errorContext = getOAuthErrorLogContext(error);
29
+ if (errorContext.code === "OAUTH_RESPONSE_BODY_ERROR" && errorContext.error === "invalid_grant") logger.info({
30
+ ...errorContext,
31
+ issuer: oAuthConfig.issuer
32
+ }, "Token refresh failed - refresh token expired or invalid");
33
+ else logger.error({
34
+ error,
35
+ issuer: oAuthConfig.issuer
36
+ }, "Token refresh failed");
37
+ throw error;
25
38
  }
26
39
  }
27
- async function m(e) {
28
- let t = l(), i = await n(t), a = r(), o = {
29
- redirect_uri: e.redirect_uri,
30
- scope: e.scope ?? "openid profile email offline_access",
31
- code_challenge: i,
40
+ /**
41
+ * Creates PKCE parameters for an OAuth flow.
42
+ *
43
+ * @param config - OAuthConfig configuration including the redirect URI and scope.
44
+ * @returns A promise that resolves to an object containing:
45
+ * - code_verifier: The randomly generated PKCE code verifier.
46
+ * - code_challenge: The derived PKCE code challenge.
47
+ * - state: A random state to protect against CSRF.
48
+ * - parameters: A Record<string,string> with the assembled parameters.
49
+ */
50
+ async function buildPKCEOptions(config) {
51
+ const code_verifier = openid.randomPKCECodeVerifier();
52
+ const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);
53
+ const state = openid.randomState();
54
+ const parameters = {
55
+ redirect_uri: config.redirect_uri,
56
+ scope: config.scope ?? "openid profile email offline_access",
57
+ code_challenge,
32
58
  code_challenge_method: "S256",
33
- state: a
59
+ state
34
60
  };
35
- return d.info({
36
- redirectUri: e.redirect_uri,
37
- scope: o.scope
38
- }, "PKCE parameters generated"), {
39
- code_verifier: t,
40
- code_challenge: i,
41
- state: a,
42
- parameters: o
61
+ logger.info({
62
+ redirectUri: config.redirect_uri,
63
+ scope: parameters.scope
64
+ }, "PKCE parameters generated");
65
+ return {
66
+ code_verifier,
67
+ code_challenge,
68
+ state,
69
+ parameters
43
70
  };
44
71
  }
45
- async function h(e, t) {
72
+ /**
73
+ * Builds an authorization URL with PKCE parameters.
74
+ *
75
+ * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)
76
+ * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
77
+ * @returns A Promise that resolves to the authorization URL
78
+ */
79
+ async function buildAuthorizationUrl(config, pkceOptions) {
46
80
  try {
47
- let n = c(e, t.parameters);
48
- return d.info("Authorization URL built successfully"), n;
49
- } catch (e) {
50
- throw d.error({ error: e }, "Failed to build authorization URL"), e;
81
+ const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);
82
+ logger.info("Authorization URL built successfully");
83
+ return authUrl;
84
+ } catch (error) {
85
+ logger.error({ error }, "Failed to build authorization URL");
86
+ throw error;
51
87
  }
52
88
  }
53
- async function g(e, t) {
89
+ /**
90
+ * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).
91
+ *
92
+ * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL
93
+ * containing only `client_id` and `request_uri`. The provider's discovery
94
+ * metadata must include `pushed_authorization_request_endpoint`.
95
+ *
96
+ * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)
97
+ * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
98
+ * @returns A Promise that resolves to the PAR-based authorization URL
99
+ */
100
+ async function buildAuthorizationUrlWithPAR(config, pkceOptions) {
54
101
  try {
55
- let n = await i(e, t.parameters);
56
- return d.info("PAR authorization URL built successfully"), n;
57
- } catch (e) {
58
- throw d.error({ error: e }, "Failed to build PAR authorization URL"), e;
102
+ const authUrl = await openid.buildAuthorizationUrlWithPAR(config, pkceOptions.parameters);
103
+ logger.info("PAR authorization URL built successfully");
104
+ return authUrl;
105
+ } catch (error) {
106
+ logger.error({ error }, "Failed to build PAR authorization URL");
107
+ throw error;
59
108
  }
60
109
  }
61
- async function _(e, t) {
62
- d.debug({ issuer: e.issuer }, "Discovering OpenID configuration");
110
+ /**
111
+ * Convenience function to discover OpenID configuration and build an
112
+ * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and
113
+ * the provider advertises `pushed_authorization_request_endpoint`.
114
+ *
115
+ * - `usePar === true` + provider advertises PAR → uses PAR.
116
+ * - `usePar === true` + provider does **not** advertise PAR → throws.
117
+ * - `usePar` unset/false + provider advertises PAR → standard flow + one-line
118
+ * `info` advisory that PAR is available but not enabled.
119
+ * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.
120
+ *
121
+ * @param oauthConfig - Your OAuth configuration
122
+ * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)
123
+ * @returns A Promise that resolves to the authorization URL
124
+ */
125
+ async function discoverAndBuildAuthorizationUrl(oauthConfig, pkceOptions) {
126
+ logger.debug({ issuer: oauthConfig.issuer }, "Discovering OpenID configuration");
63
127
  try {
64
- let n = await s(new URL(e.issuer), e.clientId, e.clientSecret, u(e.clientSecret)), r = n.serverMetadata().pushed_authorization_request_endpoint;
65
- if (e.usePar) {
66
- if (!r) throw Error("PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint");
67
- return g(n, t);
128
+ const config = await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret));
129
+ const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;
130
+ if (oauthConfig.usePar) {
131
+ if (!parEndpoint) throw new Error("PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint");
132
+ return buildAuthorizationUrlWithPAR(config, pkceOptions);
68
133
  }
69
- return r && d.info({ issuer: e.issuer }, "Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it"), h(n, t);
70
- } catch (t) {
71
- throw d.error({
72
- error: t,
73
- issuer: e.issuer
74
- }, "Failed to discover config or build URL"), t;
134
+ if (parEndpoint) logger.info({ issuer: oauthConfig.issuer }, "Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it");
135
+ return buildAuthorizationUrl(config, pkceOptions);
136
+ } catch (error) {
137
+ logger.error({
138
+ error,
139
+ issuer: oauthConfig.issuer
140
+ }, "Failed to discover config or build URL");
141
+ throw error;
75
142
  }
76
143
  }
77
- async function v(e, t, n, r) {
78
- d.debug({
79
- issuer: e.issuer,
80
- clientId: e.clientId
144
+ /**
145
+ * Exchanges an authorization code for tokens using OIDC discovery and PKCE.
146
+ *
147
+ * @param oauthConfig - OAuth configuration
148
+ * @param callbackUrl - The full callback URL with query parameters from the OIDC provider
149
+ * @param codeVerifier - The PKCE code verifier stored during login initiation
150
+ * @param expectedState - The state parameter stored during login initiation
151
+ * @returns Token response from the OIDC provider
152
+ */
153
+ async function exchangeAuthorizationCode(oauthConfig, callbackUrl, codeVerifier, expectedState) {
154
+ logger.debug({
155
+ issuer: oauthConfig.issuer,
156
+ clientId: oauthConfig.clientId
81
157
  }, "Exchanging authorization code for tokens");
82
- let i = await o(await s(new URL(e.issuer), e.clientId, e.clientSecret, u(e.clientSecret)), t, {
83
- pkceCodeVerifier: n,
84
- expectedState: r
85
- }, { redirect_uri: e.redirect_uri });
86
- return d.info({
87
- hasAccessToken: !!i.access_token,
88
- hasRefreshToken: !!i.refresh_token,
89
- expiresIn: i.expires_in
90
- }, "Authorization code exchange successful"), i;
158
+ const config = await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret));
159
+ const tokens = await openid.authorizationCodeGrant(config, callbackUrl, {
160
+ pkceCodeVerifier: codeVerifier,
161
+ expectedState
162
+ }, { redirect_uri: oauthConfig.redirect_uri });
163
+ logger.info({
164
+ hasAccessToken: !!tokens.access_token,
165
+ hasRefreshToken: !!tokens.refresh_token,
166
+ expiresIn: tokens.expires_in
167
+ }, "Authorization code exchange successful");
168
+ return tokens;
91
169
  }
92
- function y(e, n = "roles") {
93
- if (!e.id_token) throw Error("OIDC token response is missing id_token");
94
- let r = t(e.id_token), i = r[n], a = typeof i == "string" ? [i] : [], o = Array.isArray(i) ? i : a;
95
- return d.debug({
96
- sub: r.sub,
97
- rolesClaim: n,
98
- rolesCount: o.length
99
- }, "Extracted user from ID token"), {
100
- sub: r.sub ?? "",
101
- name: r.name,
102
- email: r.email,
103
- roles: o
170
+ /**
171
+ * Extracts user information from an OIDC token response.
172
+ * Decodes the ID token and extracts standard claims plus roles from a configurable claim.
173
+ *
174
+ * @param tokens - Token response from the OIDC provider
175
+ * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
176
+ * @returns Extracted user information
177
+ */
178
+ function extractUserFromTokens(tokens, rolesClaim = "roles") {
179
+ if (!tokens.id_token) throw new Error("OIDC token response is missing id_token");
180
+ const idToken = decodeJwt(tokens.id_token);
181
+ const rawRoles = idToken[rolesClaim];
182
+ const nonArrayRoles = typeof rawRoles === "string" ? [rawRoles] : [];
183
+ const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;
184
+ logger.debug({
185
+ sub: idToken.sub,
186
+ rolesClaim,
187
+ rolesCount: roles.length
188
+ }, "Extracted user from ID token");
189
+ return {
190
+ sub: idToken.sub ?? "",
191
+ name: idToken.name,
192
+ email: idToken.email,
193
+ roles
104
194
  };
105
195
  }
106
- function b(e, t = "roles") {
107
- if (!e.access_token) throw Error("OIDC token response is missing access_token");
108
- let n = y(e, t);
196
+ /**
197
+ * Builds a Session object from an OIDC token response.
198
+ * Maps provider tokens and user claims into the internal Session format.
199
+ *
200
+ * @param tokens - Token response from the OIDC provider
201
+ * @param rolesClaim - Name of the claim containing user roles (default: 'roles')
202
+ * @returns A Session object ready to be encrypted and stored
203
+ */
204
+ function buildSessionFromTokens(tokens, rolesClaim = "roles") {
205
+ if (!tokens.access_token) throw new Error("OIDC token response is missing access_token");
206
+ const userInfo = extractUserFromTokens(tokens, rolesClaim);
207
+ let scopes;
208
+ if (typeof tokens.scope === "string") {
209
+ const parsed = tokens.scope.split(" ").filter((s) => s.length > 0);
210
+ if (parsed.length > 0) scopes = parsed;
211
+ }
109
212
  return {
110
213
  tokens: {
111
- accessToken: e.access_token,
112
- accessTokenExpiresAt: e.expires_in ? new Date(Date.now() + e.expires_in * 1e3) : void 0,
113
- refreshToken: e.refresh_token
214
+ accessToken: tokens.access_token,
215
+ accessTokenExpiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1e3) : void 0,
216
+ refreshToken: tokens.refresh_token
114
217
  },
115
218
  user: {
116
- name: n.name,
117
- email: n.email,
118
- roles: n.roles
119
- }
219
+ name: userInfo.name,
220
+ email: userInfo.email,
221
+ roles: userInfo.roles
222
+ },
223
+ scopes
120
224
  };
121
225
  }
122
- function x(e, t, n = "/", r) {
123
- let i = e ?? n, a;
226
+ /**
227
+ * Validates a return URL against open redirect attacks.
228
+ * Ensures the URL is same-origin and passes optional custom validation.
229
+ *
230
+ * @param returnTo - The raw returnTo path/URL to validate
231
+ * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')
232
+ * @param defaultPath - Fallback path if validation fails (default: '/')
233
+ * @param validate - Optional custom validation function for the pathname
234
+ * @returns A safe, validated URL
235
+ */
236
+ function validateReturnUrl(returnTo, applicationOrigin, defaultPath = "/", validate) {
237
+ const redirectPath = returnTo ?? defaultPath;
238
+ let redirectUrl;
124
239
  try {
125
- a = new URL(i, t);
240
+ redirectUrl = new URL(redirectPath, applicationOrigin);
126
241
  } catch {
127
- return d.warn({ returnTo: i }, "Invalid returnTo URL, using default"), new URL(n, t);
242
+ logger.warn({ returnTo: redirectPath }, "Invalid returnTo URL, using default");
243
+ return new URL(defaultPath, applicationOrigin);
244
+ }
245
+ const originUrl = new URL(applicationOrigin);
246
+ if (redirectUrl.origin !== originUrl.origin) {
247
+ logger.error({
248
+ returnTo: redirectPath,
249
+ redirectOrigin: redirectUrl.origin,
250
+ applicationOrigin: originUrl.origin
251
+ }, "Possible open redirect attack: returnTo points outside application origin");
252
+ return new URL(defaultPath, applicationOrigin);
128
253
  }
129
- let o = new URL(t);
130
- return a.origin === o.origin ? r && !r(a.pathname) ? (d.warn({ pathname: a.pathname }, "returnTo failed custom validation, falling back to default"), new URL(n, t)) : a : (d.error({
131
- returnTo: i,
132
- redirectOrigin: a.origin,
133
- applicationOrigin: o.origin
134
- }, "Possible open redirect attack: returnTo points outside application origin"), new URL(n, t));
254
+ if (validate && !validate(redirectUrl.pathname)) {
255
+ logger.warn({ pathname: redirectUrl.pathname }, "returnTo failed custom validation, falling back to default");
256
+ return new URL(defaultPath, applicationOrigin);
257
+ }
258
+ return redirectUrl;
135
259
  }
136
- async function S(e, t) {
260
+ /**
261
+ * Discovers the OIDC provider's end_session_endpoint and builds a logout URL.
262
+ * Returns null if the provider doesn't expose an end_session_endpoint.
263
+ *
264
+ * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)
265
+ * @param postLogoutRedirectUri - URL to redirect to after provider logout
266
+ * @returns Logout URL or null if not supported
267
+ */
268
+ async function buildOidcLogoutUrl(oauthConfig, postLogoutRedirectUri) {
137
269
  try {
138
- let n = (await s(new URL(e.issuer), e.clientId, e.clientSecret, u(e.clientSecret))).serverMetadata().end_session_endpoint;
139
- if (!n) return d.info({ issuer: e.issuer }, "No end_session_endpoint found"), null;
140
- let r = new URL(n);
141
- return r.searchParams.set("client_id", e.clientId), r.searchParams.set("post_logout_redirect_uri", t), d.debug({ logoutUrl: r.toString() }, "Built OIDC logout URL"), r;
142
- } catch (t) {
143
- return d.warn({
144
- error: t,
145
- issuer: e.issuer
146
- }, "OIDC discovery failed for logout"), null;
270
+ const endSessionEndpoint = (await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret))).serverMetadata().end_session_endpoint;
271
+ if (!endSessionEndpoint) {
272
+ logger.info({ issuer: oauthConfig.issuer }, "No end_session_endpoint found");
273
+ return null;
274
+ }
275
+ const logoutUrl = new URL(endSessionEndpoint);
276
+ logoutUrl.searchParams.set("client_id", oauthConfig.clientId);
277
+ logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUri);
278
+ logger.debug({ logoutUrl: logoutUrl.toString() }, "Built OIDC logout URL");
279
+ return logoutUrl;
280
+ } catch (error) {
281
+ logger.warn({
282
+ error,
283
+ issuer: oauthConfig.issuer
284
+ }, "OIDC discovery failed for logout");
285
+ return null;
147
286
  }
148
287
  }
149
- async function C(e) {
288
+ /**
289
+ * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
290
+ *
291
+ * @param config - Client credentials configuration including token endpoint, client ID, and secret
292
+ * @returns Token response from the OAuth provider
293
+ * @throws Error if the token request fails
294
+ *
295
+ * @example
296
+ * ```typescript
297
+ * const tokens = await clientCredentialsGrant({
298
+ * tokenEndpoint: 'https://api.example.com/oauth2/token',
299
+ * clientId: 'my-client-id',
300
+ * clientSecret: 'my-client-secret',
301
+ * scope: 'api:read api:write',
302
+ * });
303
+ * ```
304
+ */
305
+ async function clientCredentialsGrant(config) {
150
306
  try {
151
- let t = new URLSearchParams({
307
+ const params = new URLSearchParams({
152
308
  grant_type: "client_credentials",
153
- client_id: e.clientId,
154
- client_secret: e.clientSecret
309
+ client_id: config.clientId,
310
+ client_secret: config.clientSecret
155
311
  });
156
- e.scope && t.set("scope", e.scope);
157
- let n = await fetch(e.tokenEndpoint, {
312
+ if (config.scope) params.set("scope", config.scope);
313
+ const response = await fetch(config.tokenEndpoint, {
158
314
  method: "POST",
159
315
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
160
- body: t
316
+ body: params
161
317
  });
162
- if (!n.ok) {
163
- let e = await n.text();
164
- throw d.error({
165
- status: n.status,
166
- error: e
167
- }, "Client credentials grant failed"), Error(`Client credentials grant failed: ${n.status} - ${e}`);
318
+ if (!response.ok) {
319
+ const errorText = await response.text();
320
+ logger.error({
321
+ status: response.status,
322
+ error: errorText
323
+ }, "Client credentials grant failed");
324
+ throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);
168
325
  }
169
- return await n.json();
170
- } catch (t) {
171
- throw d.error({
172
- error: t,
173
- tokenEndpoint: e.tokenEndpoint
174
- }, "Client credentials grant error"), t;
326
+ return await response.json();
327
+ } catch (error) {
328
+ logger.error({
329
+ error,
330
+ tokenEndpoint: config.tokenEndpoint
331
+ }, "Client credentials grant error");
332
+ throw error;
175
333
  }
176
334
  }
177
335
  //#endregion
178
- export { h as buildAuthorizationUrl, g as buildAuthorizationUrlWithPAR, S as buildOidcLogoutUrl, m as buildPKCEOptions, b as buildSessionFromTokens, C as clientCredentialsGrant, f as defaultScope, _ as discoverAndBuildAuthorizationUrl, v as exchangeAuthorizationCode, y as extractUserFromTokens, p as refreshAccessToken, x as validateReturnUrl };
336
+ export { buildAuthorizationUrl, buildAuthorizationUrlWithPAR, buildOidcLogoutUrl, buildPKCEOptions, buildSessionFromTokens, clientCredentialsGrant, defaultScope, discoverAndBuildAuthorizationUrl, exchangeAuthorizationCode, extractUserFromTokens, refreshAccessToken, validateReturnUrl };
337
+
338
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["import { decodeJwt } from 'jose';\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from './logger';\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport type { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:oauth' });\n\n// Re-export commonly used types from openid-client for convenience\nexport type { Configuration, TokenEndpointResponse } from 'openid-client';\n\n/** Default OIDC scope requesting user identity, profile, email, and offline access. */\nexport const defaultScope = 'openid profile email offline_access';\n\n/** Configuration for server-side OAuth / OIDC flows. */\nexport type OAuthConfig = {\n issuer: string;\n clientId: string;\n clientSecret: string;\n redirect_uri: string;\n scope: string;\n /**\n * Opt in to Pushed Authorization Requests (RFC 9126) when the provider\n * supports it. When true, authorization parameters are posted to the\n * provider's PAR endpoint ahead of the redirect, and only `client_id` +\n * `request_uri` travel through the user-agent. The provider must advertise\n * `pushed_authorization_request_endpoint` in its discovery metadata.\n */\n usePar?: boolean;\n};\n\n/**\n * Refreshes an access token using a refresh token grant.\n *\n * @param oAuthConfig - OAuth configuration for the identity provider\n * @param refreshToken - The refresh token to exchange\n * @returns Token endpoint response containing new access and refresh tokens\n */\nexport async function refreshAccessToken(\n oAuthConfig: OAuthConfig,\n refreshToken: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug({ issuer: oAuthConfig.issuer }, 'Starting token refresh');\n\n try {\n const config = await openid.discovery(\n new URL(oAuthConfig.issuer),\n oAuthConfig.clientId,\n oAuthConfig.clientSecret,\n openid.ClientSecretPost(oAuthConfig.clientSecret),\n );\n\n const tokens = await openid.refreshTokenGrant(\n config,\n refreshToken,\n {\n scope: oAuthConfig.scope,\n },\n );\n\n logger.info({\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in\n }, 'Token refresh successful');\n\n return tokens;\n } catch (error) {\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Most often the refresh token has expired - log at info level\n logger.info(\n { ...errorContext, issuer: oAuthConfig.issuer },\n 'Token refresh failed - refresh token expired or invalid',\n );\n } else {\n // Unexpected error - log at error level\n logger.error({ error, issuer: oAuthConfig.issuer }, 'Token refresh failed');\n }\n\n throw error;\n }\n}\n\n/** PKCE parameters generated for an authorization request. */\nexport interface PKCEOptions {\n code_verifier: string;\n code_challenge: string;\n state: string;\n parameters: Record<string, string>;\n}\n\n/**\n * Creates PKCE parameters for an OAuth flow.\n *\n * @param config - OAuthConfig configuration including the redirect URI and scope.\n * @returns A promise that resolves to an object containing:\n * - code_verifier: The randomly generated PKCE code verifier.\n * - code_challenge: The derived PKCE code challenge.\n * - state: A random state to protect against CSRF.\n * - parameters: A Record<string,string> with the assembled parameters.\n */\nexport async function buildPKCEOptions(config: OAuthConfig): Promise<PKCEOptions> {\n const code_verifier = openid.randomPKCECodeVerifier();\n const code_challenge = await openid.calculatePKCECodeChallenge(code_verifier);\n const state = openid.randomState();\n\n const parameters: Record<string, string> = {\n redirect_uri: config.redirect_uri,\n scope: config.scope ?? defaultScope,\n code_challenge,\n code_challenge_method: 'S256',\n state,\n };\n\n logger.info(\n { redirectUri: config.redirect_uri, scope: parameters.scope },\n 'PKCE parameters generated'\n );\n\n return { code_verifier, code_challenge, state, parameters };\n}\n\n/**\n * Builds an authorization URL with PKCE parameters.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function buildAuthorizationUrl(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = openid.buildAuthorizationUrl(config, pkceOptions.parameters);\n logger.info('Authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build authorization URL');\n throw error;\n }\n}\n\n/**\n * Builds an authorization URL using Pushed Authorization Requests (RFC 9126).\n *\n * Posts the PKCE parameters to the provider's PAR endpoint and returns a URL\n * containing only `client_id` and `request_uri`. The provider's discovery\n * metadata must include `pushed_authorization_request_endpoint`.\n *\n * @param config - Discovered OpenID Configuration (use openid.discovery() to obtain)\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the PAR-based authorization URL\n */\nexport async function buildAuthorizationUrlWithPAR(\n config: openid.Configuration,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n try {\n const authUrl = await openid.buildAuthorizationUrlWithPAR(\n config,\n pkceOptions.parameters,\n );\n logger.info('PAR authorization URL built successfully');\n return authUrl;\n } catch (error) {\n logger.error({ error }, 'Failed to build PAR authorization URL');\n throw error;\n }\n}\n\n/**\n * Convenience function to discover OpenID configuration and build an\n * authorization URL. Routes through PAR when `oauthConfig.usePar` is true and\n * the provider advertises `pushed_authorization_request_endpoint`.\n *\n * - `usePar === true` + provider advertises PAR → uses PAR.\n * - `usePar === true` + provider does **not** advertise PAR → throws.\n * - `usePar` unset/false + provider advertises PAR → standard flow + one-line\n * `info` advisory that PAR is available but not enabled.\n * - `usePar` unset/false + no PAR endpoint → standard flow, no advisory.\n *\n * @param oauthConfig - Your OAuth configuration\n * @param pkceOptions - The PKCE options (e.g., code challenge, state, parameters)\n * @returns A Promise that resolves to the authorization URL\n */\nexport async function discoverAndBuildAuthorizationUrl(\n oauthConfig: OAuthConfig,\n pkceOptions: PKCEOptions\n): Promise<URL> {\n logger.debug({ issuer: oauthConfig.issuer }, 'Discovering OpenID configuration');\n\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret)\n );\n\n const parEndpoint = config.serverMetadata().pushed_authorization_request_endpoint;\n\n if (oauthConfig.usePar) {\n if (!parEndpoint) {\n throw new Error(\n 'PAR requested (usePar=true) but provider does not advertise pushed_authorization_request_endpoint',\n );\n }\n return buildAuthorizationUrlWithPAR(config, pkceOptions);\n }\n\n if (parEndpoint) {\n logger.info(\n { issuer: oauthConfig.issuer },\n 'Provider advertises PAR but usePar is not enabled — set OAuthConfig.usePar=true to use it',\n );\n }\n\n return buildAuthorizationUrl(config, pkceOptions);\n } catch (error) {\n logger.error({ error, issuer: oauthConfig.issuer }, 'Failed to discover config or build URL');\n throw error;\n }\n}\n\n/**\n * Configuration for OAuth client credentials flow (machine-to-machine authentication)\n */\nexport type ClientCredentialsConfig = {\n /** OAuth token endpoint URL */\n tokenEndpoint: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Optional scope for the access token */\n scope?: string;\n};\n\n/**\n * Exchanges an authorization code for tokens using OIDC discovery and PKCE.\n *\n * @param oauthConfig - OAuth configuration\n * @param callbackUrl - The full callback URL with query parameters from the OIDC provider\n * @param codeVerifier - The PKCE code verifier stored during login initiation\n * @param expectedState - The state parameter stored during login initiation\n * @returns Token response from the OIDC provider\n */\nexport async function exchangeAuthorizationCode(\n oauthConfig: OAuthConfig,\n callbackUrl: URL,\n codeVerifier: string,\n expectedState: string,\n): Promise<openid.TokenEndpointResponse> {\n logger.debug(\n { issuer: oauthConfig.issuer, clientId: oauthConfig.clientId },\n 'Exchanging authorization code for tokens',\n );\n\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n\n const tokens = await openid.authorizationCodeGrant(\n config,\n callbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState,\n },\n { redirect_uri: oauthConfig.redirect_uri },\n );\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Authorization code exchange successful',\n );\n\n return tokens;\n}\n\n/**\n * Result of extracting user information from an OIDC token response.\n */\nexport interface OidcUserInfo {\n sub: string;\n name: string;\n email: string;\n roles: string[];\n}\n\n/**\n * Extracts user information from an OIDC token response.\n * Decodes the ID token and extracts standard claims plus roles from a configurable claim.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns Extracted user information\n */\nexport function extractUserFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): OidcUserInfo {\n if (!tokens.id_token) {\n throw new Error('OIDC token response is missing id_token');\n }\n\n const idToken = decodeJwt(tokens.id_token);\n\n // Normalize roles: some providers send a single string instead of an array\n const rawRoles = idToken[rolesClaim];\n const nonArrayRoles = typeof rawRoles === 'string' ? [rawRoles] : [];\n const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;\n\n logger.debug({ sub: idToken.sub, rolesClaim, rolesCount: roles.length }, 'Extracted user from ID token');\n\n return {\n sub: idToken.sub ?? '',\n name: idToken.name as string,\n email: idToken.email as string,\n roles,\n };\n}\n\n/**\n * Builds a Session object from an OIDC token response.\n * Maps provider tokens and user claims into the internal Session format.\n *\n * @param tokens - Token response from the OIDC provider\n * @param rolesClaim - Name of the claim containing user roles (default: 'roles')\n * @returns A Session object ready to be encrypted and stored\n */\nexport function buildSessionFromTokens(\n tokens: openid.TokenEndpointResponse,\n rolesClaim: string = 'roles',\n): Session {\n if (!tokens.access_token) {\n throw new Error('OIDC token response is missing access_token');\n }\n\n const userInfo = extractUserFromTokens(tokens, rolesClaim);\n\n let scopes: string[] | undefined;\n if (typeof tokens.scope === 'string') {\n const parsed = tokens.scope.split(' ').filter(s => s.length > 0);\n if (parsed.length > 0) scopes = parsed;\n }\n\n return {\n tokens: {\n accessToken: tokens.access_token,\n accessTokenExpiresAt: tokens.expires_in\n ? new Date(Date.now() + tokens.expires_in * 1000)\n : undefined,\n refreshToken: tokens.refresh_token,\n },\n user: {\n name: userInfo.name,\n email: userInfo.email,\n roles: userInfo.roles,\n },\n scopes,\n };\n}\n\n/**\n * Validates a return URL against open redirect attacks.\n * Ensures the URL is same-origin and passes optional custom validation.\n *\n * @param returnTo - The raw returnTo path/URL to validate\n * @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')\n * @param defaultPath - Fallback path if validation fails (default: '/')\n * @param validate - Optional custom validation function for the pathname\n * @returns A safe, validated URL\n */\nexport function validateReturnUrl(\n returnTo: string | null | undefined,\n applicationOrigin: string,\n defaultPath: string = '/',\n validate?: (pathname: string) => boolean,\n): URL {\n const redirectPath = returnTo ?? defaultPath;\n let redirectUrl: URL;\n\n try {\n redirectUrl = new URL(redirectPath, applicationOrigin);\n } catch {\n logger.warn({ returnTo: redirectPath }, 'Invalid returnTo URL, using default');\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Enforce same-origin — cross-origin returnTo is a potential open redirect attack\n const originUrl = new URL(applicationOrigin);\n if (redirectUrl.origin !== originUrl.origin) {\n logger.error(\n { returnTo: redirectPath, redirectOrigin: redirectUrl.origin, applicationOrigin: originUrl.origin },\n 'Possible open redirect attack: returnTo points outside application origin',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n // Custom validation\n if (validate && !validate(redirectUrl.pathname)) {\n logger.warn(\n { pathname: redirectUrl.pathname },\n 'returnTo failed custom validation, falling back to default',\n );\n return new URL(defaultPath, applicationOrigin);\n }\n\n return redirectUrl;\n}\n\n/**\n * Discovers the OIDC provider's end_session_endpoint and builds a logout URL.\n * Returns null if the provider doesn't expose an end_session_endpoint.\n *\n * @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)\n * @param postLogoutRedirectUri - URL to redirect to after provider logout\n * @returns Logout URL or null if not supported\n */\nexport async function buildOidcLogoutUrl(\n oauthConfig: OAuthConfig,\n postLogoutRedirectUri: string,\n): Promise<URL | null> {\n try {\n const config = await openid.discovery(\n new URL(oauthConfig.issuer),\n oauthConfig.clientId,\n oauthConfig.clientSecret,\n openid.ClientSecretPost(oauthConfig.clientSecret),\n );\n const endSessionEndpoint = config.serverMetadata().end_session_endpoint;\n\n if (!endSessionEndpoint) {\n logger.info({ issuer: oauthConfig.issuer }, 'No end_session_endpoint found');\n return null;\n }\n\n const logoutUrl = new URL(endSessionEndpoint);\n logoutUrl.searchParams.set('client_id', oauthConfig.clientId);\n logoutUrl.searchParams.set('post_logout_redirect_uri', postLogoutRedirectUri);\n\n logger.debug({ logoutUrl: logoutUrl.toString() }, 'Built OIDC logout URL');\n return logoutUrl;\n } catch (error) {\n logger.warn({ error, issuer: oauthConfig.issuer }, 'OIDC discovery failed for logout');\n return null;\n }\n}\n\n/**\n * Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.\n *\n * @param config - Client credentials configuration including token endpoint, client ID, and secret\n * @returns Token response from the OAuth provider\n * @throws Error if the token request fails\n *\n * @example\n * ```typescript\n * const tokens = await clientCredentialsGrant({\n * tokenEndpoint: 'https://api.example.com/oauth2/token',\n * clientId: 'my-client-id',\n * clientSecret: 'my-client-secret',\n * scope: 'api:read api:write',\n * });\n * ```\n */\nexport async function clientCredentialsGrant(\n config: ClientCredentialsConfig\n): Promise<openid.TokenEndpointResponse> {\n try {\n const params = new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: config.clientId,\n client_secret: config.clientSecret,\n });\n\n if (config.scope) {\n params.set('scope', config.scope);\n }\n\n const response = await fetch(config.tokenEndpoint, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: params,\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n { status: response.status, error: errorText },\n 'Client credentials grant failed'\n );\n throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);\n }\n\n return (await response.json()) as openid.TokenEndpointResponse;\n } catch (error) {\n logger.error({ error, tokenEndpoint: config.tokenEndpoint }, 'Client credentials grant error');\n throw error;\n }\n}\n"],"mappings":"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"}