@better-auth/oauth-provider 1.7.9 → 1.8.0-beta.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/{authorize-D8r6mvZx.mjs → authorize-92EtLsBi.mjs} +735 -502
- package/dist/client-resource.d.mts +1 -1
- package/dist/client-resource.mjs +2 -2
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index-sGnh4IfY.d.mts +275 -0
- package/dist/index.d.mts +4 -203
- package/dist/index.mjs +198 -41
- package/dist/internal.d.mts +108 -40
- package/dist/internal.mjs +4 -2
- package/dist/{introspect-7WCMPe5Z.mjs → introspect-CEo9Q06v.mjs} +318 -237
- package/dist/{oauth-DAMtT2vg.d.mts → oauth-BlEuA8oD.d.mts} +380 -20
- package/dist/{oauth-BWgg8X-A.d.mts → oauth-mdkaIwmm.d.mts} +191 -86
- package/dist/{utils-DKU5wblX.mjs → utils-BMNe8Sg2.mjs} +343 -5
- package/dist/{version-DVUrz-nv.mjs → version-DeABnmP9.mjs} +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { d as
|
|
1
|
+
import { E as oauthClientMetadataSchema, r as getOAuthProviderApi, v as resolveResourcePolicy, x as ResourceUriSchema } from "./introspect-CEo9Q06v.mjs";
|
|
2
|
+
import { C as parseScopes, H as extendOAuthProvider, I as validateClientScopes, L as verifyOAuthQueryParams, N as toResourceList, P as tokenError, i as consentCovers, l as findConsent, p as getOAuthProviderPlugin, u as getClient, v as normalizeClientAuthenticationParameters, w as readFormParams, y as normalizeTimestampValue } from "./utils-BMNe8Sg2.mjs";
|
|
3
|
+
import { c as consumeClientAssertion, d as authServerMetadata, f as metadataResponse, g as oidcServerMetadata, h as oauthProviderOpenIdConfigMetadata, i as oauthProvider, m as oauthProviderAuthServerMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthAuthorizationServerMetadata, r as getOAuthProviderState, t as getIssuer } from "./authorize-92EtLsBi.mjs";
|
|
4
4
|
import { t as createResourceServerChallenge } from "./resource-challenge-Damwi0Or.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { appendQueryParams } from "@better-auth/core/utils/url";
|
|
6
|
+
import { isAPIError } from "better-auth/api";
|
|
6
7
|
import * as z from "zod";
|
|
7
8
|
import { BetterAuthError } from "@better-auth/core/error";
|
|
8
|
-
import { deviceAuthorization, redeemDeviceCode } from "better-auth/plugins/device-authorization";
|
|
9
|
+
import { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceAuthorization, findDeviceCodeByUserCode, redeemDeviceCode } from "better-auth/plugins/device-authorization";
|
|
9
10
|
//#region src/device-code.ts
|
|
10
11
|
/**
|
|
11
12
|
* RFC 8628 device authorization grant type. A registered OAuth client polls the
|
|
@@ -29,16 +30,6 @@ const oauthDeviceRequestFields = {
|
|
|
29
30
|
client_assertion_type: z.string().meta({ description: "OAuth client assertion type" }).optional(),
|
|
30
31
|
resource: deviceCodeResourceSchema.meta({ description: "RFC 8707 resource indicator(s) to bind to this authorization request" }).optional()
|
|
31
32
|
};
|
|
32
|
-
function tokenError(status, error, errorDescription) {
|
|
33
|
-
throw new APIError(status, {
|
|
34
|
-
error,
|
|
35
|
-
error_description: errorDescription
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function parseScopes(scope) {
|
|
39
|
-
const normalized = scope?.trim();
|
|
40
|
-
return normalized ? normalized.split(/\s+/) : [];
|
|
41
|
-
}
|
|
42
33
|
/**
|
|
43
34
|
* Exchanges an approved RFC 8628 device code for an OAuth token set. Unlike the
|
|
44
35
|
* device-authorization plugin's `/device/token` (which mints a first-party
|
|
@@ -52,14 +43,14 @@ async function exchangeOAuthDeviceCode(input) {
|
|
|
52
43
|
const body = ctx.body;
|
|
53
44
|
const deviceCode = body?.device_code;
|
|
54
45
|
if (!deviceCode) tokenError("BAD_REQUEST", "invalid_request", "device_code is required");
|
|
55
|
-
const
|
|
46
|
+
const authentication = await provider.authenticateClient({ requireCredentials: false });
|
|
47
|
+
const { authorizationContext: { client, scopes }, claimedDeviceCode, redemptionContext: { resources, confirmation }, user } = await redeemDeviceCode({
|
|
56
48
|
ctx,
|
|
57
49
|
deviceCode,
|
|
58
50
|
authorizeRedemption: async (record) => {
|
|
59
51
|
if (!record.oauthClientId) tokenError("BAD_REQUEST", "invalid_grant", "invalid device code");
|
|
60
52
|
if (body?.client_id && record.oauthClientId !== body.client_id) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
|
|
61
53
|
const scopes = parseScopes(record.scope);
|
|
62
|
-
const authentication = await provider.authenticateClient({ requireCredentials: false });
|
|
63
54
|
if (record.oauthClientId !== authentication.client.clientId) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
|
|
64
55
|
validateClientScopes(authentication.client, scopes);
|
|
65
56
|
return {
|
|
@@ -86,7 +77,15 @@ async function exchangeOAuthDeviceCode(input) {
|
|
|
86
77
|
clientId: authorization.client.clientId,
|
|
87
78
|
requestedScopes: authorization.scopes
|
|
88
79
|
});
|
|
89
|
-
return
|
|
80
|
+
return {
|
|
81
|
+
resources,
|
|
82
|
+
confirmation: await provider.resolveConfirmation({
|
|
83
|
+
client: authorization.client,
|
|
84
|
+
scopes: authorization.scopes,
|
|
85
|
+
resources,
|
|
86
|
+
confirmation: authorization.confirmation
|
|
87
|
+
})
|
|
88
|
+
};
|
|
90
89
|
}
|
|
91
90
|
});
|
|
92
91
|
return provider.issueTokens({
|
|
@@ -94,10 +93,77 @@ async function exchangeOAuthDeviceCode(input) {
|
|
|
94
93
|
scopes,
|
|
95
94
|
user,
|
|
96
95
|
resources,
|
|
96
|
+
referenceId: claimedDeviceCode.referenceId ?? void 0,
|
|
97
|
+
authTime: normalizeTimestampValue(claimedDeviceCode.authTime),
|
|
98
|
+
acr: claimedDeviceCode.acr ?? void 0,
|
|
99
|
+
amr: claimedDeviceCode.amr ?? void 0,
|
|
100
|
+
originalResources: claimedDeviceCode.resources ?? void 0,
|
|
97
101
|
confirmation
|
|
98
102
|
});
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Lets the user approve an OAuth device code through `/oauth2/authorize`, so
|
|
106
|
+
* device approvals run the same sign-in, account selection, post-login, and
|
|
107
|
+
* consent pages as authorization requests. The user is bound to the code only
|
|
108
|
+
* at the final approval, after any account switch.
|
|
109
|
+
*/
|
|
110
|
+
function deviceApprovalRequest(verificationUri) {
|
|
111
|
+
async function findOAuthDeviceCode(ctx, userCode) {
|
|
112
|
+
const deviceCode = await findDeviceCodeByUserCode(ctx, userCode);
|
|
113
|
+
if (!deviceCode?.oauthClientId) tokenError("BAD_REQUEST", "invalid_request", "Invalid user code");
|
|
114
|
+
return {
|
|
115
|
+
...deviceCode,
|
|
116
|
+
oauthClientId: deviceCode.oauthClientId
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const verificationPage = (ctx, userCode) => buildVerificationUris(verificationUri, ctx.context.baseURL, userCode).verificationUriComplete;
|
|
120
|
+
const completionURL = (ctx, userCode, status) => appendQueryParams(verificationPage(ctx, userCode), new URLSearchParams({ status }));
|
|
121
|
+
return {
|
|
122
|
+
load: async ({ ctx, opts, value, signedQuery }) => {
|
|
123
|
+
const deviceCode = await findOAuthDeviceCode(ctx, value);
|
|
124
|
+
assertDeviceCodePending(deviceCode);
|
|
125
|
+
const client = await getClient(ctx, opts, deviceCode.oauthClientId);
|
|
126
|
+
if (!client) tokenError("BAD_REQUEST", "invalid_client", "client_id is required");
|
|
127
|
+
const scopes = parseScopes(signedQuery?.scope ?? deviceCode.scope);
|
|
128
|
+
return {
|
|
129
|
+
query: {
|
|
130
|
+
user_code: deviceCode.userCode,
|
|
131
|
+
client_id: client.clientId,
|
|
132
|
+
scope: scopes.join(" "),
|
|
133
|
+
resource: deviceCode.resources ?? []
|
|
134
|
+
},
|
|
135
|
+
client,
|
|
136
|
+
requireConsent: true,
|
|
137
|
+
approve: async (approval) => {
|
|
138
|
+
await decideDeviceCode(ctx, {
|
|
139
|
+
deviceCode,
|
|
140
|
+
userId: approval.userId,
|
|
141
|
+
decision: "approved",
|
|
142
|
+
fields: {
|
|
143
|
+
scope: approval.query.scope,
|
|
144
|
+
referenceId: approval.referenceId ?? null,
|
|
145
|
+
authTime: new Date(approval.authTime),
|
|
146
|
+
acr: approval.acr ?? null,
|
|
147
|
+
amr: approval.amr ?? null
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return completionURL(ctx, deviceCode.userCode, "approved");
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
deny: async ({ ctx, value, userId }) => {
|
|
155
|
+
const deviceCode = await findOAuthDeviceCode(ctx, value);
|
|
156
|
+
await decideDeviceCode(ctx, {
|
|
157
|
+
deviceCode,
|
|
158
|
+
userId,
|
|
159
|
+
decision: "denied"
|
|
160
|
+
});
|
|
161
|
+
return completionURL(ctx, deviceCode.userCode, "denied");
|
|
162
|
+
},
|
|
163
|
+
errorPage: ({ ctx, value }) => verificationPage(ctx, value)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function buildOAuthDeviceGrant(verificationUri) {
|
|
101
167
|
return {
|
|
102
168
|
requestSchemaFields: oauthDeviceRequestFields,
|
|
103
169
|
requestErrorCodes: ["invalid_target"],
|
|
@@ -133,14 +199,30 @@ function buildOAuthDeviceGrant() {
|
|
|
133
199
|
oauthClientId: {
|
|
134
200
|
type: "string",
|
|
135
201
|
required: false
|
|
202
|
+
},
|
|
203
|
+
referenceId: {
|
|
204
|
+
type: "string",
|
|
205
|
+
required: false
|
|
206
|
+
},
|
|
207
|
+
authTime: {
|
|
208
|
+
type: "date",
|
|
209
|
+
required: false
|
|
210
|
+
},
|
|
211
|
+
acr: {
|
|
212
|
+
type: "string",
|
|
213
|
+
required: false
|
|
214
|
+
},
|
|
215
|
+
amr: {
|
|
216
|
+
type: "string[]",
|
|
217
|
+
required: false
|
|
136
218
|
}
|
|
137
219
|
},
|
|
138
220
|
grants: { [DEVICE_CODE_GRANT_TYPE]: exchangeOAuthDeviceCode },
|
|
139
221
|
metadata: (metadataInput) => ({ device_authorization_endpoint: `${metadataInput.ctx.context.baseURL}${DEVICE_AUTHORIZATION_PATH}` }),
|
|
140
222
|
authorizeRequest: async ({ ctx, request }) => {
|
|
141
223
|
const { detected: hasClientAuthentication } = await normalizeClientAuthenticationParameters(ctx.request, ctx.body ?? {});
|
|
142
|
-
const formResources = ctx.request ? await
|
|
143
|
-
if (formResources) {
|
|
224
|
+
const formResources = ctx.request ? (await readFormParams(ctx.request))?.getAll("resource").filter(Boolean) : void 0;
|
|
225
|
+
if (formResources?.length) {
|
|
144
226
|
const parsedResource = deviceCodeResourceSchema.safeParse(formResources.length === 1 ? formResources[0] : formResources);
|
|
145
227
|
if (!parsedResource.success) tokenError("BAD_REQUEST", "invalid_target", "Invalid resource indicator");
|
|
146
228
|
request.resource = parsedResource.data;
|
|
@@ -172,26 +254,12 @@ function buildOAuthDeviceGrant() {
|
|
|
172
254
|
}
|
|
173
255
|
};
|
|
174
256
|
},
|
|
175
|
-
|
|
257
|
+
assertStandaloneUse: ({ deviceCode, use }) => {
|
|
176
258
|
if (typeof deviceCode.oauthClientId !== "string") return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
error_description: "This device code must be exchanged at the OAuth token endpoint (/oauth2/token)."
|
|
180
|
-
});
|
|
259
|
+
if (use === "redeem") tokenError("BAD_REQUEST", "invalid_grant", "This device code must be exchanged at the OAuth token endpoint (/oauth2/token).");
|
|
260
|
+
tokenError("BAD_REQUEST", "invalid_request", `Approve this device code through the OAuth provider: send the user to /oauth2/authorize?user_code=${encodeURIComponent(String(deviceCode.userCode))}.`);
|
|
181
261
|
},
|
|
182
|
-
|
|
183
|
-
if (typeof deviceCode.oauthClientId !== "string") return;
|
|
184
|
-
const resources = deviceCode.resources;
|
|
185
|
-
if (!Array.isArray(resources) || !resources.every((resource) => typeof resource === "string")) return;
|
|
186
|
-
return { resource: toAudienceClaim(resources) };
|
|
187
|
-
},
|
|
188
|
-
verificationOpenAPIProperties: { resource: {
|
|
189
|
-
oneOf: [{ type: "string" }, {
|
|
190
|
-
type: "array",
|
|
191
|
-
items: { type: "string" }
|
|
192
|
-
}],
|
|
193
|
-
description: "The requested resource indicators, returned only to the authenticated user who owns this request"
|
|
194
|
-
} }
|
|
262
|
+
interactionRequests: { user_code: deviceApprovalRequest(verificationUri) }
|
|
195
263
|
};
|
|
196
264
|
}
|
|
197
265
|
/**
|
|
@@ -214,7 +282,7 @@ function buildOAuthDeviceGrant() {
|
|
|
214
282
|
* ```
|
|
215
283
|
*/
|
|
216
284
|
function oauthDeviceAuthorization(options = {}) {
|
|
217
|
-
const grant = buildOAuthDeviceGrant();
|
|
285
|
+
const grant = buildOAuthDeviceGrant(options.verificationUri);
|
|
218
286
|
return {
|
|
219
287
|
...deviceAuthorization({
|
|
220
288
|
...options,
|
|
@@ -228,4 +296,93 @@ function oauthDeviceAuthorization(options = {}) {
|
|
|
228
296
|
};
|
|
229
297
|
}
|
|
230
298
|
//#endregion
|
|
231
|
-
|
|
299
|
+
//#region src/token-exchange.ts
|
|
300
|
+
/** RFC 8693 §2.1 token exchange grant type. */
|
|
301
|
+
const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
302
|
+
/** RFC 8693 §3 token type identifier for an OAuth 2.0 access token. */
|
|
303
|
+
const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
304
|
+
const NON_EXCHANGEABLE_SCOPES = new Set(["openid", "offline_access"]);
|
|
305
|
+
function deny(description) {
|
|
306
|
+
tokenError("BAD_REQUEST", "invalid_request", description);
|
|
307
|
+
}
|
|
308
|
+
async function exchangeToken({ ctx, opts, provider }, options) {
|
|
309
|
+
const body = ctx.body ?? {};
|
|
310
|
+
const subjectToken = body.subject_token;
|
|
311
|
+
if (typeof subjectToken !== "string" || !subjectToken) deny("subject_token is required");
|
|
312
|
+
if (body.subject_token_type !== "urn:ietf:params:oauth:token-type:access_token") deny(`subject_token_type must be ${ACCESS_TOKEN_TYPE}`);
|
|
313
|
+
if (body.actor_token !== void 0 || body.actor_token_type !== void 0) deny("actor_token is not supported");
|
|
314
|
+
if (body.requested_token_type !== void 0 && body.requested_token_type !== "urn:ietf:params:oauth:token-type:access_token") deny("requested_token_type is not supported");
|
|
315
|
+
const targets = [body.resource, body.audience].flat().filter((target) => target != null);
|
|
316
|
+
if (targets.length === 0) deny("resource or audience is required");
|
|
317
|
+
const { client, confirmation } = await provider.authenticateClient();
|
|
318
|
+
const userInfo = `${ctx.context.baseURL}/oauth2/userinfo`;
|
|
319
|
+
if (targets.some((target) => typeof target !== "string" || !target || target === userInfo)) tokenError("BAD_REQUEST", "invalid_target", "each resource or audience must be a registered resource");
|
|
320
|
+
const resources = [...new Set(targets)];
|
|
321
|
+
const subject = await Promise.resolve(provider.validateAccessToken(subjectToken, client.clientId)).catch((error) => {
|
|
322
|
+
if (isAPIError(error)) return;
|
|
323
|
+
throw error;
|
|
324
|
+
});
|
|
325
|
+
if (!subject?.active || !subject.sub || !subject.exp) deny("subject_token is invalid");
|
|
326
|
+
const user = await ctx.context.internalAdapter.findUserById(subject.sub);
|
|
327
|
+
if (!user) deny("subject_token does not represent a user");
|
|
328
|
+
const exchangeable = parseScopes(subject.scope).filter((scope) => !NON_EXCHANGEABLE_SCOPES.has(scope));
|
|
329
|
+
const scopes = typeof body.scope === "string" && body.scope.trim() ? [...new Set(parseScopes(body.scope))] : exchangeable.filter((scope) => !client.scopes || client.scopes.includes(scope));
|
|
330
|
+
const widened = scopes.find((scope) => !exchangeable.includes(scope));
|
|
331
|
+
if (widened) tokenError("BAD_REQUEST", "invalid_scope", `scope ${widened} is not available through token exchange`);
|
|
332
|
+
validateClientScopes(client, scopes);
|
|
333
|
+
const { effectiveScopes } = await resolveResourcePolicy(ctx, opts, {
|
|
334
|
+
resource: resources,
|
|
335
|
+
clientId: client.clientId,
|
|
336
|
+
requestedScopes: scopes
|
|
337
|
+
});
|
|
338
|
+
const referenceId = typeof subject.reference_id === "string" && subject.reference_id || void 0;
|
|
339
|
+
if (!client.skipConsent) {
|
|
340
|
+
const consent = await findConsent(ctx, client.clientId, user.id, referenceId);
|
|
341
|
+
if (!consent || !consentCovers(consent, effectiveScopes, resources)) deny("the user has not consented to this client for this exchange");
|
|
342
|
+
}
|
|
343
|
+
if (options.allowExchange && !await options.allowExchange({
|
|
344
|
+
client,
|
|
345
|
+
user,
|
|
346
|
+
subjectToken: subject,
|
|
347
|
+
resources,
|
|
348
|
+
scopes: effectiveScopes,
|
|
349
|
+
referenceId
|
|
350
|
+
})) deny("token exchange denied by policy");
|
|
351
|
+
return provider.issueTokens({
|
|
352
|
+
client,
|
|
353
|
+
user,
|
|
354
|
+
scopes: effectiveScopes,
|
|
355
|
+
resources,
|
|
356
|
+
referenceId,
|
|
357
|
+
sessionId: typeof subject.sid === "string" ? subject.sid : void 0,
|
|
358
|
+
maxExpiresAt: /* @__PURE__ */ new Date(subject.exp * 1e3),
|
|
359
|
+
confirmation,
|
|
360
|
+
tokenResponse: { issued_token_type: ACCESS_TOKEN_TYPE }
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* RFC 8693 token exchange for the OAuth Provider. A confidential client
|
|
365
|
+
* registered for the token-exchange grant trades a user's access token, issued
|
|
366
|
+
* by this server to it or for a resource it serves, for an access token to
|
|
367
|
+
* another registered resource. The issued token represents the same user
|
|
368
|
+
* (impersonation), carries the requesting client as `client_id`, and expires
|
|
369
|
+
* no later than the subject token.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```ts
|
|
373
|
+
* const auth = betterAuth({
|
|
374
|
+
* plugins: [jwt(), oauthProvider({ ... }), oauthTokenExchange()],
|
|
375
|
+
* });
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
function oauthTokenExchange(options = {}) {
|
|
379
|
+
const extension = { grants: { [TOKEN_EXCHANGE_GRANT_TYPE]: (input) => exchangeToken(input, options) } };
|
|
380
|
+
return {
|
|
381
|
+
id: "oauth-token-exchange",
|
|
382
|
+
init(ctx) {
|
|
383
|
+
extendOAuthProvider(ctx, extension);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
export { ACCESS_TOKEN_TYPE, DEFAULT_OAUTH_SCOPES, DEVICE_CODE_GRANT_TYPE, ResourceUriSchema, TOKEN_EXCHANGE_GRANT_TYPE, authServerMetadata, consumeClientAssertion, createResourceServerChallenge, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthClientMetadataSchema, oauthDeviceAuthorization, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthTokenExchange, oidcServerMetadata, verifyOAuthQueryParams };
|
package/dist/internal.d.mts
CHANGED
|
@@ -1,52 +1,120 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { J as Scope, L as OAuthOptions, at as OAuthClientRegistrationResult, ct as registerClientMetadataDocument, ot as RegisterClientMetadataDocumentInput, st as assertPublicRoutableHttpsUri } from "./oauth-BlEuA8oD.mjs";
|
|
2
|
+
import { a as validatePublicClientJwks, i as PublicClientJwksValidationResult, p as createOAuthEndpoint } from "./oauth-mdkaIwmm.mjs";
|
|
3
|
+
import { b as getExtensionClientMetadataFields, i as validateClientScopes, n as parseClientMetadata, r as toResourceList } from "./index-sGnh4IfY.mjs";
|
|
4
|
+
import { JWSAlgorithms } from "better-auth/plugins";
|
|
5
|
+
import { Session, User } from "better-auth/types";
|
|
6
|
+
import { AuthContext, GenericEndpointContext } from "@better-auth/core";
|
|
4
7
|
|
|
5
|
-
//#region src/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
jwks: JSONWebKeySet;
|
|
9
|
-
} | {
|
|
10
|
-
valid: false;
|
|
11
|
-
error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array" | "jwks must contain only public asymmetric keys" | "jwks keys must be supported public JWKs with required key parameters" | "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve";
|
|
12
|
-
};
|
|
13
|
-
/**
|
|
14
|
-
* Validates an OAuth client's public asymmetric JWK set.
|
|
15
|
-
*
|
|
16
|
-
* This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
|
|
17
|
-
* It performs no I/O and returns the validated set for downstream JOSE
|
|
18
|
-
* verification.
|
|
19
|
-
*
|
|
20
|
-
* @internal
|
|
21
|
-
*/
|
|
22
|
-
declare function validatePublicClientJwks(input: unknown): PublicClientJwksValidationResult;
|
|
8
|
+
//#region src/authentication-context.d.ts
|
|
9
|
+
/** The ACR values the provider may assert, advertised in discovery. */
|
|
10
|
+
declare function getAcrValuesSupported(opts: OAuthOptions<Scope[]>): string[];
|
|
23
11
|
//#endregion
|
|
24
12
|
//#region src/client-metadata.d.ts
|
|
25
13
|
declare function isForbiddenCimdClientMetadataField(field: string): boolean;
|
|
26
14
|
//#endregion
|
|
27
|
-
//#region src/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
15
|
+
//#region src/resources.d.ts
|
|
16
|
+
/**
|
|
17
|
+
* Resolved resource policy for a single `/oauth2/token` (or `/oauth2/authorize`)
|
|
18
|
+
* request. Computed by {@link resolveResourcePolicy} and consumed by the
|
|
19
|
+
* token-issuance pipeline.
|
|
20
|
+
*/
|
|
21
|
+
interface ResolvedResourcePolicy {
|
|
22
|
+
/**
|
|
23
|
+
* The `aud` claim value:
|
|
24
|
+
* - `undefined` when the request omitted `resource` (legacy behavior)
|
|
25
|
+
* - a single string when exactly one resource was requested
|
|
26
|
+
* - an array when multiple were requested
|
|
27
|
+
*
|
|
28
|
+
* Includes the implicit `/oauth2/userinfo` audience when `openid` is in
|
|
29
|
+
* scope — the OIDC userinfo endpoint expects to find itself in `aud`.
|
|
30
|
+
*/
|
|
31
|
+
audienceClaim: string | string[] | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Effective access-token TTL in seconds. `null` means "no resource-specific
|
|
34
|
+
* override — caller should use the plugin default."
|
|
35
|
+
*
|
|
36
|
+
* When multiple resources are requested, this is the **minimum** TTL
|
|
37
|
+
* across all of them (OAuth 2.1 §1.5 bounded-blast-radius rule).
|
|
38
|
+
*/
|
|
39
|
+
accessTokenTtl: number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Effective refresh-token TTL in seconds. `null` means "no resource-specific
|
|
42
|
+
* override — caller should use {@link OAuthOptions.refreshTokenExpiresIn}
|
|
43
|
+
* (or its built-in default)."
|
|
44
|
+
*
|
|
45
|
+
* When multiple resources are requested, this is the **minimum** TTL
|
|
46
|
+
* across all of them (same bounded-blast-radius rule as access tokens).
|
|
47
|
+
*/
|
|
48
|
+
refreshTokenTtl: number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Effective signing algorithm and key id for the issued token.
|
|
51
|
+
*
|
|
52
|
+
* The JWS Protected Header (RFC 7515 §4.1) carries exactly one `alg` and
|
|
53
|
+
* at most one `kid`, so a single signed token can satisfy a multi-resource
|
|
54
|
+
* request only when every resource's pins are mutually compatible:
|
|
55
|
+
* the set of unique non-null `signingAlgorithm` values across requested
|
|
56
|
+
* resources must be ≤ 1, and the same for `signingKeyId`. When the
|
|
57
|
+
* compatibility check passes, the single unique pin (if any) is honored;
|
|
58
|
+
* `null` means "no resource pinned this, fall back to the JWT plugin
|
|
59
|
+
* default." Conflicts raise `invalid_request`.
|
|
60
|
+
*/
|
|
61
|
+
signingAlgorithm: JWSAlgorithms | null;
|
|
62
|
+
signingKeyId: string | null;
|
|
63
|
+
/**
|
|
64
|
+
* Merged custom claims across requested resources, returned RAW: reserved
|
|
65
|
+
* RFC 9068 names are NOT stripped here. Feed this into
|
|
66
|
+
* `resolveAccessTokenClaims`, which owns the single strip; do not place it on
|
|
67
|
+
* a token directly or a resource could override an AS-owned claim.
|
|
68
|
+
*/
|
|
69
|
+
rawCustomClaims: Record<string, unknown>;
|
|
70
|
+
/**
|
|
71
|
+
* True when at least one requested resource requires DPoP-bound access
|
|
72
|
+
* tokens.
|
|
73
|
+
*/
|
|
74
|
+
dpopBoundAccessTokensRequired: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The intersection of the caller's `requestedScopes` with each requested
|
|
77
|
+
* resource's `allowedScopes`. When no requested resource defines an
|
|
78
|
+
* allowlist, equals `requestedScopes` unchanged.
|
|
79
|
+
*
|
|
80
|
+
* - A resource with `allowedScopes: null` is treated as "no restriction"
|
|
81
|
+
* for that resource (other resources' allowlists still apply).
|
|
82
|
+
* - A resource whose allowlist excludes every requested scope causes
|
|
83
|
+
* {@link resolveResourcePolicy} to throw `invalid_scope`.
|
|
84
|
+
* - Otherwise this is the filtered scope set the token endpoint should
|
|
85
|
+
* mint with (RFC 6749 §3.3 narrow-but-don't-widen).
|
|
86
|
+
*/
|
|
87
|
+
effectiveScopes: string[];
|
|
39
88
|
}
|
|
40
89
|
/**
|
|
41
|
-
*
|
|
90
|
+
* Resolves the resource policy for a request.
|
|
42
91
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
92
|
+
* Validation steps (RFC 8707 §3 + per-resource policy):
|
|
93
|
+
*
|
|
94
|
+
* 1. Resolve `resource` parameter to a list of identifiers.
|
|
95
|
+
* 2. For each identifier, look up the {@link OAuthResource} row.
|
|
96
|
+
* - Missing → `invalid_target`.
|
|
97
|
+
* - `disabled` → `invalid_target` (no new issuance).
|
|
98
|
+
* 3. If {@link OAuthOptions.enforcePerClientResources} resolves to true,
|
|
99
|
+
* confirm the client is linked to every requested resource via
|
|
100
|
+
* `oauthClientResource`. Unlinked → `invalid_target`.
|
|
101
|
+
* 4. Intersect `requestedScopes` with each resource's `allowedScopes`.
|
|
102
|
+
* Empty intersection → `invalid_scope`.
|
|
103
|
+
* 5. Pick the minimum `accessTokenTtl` across requested resources.
|
|
104
|
+
* 6. Pick signing config — only honored for single-resource requests
|
|
105
|
+
* (a JWT can only have one signature).
|
|
106
|
+
* 7. Merge per-resource `customClaims` (returned raw; reserved-claim
|
|
107
|
+
* stripping is owned by `resolveAccessTokenClaims`).
|
|
108
|
+
*
|
|
109
|
+
* When no `resource` param is present, returns an empty policy: no `aud`
|
|
110
|
+
* claim and no resource-specific overrides.
|
|
47
111
|
*
|
|
48
112
|
* @internal
|
|
49
113
|
*/
|
|
50
|
-
declare function
|
|
114
|
+
declare function resolveResourcePolicy(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>, params: {
|
|
115
|
+
resource: unknown;
|
|
116
|
+
clientId: string;
|
|
117
|
+
requestedScopes: string[];
|
|
118
|
+
}): Promise<ResolvedResourcePolicy>;
|
|
51
119
|
//#endregion
|
|
52
|
-
export { type OAuthClientRegistrationResult, type PublicClientJwksValidationResult, type RegisterClientMetadataDocumentInput, isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
|
|
120
|
+
export { type OAuthClientRegistrationResult, type PublicClientJwksValidationResult, type RegisterClientMetadataDocumentInput, assertPublicRoutableHttpsUri, createOAuthEndpoint, getAcrValuesSupported, getExtensionClientMetadataFields, isForbiddenCimdClientMetadataField, parseClientMetadata, registerClientMetadataDocument, resolveResourcePolicy, toResourceList, validateClientScopes, validatePublicClientJwks };
|
package/dist/internal.mjs
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { A as getAcrValuesSupported, v as resolveResourcePolicy } from "./introspect-CEo9Q06v.mjs";
|
|
2
|
+
import { I as validateClientScopes, N as toResourceList, W as getExtensionClientMetadataFields, et as isForbiddenCimdClientMetadataField, x as parseClientMetadata } from "./utils-BMNe8Sg2.mjs";
|
|
3
|
+
import { a as assertPublicRoutableHttpsUri, l as validatePublicClientJwks, o as registerClientMetadataDocument, u as createOAuthEndpoint } from "./authorize-92EtLsBi.mjs";
|
|
4
|
+
export { assertPublicRoutableHttpsUri, createOAuthEndpoint, getAcrValuesSupported, getExtensionClientMetadataFields, isForbiddenCimdClientMetadataField, parseClientMetadata, registerClientMetadataDocument, resolveResourcePolicy, toResourceList, validateClientScopes, validatePublicClientJwks };
|