@eventuras/fides-auth 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/logger.js +102 -58
- package/dist/logger.js.map +1 -0
- package/dist/oauth-browser.js +52 -30
- package/dist/oauth-browser.js.map +1 -0
- package/dist/oauth-logging.d.ts +45 -0
- package/dist/oauth-logging.d.ts.map +1 -0
- package/dist/oauth-logging.js +50 -0
- package/dist/oauth-logging.js.map +1 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +295 -136
- package/dist/oauth.js.map +1 -0
- package/dist/providers/vipps/index.js +162 -76
- package/dist/providers/vipps/index.js.map +1 -0
- package/dist/rate-limit.js +55 -17
- package/dist/rate-limit.js.map +1 -0
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +40 -19
- package/dist/session-refresh.js.map +1 -0
- package/dist/session-validation.js +77 -2
- package/dist/session-validation.js.map +1 -0
- package/dist/silent-login.js +133 -33
- package/dist/silent-login.js.map +1 -0
- package/dist/utils.js +196 -2
- package/dist/utils.js.map +1 -0
- package/package.json +8 -4
- package/dist/build-nL2--iMi.js +0 -1014
- package/dist/decode_jwt-1J26fl4I.js +0 -25
- package/dist/decrypt-Cahlu_6Y.js +0 -92
- package/dist/deflate-koSuX7FB.js +0 -1015
- package/dist/session-validation-CHSsh4q_.js +0 -66
- package/dist/utils-C0OavYxH.js +0 -382
package/dist/oauth.js
CHANGED
|
@@ -1,183 +1,342 @@
|
|
|
1
|
-
import { createLogger
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
59
|
+
state
|
|
34
60
|
};
|
|
35
|
-
|
|
36
|
-
redirectUri:
|
|
37
|
-
scope:
|
|
38
|
-
}, "PKCE parameters generated")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
},
|
|
158
|
+
const config = await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret));
|
|
159
|
+
const normalizedCallbackUrl = new URL(oauthConfig.redirect_uri);
|
|
160
|
+
normalizedCallbackUrl.pathname = callbackUrl.pathname;
|
|
161
|
+
normalizedCallbackUrl.search = callbackUrl.search;
|
|
162
|
+
normalizedCallbackUrl.hash = "";
|
|
163
|
+
const tokens = await openid.authorizationCodeGrant(config, normalizedCallbackUrl, {
|
|
164
|
+
pkceCodeVerifier: codeVerifier,
|
|
165
|
+
expectedState
|
|
166
|
+
}, { redirect_uri: oauthConfig.redirect_uri });
|
|
167
|
+
logger.info({
|
|
168
|
+
hasAccessToken: !!tokens.access_token,
|
|
169
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
170
|
+
expiresIn: tokens.expires_in
|
|
171
|
+
}, "Authorization code exchange successful");
|
|
172
|
+
return tokens;
|
|
91
173
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Extracts user information from an OIDC token response.
|
|
176
|
+
* Decodes the ID token and extracts standard claims plus roles from a configurable claim.
|
|
177
|
+
*
|
|
178
|
+
* @param tokens - Token response from the OIDC provider
|
|
179
|
+
* @param rolesClaim - Name of the claim containing user roles (default: 'roles')
|
|
180
|
+
* @returns Extracted user information
|
|
181
|
+
*/
|
|
182
|
+
function extractUserFromTokens(tokens, rolesClaim = "roles") {
|
|
183
|
+
if (!tokens.id_token) throw new Error("OIDC token response is missing id_token");
|
|
184
|
+
const idToken = decodeJwt(tokens.id_token);
|
|
185
|
+
const rawRoles = idToken[rolesClaim];
|
|
186
|
+
const nonArrayRoles = typeof rawRoles === "string" ? [rawRoles] : [];
|
|
187
|
+
const roles = Array.isArray(rawRoles) ? rawRoles : nonArrayRoles;
|
|
188
|
+
logger.debug({
|
|
189
|
+
sub: idToken.sub,
|
|
190
|
+
rolesClaim,
|
|
191
|
+
rolesCount: roles.length
|
|
192
|
+
}, "Extracted user from ID token");
|
|
193
|
+
return {
|
|
194
|
+
sub: idToken.sub ?? "",
|
|
195
|
+
name: idToken.name,
|
|
196
|
+
email: idToken.email,
|
|
197
|
+
roles
|
|
104
198
|
};
|
|
105
199
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
200
|
+
/**
|
|
201
|
+
* Builds a Session object from an OIDC token response.
|
|
202
|
+
* Maps provider tokens and user claims into the internal Session format.
|
|
203
|
+
*
|
|
204
|
+
* @param tokens - Token response from the OIDC provider
|
|
205
|
+
* @param rolesClaim - Name of the claim containing user roles (default: 'roles')
|
|
206
|
+
* @returns A Session object ready to be encrypted and stored
|
|
207
|
+
*/
|
|
208
|
+
function buildSessionFromTokens(tokens, rolesClaim = "roles") {
|
|
209
|
+
if (!tokens.access_token) throw new Error("OIDC token response is missing access_token");
|
|
210
|
+
const userInfo = extractUserFromTokens(tokens, rolesClaim);
|
|
211
|
+
let scopes;
|
|
212
|
+
if (typeof tokens.scope === "string") {
|
|
213
|
+
const parsed = tokens.scope.split(" ").filter((s) => s.length > 0);
|
|
214
|
+
if (parsed.length > 0) scopes = parsed;
|
|
112
215
|
}
|
|
113
216
|
return {
|
|
114
217
|
tokens: {
|
|
115
|
-
accessToken:
|
|
116
|
-
accessTokenExpiresAt:
|
|
117
|
-
refreshToken:
|
|
218
|
+
accessToken: tokens.access_token,
|
|
219
|
+
accessTokenExpiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1e3) : void 0,
|
|
220
|
+
refreshToken: tokens.refresh_token
|
|
118
221
|
},
|
|
119
222
|
user: {
|
|
120
|
-
name:
|
|
121
|
-
email:
|
|
122
|
-
roles:
|
|
223
|
+
name: userInfo.name,
|
|
224
|
+
email: userInfo.email,
|
|
225
|
+
roles: userInfo.roles
|
|
123
226
|
},
|
|
124
|
-
scopes
|
|
227
|
+
scopes
|
|
125
228
|
};
|
|
126
229
|
}
|
|
127
|
-
|
|
128
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Validates a return URL against open redirect attacks.
|
|
232
|
+
* Ensures the URL is same-origin and passes optional custom validation.
|
|
233
|
+
*
|
|
234
|
+
* @param returnTo - The raw returnTo path/URL to validate
|
|
235
|
+
* @param applicationOrigin - The application's origin URL (e.g., 'https://example.com')
|
|
236
|
+
* @param defaultPath - Fallback path if validation fails (default: '/')
|
|
237
|
+
* @param validate - Optional custom validation function for the pathname
|
|
238
|
+
* @returns A safe, validated URL
|
|
239
|
+
*/
|
|
240
|
+
function validateReturnUrl(returnTo, applicationOrigin, defaultPath = "/", validate) {
|
|
241
|
+
const redirectPath = returnTo ?? defaultPath;
|
|
242
|
+
let redirectUrl;
|
|
129
243
|
try {
|
|
130
|
-
|
|
244
|
+
redirectUrl = new URL(redirectPath, applicationOrigin);
|
|
131
245
|
} catch {
|
|
132
|
-
|
|
246
|
+
logger.warn({ returnTo: redirectPath }, "Invalid returnTo URL, using default");
|
|
247
|
+
return new URL(defaultPath, applicationOrigin);
|
|
248
|
+
}
|
|
249
|
+
const originUrl = new URL(applicationOrigin);
|
|
250
|
+
if (redirectUrl.origin !== originUrl.origin) {
|
|
251
|
+
logger.error({
|
|
252
|
+
returnTo: redirectPath,
|
|
253
|
+
redirectOrigin: redirectUrl.origin,
|
|
254
|
+
applicationOrigin: originUrl.origin
|
|
255
|
+
}, "Possible open redirect attack: returnTo points outside application origin");
|
|
256
|
+
return new URL(defaultPath, applicationOrigin);
|
|
133
257
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}, "Possible open redirect attack: returnTo points outside application origin"), new URL(n, t));
|
|
258
|
+
if (validate && !validate(redirectUrl.pathname)) {
|
|
259
|
+
logger.warn({ pathname: redirectUrl.pathname }, "returnTo failed custom validation, falling back to default");
|
|
260
|
+
return new URL(defaultPath, applicationOrigin);
|
|
261
|
+
}
|
|
262
|
+
return redirectUrl;
|
|
140
263
|
}
|
|
141
|
-
|
|
264
|
+
/**
|
|
265
|
+
* Discovers the OIDC provider's end_session_endpoint and builds a logout URL.
|
|
266
|
+
* Returns null if the provider doesn't expose an end_session_endpoint.
|
|
267
|
+
*
|
|
268
|
+
* @param oauthConfig - OAuth configuration (issuer, clientId, clientSecret)
|
|
269
|
+
* @param postLogoutRedirectUri - URL to redirect to after provider logout
|
|
270
|
+
* @returns Logout URL or null if not supported
|
|
271
|
+
*/
|
|
272
|
+
async function buildOidcLogoutUrl(oauthConfig, postLogoutRedirectUri) {
|
|
142
273
|
try {
|
|
143
|
-
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}, "OIDC
|
|
274
|
+
const endSessionEndpoint = (await openid.discovery(new URL(oauthConfig.issuer), oauthConfig.clientId, oauthConfig.clientSecret, openid.ClientSecretPost(oauthConfig.clientSecret))).serverMetadata().end_session_endpoint;
|
|
275
|
+
if (!endSessionEndpoint) {
|
|
276
|
+
logger.info({ issuer: oauthConfig.issuer }, "No end_session_endpoint found");
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const logoutUrl = new URL(endSessionEndpoint);
|
|
280
|
+
logoutUrl.searchParams.set("client_id", oauthConfig.clientId);
|
|
281
|
+
logoutUrl.searchParams.set("post_logout_redirect_uri", postLogoutRedirectUri);
|
|
282
|
+
logger.debug({ logoutUrl: logoutUrl.toString() }, "Built OIDC logout URL");
|
|
283
|
+
return logoutUrl;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
logger.warn({
|
|
286
|
+
error,
|
|
287
|
+
issuer: oauthConfig.issuer
|
|
288
|
+
}, "OIDC discovery failed for logout");
|
|
289
|
+
return null;
|
|
152
290
|
}
|
|
153
291
|
}
|
|
154
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Performs OAuth 2.0 client credentials grant flow for machine-to-machine authentication.
|
|
294
|
+
*
|
|
295
|
+
* @param config - Client credentials configuration including token endpoint, client ID, and secret
|
|
296
|
+
* @returns Token response from the OAuth provider
|
|
297
|
+
* @throws Error if the token request fails
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* const tokens = await clientCredentialsGrant({
|
|
302
|
+
* tokenEndpoint: 'https://api.example.com/oauth2/token',
|
|
303
|
+
* clientId: 'my-client-id',
|
|
304
|
+
* clientSecret: 'my-client-secret',
|
|
305
|
+
* scope: 'api:read api:write',
|
|
306
|
+
* });
|
|
307
|
+
* ```
|
|
308
|
+
*/
|
|
309
|
+
async function clientCredentialsGrant(config) {
|
|
155
310
|
try {
|
|
156
|
-
|
|
311
|
+
const params = new URLSearchParams({
|
|
157
312
|
grant_type: "client_credentials",
|
|
158
|
-
client_id:
|
|
159
|
-
client_secret:
|
|
313
|
+
client_id: config.clientId,
|
|
314
|
+
client_secret: config.clientSecret
|
|
160
315
|
});
|
|
161
|
-
|
|
162
|
-
|
|
316
|
+
if (config.scope) params.set("scope", config.scope);
|
|
317
|
+
const response = await fetch(config.tokenEndpoint, {
|
|
163
318
|
method: "POST",
|
|
164
319
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
165
|
-
body:
|
|
320
|
+
body: params
|
|
166
321
|
});
|
|
167
|
-
if (!
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
status:
|
|
171
|
-
error:
|
|
172
|
-
}, "Client credentials grant failed")
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
const errorText = await response.text();
|
|
324
|
+
logger.error({
|
|
325
|
+
status: response.status,
|
|
326
|
+
error: errorText
|
|
327
|
+
}, "Client credentials grant failed");
|
|
328
|
+
throw new Error(`Client credentials grant failed: ${response.status} - ${errorText}`);
|
|
173
329
|
}
|
|
174
|
-
return await
|
|
175
|
-
} catch (
|
|
176
|
-
|
|
177
|
-
error
|
|
178
|
-
tokenEndpoint:
|
|
179
|
-
}, "Client credentials grant error")
|
|
330
|
+
return await response.json();
|
|
331
|
+
} catch (error) {
|
|
332
|
+
logger.error({
|
|
333
|
+
error,
|
|
334
|
+
tokenEndpoint: config.tokenEndpoint
|
|
335
|
+
}, "Client credentials grant error");
|
|
336
|
+
throw error;
|
|
180
337
|
}
|
|
181
338
|
}
|
|
182
339
|
//#endregion
|
|
183
|
-
export {
|
|
340
|
+
export { buildAuthorizationUrl, buildAuthorizationUrlWithPAR, buildOidcLogoutUrl, buildPKCEOptions, buildSessionFromTokens, clientCredentialsGrant, defaultScope, discoverAndBuildAuthorizationUrl, exchangeAuthorizationCode, extractUserFromTokens, refreshAccessToken, validateReturnUrl };
|
|
341
|
+
|
|
342
|
+
//# 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 // 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 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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"}
|