@better-auth/oauth-provider 1.7.0-rc.5 → 1.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/authorize-BP2_wo6r.mjs +5710 -0
- package/dist/client-resource.d.mts +5 -7
- package/dist/client-resource.mjs +4 -4
- package/dist/client.d.mts +4 -2
- package/dist/client.mjs +2 -2
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +5 -5992
- package/dist/internal.d.mts +2 -1
- package/dist/internal.mjs +1 -2
- package/dist/introspect-Cs1_yhqH.mjs +2551 -0
- package/dist/{oauth-CjcHkAmR.d.mts → oauth-1Ud-hvZY.d.mts} +13 -26
- package/dist/{oauth-4LGMyr3C.d.mts → oauth-D8xpKR0_.d.mts} +143 -95
- package/dist/{resource-challenge-CiJTlsEh.mjs → resource-challenge-Damwi0Or.mjs} +1 -1
- package/dist/rolldown-runtime-wcPFST8Q.mjs +13 -0
- package/dist/{signed-query-BQAwsV_w.mjs → signed-query-Df1MNiSH.mjs} +1 -1
- package/dist/{utils-jZEA4KQb.mjs → utils-CCi1maVd.mjs} +4 -4
- package/dist/{version-CwxXc1Y6.mjs → version-C0TE7qRr.mjs} +1 -1
- package/package.json +7 -7
- package/dist/client-assertion-Cy8ZcH7P.mjs +0 -469
- package/dist/register-BjyzmoSI.mjs +0 -1557
|
@@ -1,1557 +0,0 @@
|
|
|
1
|
-
import { i as validatePublicClientJwks, r as isPrivateHostname } from "./client-assertion-Cy8ZcH7P.mjs";
|
|
2
|
-
import { I as getSupportedAuthMethods, L as getSupportedGrantTypes, S as storeClientSecret, h as parseClientMetadata, m as parseBearerToken, z as isExtensionTokenEndpointAuthMethod } from "./utils-jZEA4KQb.mjs";
|
|
3
|
-
import { isLoopbackIP } from "@better-auth/core/utils/host";
|
|
4
|
-
import { APIError, NO_STORE_HEADERS, getSessionFromCtx } from "better-auth/api";
|
|
5
|
-
import { generateRandomString } from "better-auth/crypto";
|
|
6
|
-
import { APIError as APIError$1 } from "better-call";
|
|
7
|
-
import { logger } from "@better-auth/core/env";
|
|
8
|
-
import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
|
|
9
|
-
import { toExpJWT } from "better-auth/plugins";
|
|
10
|
-
import { isReverseDomainPrivateUseRedirectUri } from "@better-auth/core/utils/redirect-uri";
|
|
11
|
-
//#region src/resources.ts
|
|
12
|
-
/**
|
|
13
|
-
* Source-of-truth list of asymmetric JWS algorithms supported by the JWT
|
|
14
|
-
* plugin. Mirrors the {@link JWSAlgorithms} literal-union type from
|
|
15
|
-
* `packages/better-auth/src/plugins/jwt/types.ts`. Kept as a runtime const
|
|
16
|
-
* so the admin-CRUD zod schema AND the seed-config validator can reject
|
|
17
|
-
* bad values up-front rather than surfacing opaque jose errors at issuance.
|
|
18
|
-
*
|
|
19
|
-
* MUST stay in sync with `JWSAlgorithms`. The type-level guard immediately
|
|
20
|
-
* below fails the typecheck if the two drift.
|
|
21
|
-
*/
|
|
22
|
-
const JWS_ALGORITHMS = [
|
|
23
|
-
"EdDSA",
|
|
24
|
-
"ES256",
|
|
25
|
-
"ES512",
|
|
26
|
-
"PS256",
|
|
27
|
-
"RS256"
|
|
28
|
-
];
|
|
29
|
-
const JWS_ALGORITHM_SET = new Set(JWS_ALGORITHMS);
|
|
30
|
-
/**
|
|
31
|
-
* Upper bound on how many entries are accepted in a JWT's `aud` claim during
|
|
32
|
-
* introspection / revocation validation. Without a cap, a hostile or replayed
|
|
33
|
-
* token can supply hundreds of fake resource identifiers and amplify load on
|
|
34
|
-
* the resource table (one DB lookup per unique unrecognized entry). Real-world
|
|
35
|
-
* deployments use single-digit resource lists; 64 is a generous ceiling that
|
|
36
|
-
* stays well clear of any legitimate use and bounds the per-request DB fan-out.
|
|
37
|
-
*
|
|
38
|
-
* RFC 7519 §4.1.3 does not specify a maximum, so this is a defensive limit,
|
|
39
|
-
* not a spec one. Tokens that exceed it are treated as invalid (introspect →
|
|
40
|
-
* `active: false`, revoke → no-op) rather than triggering the lookup path.
|
|
41
|
-
*
|
|
42
|
-
* @internal
|
|
43
|
-
*/
|
|
44
|
-
const MAX_AUD_VALUES = 64;
|
|
45
|
-
/**
|
|
46
|
-
* Validates a resource `identifier` per RFC 8707 §2 (absolute URI, no
|
|
47
|
-
* fragment), honoring {@link OAuthOptions.identifierValidator} when provided.
|
|
48
|
-
*
|
|
49
|
-
* Returns a structured result so callers can decide whether to throw
|
|
50
|
-
* (admin CRUD / DCR) or warn-and-skip (config seed path).
|
|
51
|
-
*
|
|
52
|
-
* @internal
|
|
53
|
-
*/
|
|
54
|
-
async function checkIdentifier(opts, identifier) {
|
|
55
|
-
const customValidator = opts.identifierValidator;
|
|
56
|
-
if (customValidator) {
|
|
57
|
-
if (!await customValidator(identifier)) return {
|
|
58
|
-
ok: false,
|
|
59
|
-
reason: `resource identifier ${identifier} failed validation`
|
|
60
|
-
};
|
|
61
|
-
return { ok: true };
|
|
62
|
-
}
|
|
63
|
-
let url;
|
|
64
|
-
try {
|
|
65
|
-
url = new URL(identifier);
|
|
66
|
-
} catch {
|
|
67
|
-
return {
|
|
68
|
-
ok: false,
|
|
69
|
-
reason: `resource identifier ${identifier} must be an absolute URI (RFC 8707 §2)`
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
if (url.hash) return {
|
|
73
|
-
ok: false,
|
|
74
|
-
reason: `resource identifier ${identifier} must not contain a URI fragment (RFC 8707 §2)`
|
|
75
|
-
};
|
|
76
|
-
return { ok: true };
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Variant of {@link checkIdentifier} that throws `invalid_target` on failure.
|
|
80
|
-
* Used by admin CRUD endpoints where validation failure is a client error.
|
|
81
|
-
*
|
|
82
|
-
* @internal
|
|
83
|
-
*/
|
|
84
|
-
async function assertIdentifierValid(opts, identifier) {
|
|
85
|
-
const result = await checkIdentifier(opts, identifier);
|
|
86
|
-
if (!result.ok) throw new APIError("BAD_REQUEST", {
|
|
87
|
-
error: "invalid_target",
|
|
88
|
-
error_description: result.reason
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* The OIDC userinfo endpoint's resource identifier. Always accepted as an
|
|
93
|
-
* implicit `aud` value when `openid` is in scope — not looked up against
|
|
94
|
-
* `oauthResource` rows.
|
|
95
|
-
*/
|
|
96
|
-
const userInfoResource = (baseURL) => `${baseURL}/oauth2/userinfo`;
|
|
97
|
-
/**
|
|
98
|
-
* Re-parses a request's raw `application/x-www-form-urlencoded` body to
|
|
99
|
-
* recover every value of the repeated `resource` parameter (RFC 8707 §2).
|
|
100
|
-
*
|
|
101
|
-
* Workaround for better-call's form-body parser (better-call ≤1.3.5), which
|
|
102
|
-
* collapses repeated form keys with last-write-wins. A client sending
|
|
103
|
-
* `resource=https://a&resource=https://b` would otherwise arrive in the
|
|
104
|
-
* handler as `{ resource: "https://b" }`, silently narrowing the issued
|
|
105
|
-
* token's `aud` to a single resource.
|
|
106
|
-
*
|
|
107
|
-
* Returns the full ordered list when the body is form-encoded and contains
|
|
108
|
-
* any `resource` entries; `undefined` otherwise. Caller MUST enable
|
|
109
|
-
* `cloneRequest: true` on the endpoint — the body stream is read here a
|
|
110
|
-
* second time, and that only works when better-call cloned the request
|
|
111
|
-
* before its own parse.
|
|
112
|
-
*
|
|
113
|
-
* Note: the URL-encoded body path is the only one affected. The query-string
|
|
114
|
-
* path (e.g. `/oauth2/authorize?resource=…&resource=…`) is parsed by the
|
|
115
|
-
* router itself, which DOES array-promote duplicate keys correctly.
|
|
116
|
-
*
|
|
117
|
-
* @internal
|
|
118
|
-
*/
|
|
119
|
-
async function extractRepeatedResourceFromForm(request) {
|
|
120
|
-
if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
|
|
121
|
-
let text;
|
|
122
|
-
try {
|
|
123
|
-
text = await request.clone().text();
|
|
124
|
-
} catch {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (!text) return void 0;
|
|
128
|
-
const values = new URLSearchParams(text).getAll("resource").filter((value) => value.length > 0);
|
|
129
|
-
return values.length > 0 ? values : void 0;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Normalizes a `resource` parameter into an array. Accepts the RFC 8707
|
|
133
|
-
* forms: a single string, a string[], or undefined.
|
|
134
|
-
*
|
|
135
|
-
* @internal
|
|
136
|
-
*/
|
|
137
|
-
function normalizeResourceParam(resource) {
|
|
138
|
-
if (resource === void 0 || resource === null) return void 0;
|
|
139
|
-
if (typeof resource === "string") return [resource];
|
|
140
|
-
if (Array.isArray(resource)) {
|
|
141
|
-
const result = resource.filter((r) => typeof r === "string" && r.length > 0);
|
|
142
|
-
return result.length > 0 ? result : void 0;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Validates a JWT `aud` claim against the OAuth resource model.
|
|
147
|
-
*
|
|
148
|
-
* Every non-implicit audience value must resolve to an `oauthResource` row.
|
|
149
|
-
* Disabled rows still pass because disabling blocks new issuance; deleting the
|
|
150
|
-
* row is the operation that invalidates already-issued tokens.
|
|
151
|
-
*
|
|
152
|
-
* @internal
|
|
153
|
-
*/
|
|
154
|
-
async function isAudienceClaimAllowed(ctx, opts, audienceClaim, implicitAudiences = []) {
|
|
155
|
-
if (audienceClaim === void 0) return true;
|
|
156
|
-
const audienceValues = Array.isArray(audienceClaim) ? audienceClaim : [audienceClaim];
|
|
157
|
-
if (audienceValues.length > MAX_AUD_VALUES) return false;
|
|
158
|
-
const implicitAudienceSet = new Set(implicitAudiences);
|
|
159
|
-
const resourcesToLookup = /* @__PURE__ */ new Set();
|
|
160
|
-
for (const audienceValue of audienceValues) {
|
|
161
|
-
if (implicitAudienceSet.has(audienceValue)) continue;
|
|
162
|
-
resourcesToLookup.add(audienceValue);
|
|
163
|
-
}
|
|
164
|
-
if (resourcesToLookup.size === 0) return true;
|
|
165
|
-
return (await Promise.all(Array.from(resourcesToLookup, (resource) => getResource(ctx, opts, resource)))).every(Boolean);
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Resolves the resource policy for a request.
|
|
169
|
-
*
|
|
170
|
-
* Validation steps (RFC 8707 §3 + per-resource policy):
|
|
171
|
-
*
|
|
172
|
-
* 1. Resolve `resource` parameter to a list of identifiers.
|
|
173
|
-
* 2. For each identifier, look up the {@link OAuthResource} row.
|
|
174
|
-
* - Missing → `invalid_target`.
|
|
175
|
-
* - `disabled` → `invalid_target` (no new issuance).
|
|
176
|
-
* 3. If {@link OAuthOptions.enforcePerClientResources} resolves to true,
|
|
177
|
-
* confirm the client is linked to every requested resource via
|
|
178
|
-
* `oauthClientResource`. Unlinked → `invalid_target`.
|
|
179
|
-
* 4. Intersect `requestedScopes` with each resource's `allowedScopes`.
|
|
180
|
-
* Empty intersection → `invalid_scope`.
|
|
181
|
-
* 5. Pick the minimum `accessTokenTtl` across requested resources.
|
|
182
|
-
* 6. Pick signing config — only honored for single-resource requests
|
|
183
|
-
* (a JWT can only have one signature).
|
|
184
|
-
* 7. Merge per-resource `customClaims` (returned raw; reserved-claim
|
|
185
|
-
* stripping is owned by `resolveAccessTokenClaims`).
|
|
186
|
-
*
|
|
187
|
-
* When no `resource` param is present, returns an empty policy: no `aud`
|
|
188
|
-
* claim and no resource-specific overrides.
|
|
189
|
-
*
|
|
190
|
-
* @internal
|
|
191
|
-
*/
|
|
192
|
-
async function resolveResourcePolicy(ctx, opts, params) {
|
|
193
|
-
const requestedResources = normalizeResourceParam(params.resource);
|
|
194
|
-
const includesOpenid = params.requestedScopes.includes("openid");
|
|
195
|
-
const baseURL = ctx.context.baseURL ?? "";
|
|
196
|
-
const userInfoResourceIdentifier = userInfoResource(baseURL);
|
|
197
|
-
if (!requestedResources) return {
|
|
198
|
-
audienceClaim: void 0,
|
|
199
|
-
accessTokenTtl: null,
|
|
200
|
-
refreshTokenTtl: null,
|
|
201
|
-
signingAlgorithm: null,
|
|
202
|
-
signingKeyId: null,
|
|
203
|
-
rawCustomClaims: {},
|
|
204
|
-
dpopBoundAccessTokensRequired: false,
|
|
205
|
-
effectiveScopes: [...params.requestedScopes]
|
|
206
|
-
};
|
|
207
|
-
const uniqueRequestedResources = [...new Set(requestedResources)];
|
|
208
|
-
const resolved = [];
|
|
209
|
-
for (const identifier of uniqueRequestedResources) {
|
|
210
|
-
if (identifier === userInfoResourceIdentifier) continue;
|
|
211
|
-
const row = await getResource(ctx, opts, identifier);
|
|
212
|
-
if (row) {
|
|
213
|
-
if (row.disabled) throw new APIError("BAD_REQUEST", {
|
|
214
|
-
error: "invalid_target",
|
|
215
|
-
error_description: `requested resource ${identifier} is disabled`
|
|
216
|
-
});
|
|
217
|
-
resolved.push(row);
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
throw new APIError("BAD_REQUEST", {
|
|
221
|
-
error: "invalid_target",
|
|
222
|
-
error_description: `requested resource ${identifier} is not configured`
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
const { value: enforcePerClient } = resolveEnforcePerClientResources(opts);
|
|
226
|
-
if (enforcePerClient && resolved.length > 0) await assertClientLinkedToResources(ctx, opts, params.clientId, resolved);
|
|
227
|
-
let effectiveScopes = [...params.requestedScopes];
|
|
228
|
-
for (const row of resolved) {
|
|
229
|
-
if (row.allowedScopes === null || row.allowedScopes === void 0) continue;
|
|
230
|
-
const allowed = new Set(row.allowedScopes);
|
|
231
|
-
const intersection = effectiveScopes.filter((s) => allowed.has(s));
|
|
232
|
-
if (intersection.length === 0) throw new APIError("BAD_REQUEST", {
|
|
233
|
-
error: "invalid_scope",
|
|
234
|
-
error_description: `none of the requested scopes are allowed for resource ${row.identifier}`
|
|
235
|
-
});
|
|
236
|
-
effectiveScopes = intersection;
|
|
237
|
-
}
|
|
238
|
-
let accessTokenTtl = null;
|
|
239
|
-
let refreshTokenTtl = null;
|
|
240
|
-
for (const row of resolved) {
|
|
241
|
-
if (row.accessTokenTtl != null) accessTokenTtl = accessTokenTtl === null ? row.accessTokenTtl : Math.min(accessTokenTtl, row.accessTokenTtl);
|
|
242
|
-
if (row.refreshTokenTtl != null) refreshTokenTtl = refreshTokenTtl === null ? row.refreshTokenTtl : Math.min(refreshTokenTtl, row.refreshTokenTtl);
|
|
243
|
-
}
|
|
244
|
-
const uniqueSigningAlgs = /* @__PURE__ */ new Set();
|
|
245
|
-
const uniqueSigningKids = /* @__PURE__ */ new Set();
|
|
246
|
-
for (const row of resolved) {
|
|
247
|
-
if (row.signingAlgorithm) uniqueSigningAlgs.add(row.signingAlgorithm);
|
|
248
|
-
if (row.signingKeyId) uniqueSigningKids.add(row.signingKeyId);
|
|
249
|
-
}
|
|
250
|
-
if (uniqueSigningAlgs.size > 1) throw new APIError("BAD_REQUEST", {
|
|
251
|
-
error: "invalid_request",
|
|
252
|
-
error_description: "multi-resource request has conflicting signingAlgorithm pins; a single JWS signature cannot satisfy multiple algorithms"
|
|
253
|
-
});
|
|
254
|
-
if (uniqueSigningKids.size > 1) throw new APIError("BAD_REQUEST", {
|
|
255
|
-
error: "invalid_request",
|
|
256
|
-
error_description: "multi-resource request has conflicting signingKeyId pins; a single JWS signature cannot satisfy multiple key ids"
|
|
257
|
-
});
|
|
258
|
-
const signingAlgorithm = uniqueSigningAlgs.values().next().value ?? null;
|
|
259
|
-
const signingKeyId = uniqueSigningKids.values().next().value ?? null;
|
|
260
|
-
const mergedClaims = {};
|
|
261
|
-
for (const row of resolved) if (row.customClaims && typeof row.customClaims === "object") Object.assign(mergedClaims, row.customClaims);
|
|
262
|
-
const dpopBoundAccessTokensRequired = resolved.some((row) => row.dpopBoundAccessTokensRequired === true);
|
|
263
|
-
const audienceIdentifiers = includesOpenid ? [...uniqueRequestedResources, userInfoResourceIdentifier] : uniqueRequestedResources;
|
|
264
|
-
const audClaim = [...new Set(audienceIdentifiers)];
|
|
265
|
-
return {
|
|
266
|
-
audienceClaim: audClaim.length === 1 ? audClaim[0] : audClaim,
|
|
267
|
-
accessTokenTtl,
|
|
268
|
-
refreshTokenTtl,
|
|
269
|
-
signingAlgorithm,
|
|
270
|
-
signingKeyId,
|
|
271
|
-
rawCustomClaims: mergedClaims,
|
|
272
|
-
dpopBoundAccessTokensRequired,
|
|
273
|
-
effectiveScopes
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Throws `invalid_target` if the client isn't linked to every resource via
|
|
278
|
-
* the `oauthClientResource` join table. Issues one `findMany` per call.
|
|
279
|
-
*
|
|
280
|
-
* @internal
|
|
281
|
-
*/
|
|
282
|
-
async function assertClientLinkedToResources(ctx, opts, clientId, resources) {
|
|
283
|
-
if (resources.length === 0) return;
|
|
284
|
-
const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
285
|
-
const links = await ctx.context.adapter.findMany({
|
|
286
|
-
model: modelName,
|
|
287
|
-
where: [{
|
|
288
|
-
field: "clientId",
|
|
289
|
-
value: clientId
|
|
290
|
-
}]
|
|
291
|
-
});
|
|
292
|
-
const linkedSet = new Set(links?.map((l) => l.resourceId) ?? []);
|
|
293
|
-
const unlinked = resources.filter((resource) => !linkedSet.has(resource.identifier));
|
|
294
|
-
if (unlinked.length > 0) throw new APIError("BAD_REQUEST", {
|
|
295
|
-
error: "invalid_target",
|
|
296
|
-
error_description: `client ${clientId} is not linked to resource(s) ${unlinked.map((resource) => resource.identifier).join(", ")}`
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Returns `true` when `clientId` is linked, via the `oauthClientResource` join
|
|
301
|
-
* table, to at least one of `resourceIdentifiers`. Authorizes cross-client token
|
|
302
|
-
* introspection (RFC 7662 §2.1/§4): a resource server may introspect a token
|
|
303
|
-
* whose audience it serves, even when a different client issued the token.
|
|
304
|
-
*
|
|
305
|
-
* @internal
|
|
306
|
-
*/
|
|
307
|
-
async function isClientLinkedToAnyResource(ctx, opts, clientId, resourceIdentifiers) {
|
|
308
|
-
if (resourceIdentifiers.length === 0) return false;
|
|
309
|
-
const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
310
|
-
return ((await ctx.context.adapter.findMany({
|
|
311
|
-
model: modelName,
|
|
312
|
-
where: [{
|
|
313
|
-
field: "clientId",
|
|
314
|
-
value: clientId
|
|
315
|
-
}, {
|
|
316
|
-
field: "resourceId",
|
|
317
|
-
operator: "in",
|
|
318
|
-
value: resourceIdentifiers
|
|
319
|
-
}],
|
|
320
|
-
limit: 1
|
|
321
|
-
}))?.length ?? 0) > 0;
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Merges `customClaims` from the existing rows of `resourceIdentifiers` in
|
|
325
|
-
* order (later resources win on collision). Missing rows are skipped; disabled
|
|
326
|
-
* rows are included, since an already-issued token keeps its claims until it
|
|
327
|
-
* expires. Returned raw; `resolveAccessTokenClaims` strips reserved names.
|
|
328
|
-
*
|
|
329
|
-
* Unlike {@link resolveResourcePolicy}, this applies none of the new-issuance
|
|
330
|
-
* gates (disabled, scope allowlist, client linkage), so it is the right way to
|
|
331
|
-
* re-derive claims for an already-issued token at introspection.
|
|
332
|
-
*
|
|
333
|
-
* @internal
|
|
334
|
-
*/
|
|
335
|
-
async function getResourceCustomClaims(ctx, opts, resourceIdentifiers) {
|
|
336
|
-
if (resourceIdentifiers.length === 0) return {};
|
|
337
|
-
const rows = await ctx.context.adapter.findMany({
|
|
338
|
-
model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
|
|
339
|
-
where: [{
|
|
340
|
-
field: "identifier",
|
|
341
|
-
operator: "in",
|
|
342
|
-
value: resourceIdentifiers
|
|
343
|
-
}]
|
|
344
|
-
});
|
|
345
|
-
const rowByIdentifier = new Map(rows.map((row) => [row.identifier, row]));
|
|
346
|
-
const merged = {};
|
|
347
|
-
for (const identifier of resourceIdentifiers) {
|
|
348
|
-
const row = rowByIdentifier.get(identifier);
|
|
349
|
-
if (row?.customClaims && typeof row.customClaims === "object") Object.assign(merged, row.customClaims);
|
|
350
|
-
}
|
|
351
|
-
return merged;
|
|
352
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Resolves the effective {@link OAuthOptions.enforcePerClientResources} value.
|
|
355
|
-
*
|
|
356
|
-
* - Explicit `true | false` always wins (`source: "explicit"`).
|
|
357
|
-
* - Otherwise resolve to `true` (`source: "default"`). RFC 8707 §3 per-client
|
|
358
|
-
* validation is the secure default.
|
|
359
|
-
*
|
|
360
|
-
* Deterministic and pure — safe to call on every validation pass.
|
|
361
|
-
*/
|
|
362
|
-
function resolveEnforcePerClientResources(opts) {
|
|
363
|
-
if (opts.enforcePerClientResources !== void 0) return {
|
|
364
|
-
value: opts.enforcePerClientResources,
|
|
365
|
-
source: "explicit"
|
|
366
|
-
};
|
|
367
|
-
return {
|
|
368
|
-
value: true,
|
|
369
|
-
source: "default"
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* In-process cache of {@link OAuthResource} rows, keyed by `identifier`.
|
|
374
|
-
*
|
|
375
|
-
* Opt-in via {@link OAuthOptions.cachedResources} (same membership-Set pattern
|
|
376
|
-
* as `cachedTrustedClients`). Resources outside the set are looked up from the
|
|
377
|
-
* DB on every request — the safe default for deployments that edit rows
|
|
378
|
-
* through external tooling.
|
|
379
|
-
*
|
|
380
|
-
* Module-scoped so admin CRUD handlers can invalidate from anywhere via
|
|
381
|
-
* {@link invalidateResourceCache}.
|
|
382
|
-
*/
|
|
383
|
-
const resourceCache = /* @__PURE__ */ new Map();
|
|
384
|
-
/**
|
|
385
|
-
* Removes an entry from the resource cache. Called by admin CRUD handlers
|
|
386
|
-
* after every write. Pass no argument to clear the entire cache.
|
|
387
|
-
*
|
|
388
|
-
* @internal
|
|
389
|
-
*/
|
|
390
|
-
function invalidateResourceCache(identifier) {
|
|
391
|
-
if (identifier === void 0) {
|
|
392
|
-
resourceCache.clear();
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
resourceCache.delete(identifier);
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Looks up an {@link OAuthResource} by `identifier`, consulting the in-process
|
|
399
|
-
* cache when the identifier is in {@link OAuthOptions.cachedResources}.
|
|
400
|
-
*
|
|
401
|
-
* Triggers the lazy seed via {@link seedResourcesOnce} on first call — this
|
|
402
|
-
* is the safety net for deployments where migrations run after plugin init
|
|
403
|
-
* (so seeding at init couldn't see the table yet). The seed is idempotent
|
|
404
|
-
* and coalesced, so the cost is paid only once per process.
|
|
405
|
-
*
|
|
406
|
-
* Returns a defensive copy so callers can't mutate cached state.
|
|
407
|
-
*
|
|
408
|
-
* @internal
|
|
409
|
-
*/
|
|
410
|
-
async function getResource(ctx, opts, identifier) {
|
|
411
|
-
await seedResourcesOnce(ctx.context, opts);
|
|
412
|
-
if (opts.cachedResources?.has(identifier)) {
|
|
413
|
-
const cached = resourceCache.get(identifier);
|
|
414
|
-
if (cached) return Object.assign({}, cached);
|
|
415
|
-
}
|
|
416
|
-
const dbResource = await ctx.context.adapter.findOne({
|
|
417
|
-
model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
|
|
418
|
-
where: [{
|
|
419
|
-
field: "identifier",
|
|
420
|
-
value: identifier
|
|
421
|
-
}]
|
|
422
|
-
});
|
|
423
|
-
if (dbResource && opts.cachedResources?.has(identifier)) resourceCache.set(identifier, Object.assign({}, dbResource));
|
|
424
|
-
return dbResource;
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Default `name` value when an input doesn't provide one. Mirrors what most
|
|
428
|
-
* admin UIs would show.
|
|
429
|
-
*/
|
|
430
|
-
function defaultName(input) {
|
|
431
|
-
return input.name ?? input.identifier;
|
|
432
|
-
}
|
|
433
|
-
/**
|
|
434
|
-
* Coerces seed-config `resources` entries — string or object form — into a
|
|
435
|
-
* normalized list of {@link OAuthResourceInput}.
|
|
436
|
-
*
|
|
437
|
-
* @internal
|
|
438
|
-
*/
|
|
439
|
-
function collectResourceInputs(opts) {
|
|
440
|
-
const inputs = [];
|
|
441
|
-
if (opts.resources?.length) for (const entry of opts.resources) inputs.push(typeof entry === "string" ? { identifier: entry } : entry);
|
|
442
|
-
return inputs;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Builds the row payload sent to the adapter for a seed insert. All policy
|
|
446
|
-
* columns default to `null` (= inherit plugin default at issuance time)
|
|
447
|
-
* when the input doesn't specify a value.
|
|
448
|
-
*/
|
|
449
|
-
function buildSeedRow(input, now) {
|
|
450
|
-
return {
|
|
451
|
-
identifier: input.identifier,
|
|
452
|
-
name: defaultName(input),
|
|
453
|
-
accessTokenTtl: input.accessTokenTtl ?? null,
|
|
454
|
-
refreshTokenTtl: input.refreshTokenTtl ?? null,
|
|
455
|
-
signingAlgorithm: input.signingAlgorithm ?? null,
|
|
456
|
-
signingKeyId: input.signingKeyId ?? null,
|
|
457
|
-
allowedScopes: input.allowedScopes ?? null,
|
|
458
|
-
customClaims: input.customClaims ?? null,
|
|
459
|
-
dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
|
|
460
|
-
disabled: input.disabled ?? false,
|
|
461
|
-
policyVersion: 1,
|
|
462
|
-
metadata: input.metadata ?? null,
|
|
463
|
-
createdAt: now,
|
|
464
|
-
updatedAt: now
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Builds the partial update payload when re-seeding an existing row.
|
|
469
|
-
*
|
|
470
|
-
* - `overwrite` mode replaces every policy column with the input value
|
|
471
|
-
* (omitted fields fall back to `null` to clear stale state).
|
|
472
|
-
* - `merge` mode updates only fields present in the input — admin edits to
|
|
473
|
-
* other fields are preserved.
|
|
474
|
-
*/
|
|
475
|
-
function buildSeedUpdate(input, mode, now) {
|
|
476
|
-
if (mode === "overwrite") {
|
|
477
|
-
const { identifier: _identifier, createdAt: _createdAt, ...rest } = buildSeedRow(input, now);
|
|
478
|
-
return rest;
|
|
479
|
-
}
|
|
480
|
-
const update = { updatedAt: now };
|
|
481
|
-
if (input.name !== void 0) update.name = input.name;
|
|
482
|
-
if (input.accessTokenTtl !== void 0) update.accessTokenTtl = input.accessTokenTtl;
|
|
483
|
-
if (input.refreshTokenTtl !== void 0) update.refreshTokenTtl = input.refreshTokenTtl;
|
|
484
|
-
if (input.signingAlgorithm !== void 0) update.signingAlgorithm = input.signingAlgorithm;
|
|
485
|
-
if (input.signingKeyId !== void 0) update.signingKeyId = input.signingKeyId;
|
|
486
|
-
if (input.allowedScopes !== void 0) update.allowedScopes = input.allowedScopes;
|
|
487
|
-
if (input.customClaims !== void 0) update.customClaims = input.customClaims;
|
|
488
|
-
if (input.dpopBoundAccessTokensRequired !== void 0) update.dpopBoundAccessTokensRequired = input.dpopBoundAccessTokensRequired;
|
|
489
|
-
if (input.disabled !== void 0) update.disabled = input.disabled;
|
|
490
|
-
if (input.metadata !== void 0) update.metadata = input.metadata;
|
|
491
|
-
return update;
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Pattern matched against adapter errors to detect the "table not yet
|
|
495
|
-
* created" case — i.e. migrations haven't been run. When matched, the
|
|
496
|
-
* caller treats the seed as deferred (will retry on first resource access).
|
|
497
|
-
*
|
|
498
|
-
* Covers SQLite ("no such table"), Postgres ("relation X does not exist"),
|
|
499
|
-
* and MySQL ("Table X does not exist" / contracted form).
|
|
500
|
-
*/
|
|
501
|
-
const MISSING_TABLE_PATTERN = /no such table|relation.*does not exist|table.*does(?: not|n[''']?t) exist/i;
|
|
502
|
-
/**
|
|
503
|
-
* Per-adapter state for the lazy-seed path. A module-level boolean would let
|
|
504
|
-
* one Better Auth instance suppress seeding for every later instance in the same
|
|
505
|
-
* process. Endpoint `AuthContext` objects are not guaranteed to be stable across
|
|
506
|
-
* requests, so keying by the adapter keeps one seed state per backing store.
|
|
507
|
-
*/
|
|
508
|
-
let seedStates = /* @__PURE__ */ new WeakMap();
|
|
509
|
-
function getSeedState(ctx) {
|
|
510
|
-
const key = ctx.adapter;
|
|
511
|
-
const existing = seedStates.get(key);
|
|
512
|
-
if (existing) return existing;
|
|
513
|
-
const created = {
|
|
514
|
-
completed: false,
|
|
515
|
-
promise: null
|
|
516
|
-
};
|
|
517
|
-
seedStates.set(key, created);
|
|
518
|
-
return created;
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Seeds `oauthResource` rows from plugin config.
|
|
522
|
-
*
|
|
523
|
-
* Behavior is controlled by {@link OAuthOptions.resourceSeedMode}:
|
|
524
|
-
*
|
|
525
|
-
* - `"insertOnly"` (default, safe): inserts rows whose `identifier` is not
|
|
526
|
-
* already present. Existing rows are untouched — admin edits via CRUD
|
|
527
|
-
* are never reverted on restart.
|
|
528
|
-
* - `"merge"`: inserts missing rows; updates only specified fields for
|
|
529
|
-
* existing rows. Useful when config holds the "preferred defaults" but
|
|
530
|
-
* admins customize per-row.
|
|
531
|
-
* - `"overwrite"`: inserts missing rows; replaces every policy column on
|
|
532
|
-
* existing rows with the config value (omitted fields → null). Use only
|
|
533
|
-
* when config is the single source of truth.
|
|
534
|
-
*
|
|
535
|
-
* Race-safety: the `identifier` column carries a UNIQUE constraint, so two
|
|
536
|
-
* processes booting simultaneously can each attempt the insert — one wins,
|
|
537
|
-
* the other catches the constraint error and treats it as a no-op.
|
|
538
|
-
*
|
|
539
|
-
* Migration ordering: at plugin `init` time, tables may not exist yet
|
|
540
|
-
* (Better Auth's test harness, and many deployment setups, run migrations
|
|
541
|
-
* after auth construction). Seeding tolerates "no such table" errors and
|
|
542
|
-
* defers — the lazy {@link seedResourcesOnce} path picks up the work on
|
|
543
|
-
* the first resource access.
|
|
544
|
-
*
|
|
545
|
-
* Idempotent: safe to call multiple times.
|
|
546
|
-
*
|
|
547
|
-
* @internal
|
|
548
|
-
*/
|
|
549
|
-
async function seedResources(ctx, opts) {
|
|
550
|
-
const inputs = collectResourceInputs(opts);
|
|
551
|
-
if (inputs.length === 0) return;
|
|
552
|
-
const mode = opts.resourceSeedMode ?? "insertOnly";
|
|
553
|
-
const modelName = opts.schema?.oauthResource?.modelName ?? "oauthResource";
|
|
554
|
-
for (const rawInput of inputs) {
|
|
555
|
-
const check = await checkIdentifier(opts, rawInput.identifier);
|
|
556
|
-
if (!check.ok) {
|
|
557
|
-
logger.warn(`oauth-provider: skipping resource seed for ${rawInput.identifier} — ${check.reason}`);
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
let input = rawInput;
|
|
561
|
-
if (input.signingAlgorithm != null && !JWS_ALGORITHM_SET.has(input.signingAlgorithm)) {
|
|
562
|
-
logger.warn(`oauth-provider: dropping unsupported signingAlgorithm "${input.signingAlgorithm}" for resource ${input.identifier} — must be one of ${JWS_ALGORITHMS.join(", ")}. Continuing without an algorithm override.`);
|
|
563
|
-
const { signingAlgorithm: _, ...rest } = input;
|
|
564
|
-
input = rest;
|
|
565
|
-
}
|
|
566
|
-
let existing;
|
|
567
|
-
try {
|
|
568
|
-
existing = await ctx.adapter.findOne({
|
|
569
|
-
model: modelName,
|
|
570
|
-
where: [{
|
|
571
|
-
field: "identifier",
|
|
572
|
-
value: input.identifier
|
|
573
|
-
}]
|
|
574
|
-
});
|
|
575
|
-
} catch (err) {
|
|
576
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
577
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
578
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
579
|
-
return;
|
|
580
|
-
}
|
|
581
|
-
throw err;
|
|
582
|
-
}
|
|
583
|
-
const now = /* @__PURE__ */ new Date();
|
|
584
|
-
if (!existing) {
|
|
585
|
-
try {
|
|
586
|
-
await ctx.adapter.create({
|
|
587
|
-
model: modelName,
|
|
588
|
-
data: buildSeedRow(input, now)
|
|
589
|
-
});
|
|
590
|
-
} catch (err) {
|
|
591
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
592
|
-
if (/unique|duplicate|UNIQUE/i.test(message)) {
|
|
593
|
-
logger.debug(`oauth-provider: resource ${input.identifier} already inserted by a concurrent process — skipping.`);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
if (MISSING_TABLE_PATTERN.test(message)) {
|
|
597
|
-
logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
throw err;
|
|
601
|
-
}
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
if (mode === "insertOnly") continue;
|
|
605
|
-
await ctx.adapter.update({
|
|
606
|
-
model: modelName,
|
|
607
|
-
where: [{
|
|
608
|
-
field: "identifier",
|
|
609
|
-
value: input.identifier
|
|
610
|
-
}],
|
|
611
|
-
update: buildSeedUpdate(input, mode, now)
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Idempotent, coalesced wrapper around {@link seedResources} for the
|
|
617
|
-
* lazy-seed path. Safe to call from every resource lookup — the first call
|
|
618
|
-
* runs the seed, concurrent calls await the same promise, and subsequent
|
|
619
|
-
* calls become no-ops.
|
|
620
|
-
*
|
|
621
|
-
* The endpoint context shape (`{ context: AuthContext }`) is what
|
|
622
|
-
* `getResource` receives, so this is a convenience overload that unwraps
|
|
623
|
-
* for callers.
|
|
624
|
-
*
|
|
625
|
-
* @internal
|
|
626
|
-
*/
|
|
627
|
-
async function seedResourcesOnce(ctx, opts) {
|
|
628
|
-
const state = getSeedState(ctx);
|
|
629
|
-
if (state.completed === true) return;
|
|
630
|
-
if (state.promise !== null) return state.promise;
|
|
631
|
-
state.promise = seedResources(ctx, opts).then(() => {
|
|
632
|
-
state.completed = true;
|
|
633
|
-
}).catch((err) => {
|
|
634
|
-
state.promise = null;
|
|
635
|
-
throw err;
|
|
636
|
-
});
|
|
637
|
-
return state.promise;
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Logs the resolved value of {@link OAuthOptions.enforcePerClientResources}
|
|
641
|
-
* so deployment admins see which default applied at init. Separated from
|
|
642
|
-
* {@link resolveEnforcePerClientResources} so the latter stays pure and
|
|
643
|
-
* cheap to call from validation flow.
|
|
644
|
-
*
|
|
645
|
-
* @internal
|
|
646
|
-
*/
|
|
647
|
-
function logEnforcePerClientResourcesResolution(opts) {
|
|
648
|
-
const resolved = resolveEnforcePerClientResources(opts);
|
|
649
|
-
logger.info(`oauth-provider: enforcePerClientResources resolved to ${resolved.value} (${resolved.source})`);
|
|
650
|
-
}
|
|
651
|
-
//#endregion
|
|
652
|
-
//#region src/oauthClient/client-credentials.ts
|
|
653
|
-
const USER_DELEGATED_SCOPES = /* @__PURE__ */ new Set([
|
|
654
|
-
"openid",
|
|
655
|
-
"profile",
|
|
656
|
-
"email",
|
|
657
|
-
"offline_access"
|
|
658
|
-
]);
|
|
659
|
-
function isUserDelegatedScope(scope) {
|
|
660
|
-
return USER_DELEGATED_SCOPES.has(scope);
|
|
661
|
-
}
|
|
662
|
-
function normalizeClientCredentialsScopes(scopes) {
|
|
663
|
-
return [...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))];
|
|
664
|
-
}
|
|
665
|
-
function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMethod, opts) {
|
|
666
|
-
if (scopes.length === 0) return;
|
|
667
|
-
if (!grantTypes.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
668
|
-
error: "invalid_client_metadata",
|
|
669
|
-
error_description: "client_credentials_scopes requires the client_credentials grant"
|
|
670
|
-
});
|
|
671
|
-
if (tokenEndpointAuthMethod === "none") throw new APIError("BAD_REQUEST", {
|
|
672
|
-
error: "invalid_client_metadata",
|
|
673
|
-
error_description: "public clients cannot be assigned client_credentials scopes"
|
|
674
|
-
});
|
|
675
|
-
const providerScopes = new Set(opts.scopes ?? []);
|
|
676
|
-
const invalidScopes = scopes.filter((scope) => !providerScopes.has(scope) || isUserDelegatedScope(scope));
|
|
677
|
-
if (invalidScopes.length > 0) throw new APIError("BAD_REQUEST", {
|
|
678
|
-
error: "invalid_scope",
|
|
679
|
-
error_description: `The following client_credentials scopes are invalid: ${invalidScopes.join(", ")}`
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
//#endregion
|
|
683
|
-
//#region src/client-metadata.ts
|
|
684
|
-
const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
|
|
685
|
-
clientId: true,
|
|
686
|
-
clientSecret: true,
|
|
687
|
-
clientDiscoveryId: true,
|
|
688
|
-
disabled: true,
|
|
689
|
-
scopes: true,
|
|
690
|
-
clientCredentialsScopes: true,
|
|
691
|
-
userId: true,
|
|
692
|
-
createdAt: true,
|
|
693
|
-
updatedAt: true,
|
|
694
|
-
expiresAt: true,
|
|
695
|
-
name: true,
|
|
696
|
-
uri: true,
|
|
697
|
-
icon: true,
|
|
698
|
-
contacts: true,
|
|
699
|
-
tos: true,
|
|
700
|
-
policy: true,
|
|
701
|
-
softwareId: true,
|
|
702
|
-
softwareVersion: true,
|
|
703
|
-
softwareStatement: true,
|
|
704
|
-
redirectUris: true,
|
|
705
|
-
postLogoutRedirectUris: true,
|
|
706
|
-
backchannelLogoutUri: true,
|
|
707
|
-
backchannelLogoutSessionRequired: true,
|
|
708
|
-
tokenEndpointAuthMethod: true,
|
|
709
|
-
grantTypes: true,
|
|
710
|
-
responseTypes: true,
|
|
711
|
-
applicationType: true,
|
|
712
|
-
jwks: true,
|
|
713
|
-
jwksUri: true,
|
|
714
|
-
requirePKCE: true,
|
|
715
|
-
dpopBoundAccessTokens: true,
|
|
716
|
-
skipConsent: true,
|
|
717
|
-
enableEndSession: true,
|
|
718
|
-
subjectType: true,
|
|
719
|
-
referenceId: true,
|
|
720
|
-
metadata: true
|
|
721
|
-
}));
|
|
722
|
-
/**
|
|
723
|
-
* Canonical wire fields emitted by schemaToOAuth. They are mapped explicitly
|
|
724
|
-
* and therefore must never also survive inside the opaque metadata envelope.
|
|
725
|
-
*/
|
|
726
|
-
const OAUTH_CLIENT_WIRE_FIELD_NAMES = [
|
|
727
|
-
"client_id",
|
|
728
|
-
"client_secret",
|
|
729
|
-
"client_secret_expires_at",
|
|
730
|
-
"scope",
|
|
731
|
-
"user_id",
|
|
732
|
-
"client_id_issued_at",
|
|
733
|
-
"client_name",
|
|
734
|
-
"client_uri",
|
|
735
|
-
"logo_uri",
|
|
736
|
-
"contacts",
|
|
737
|
-
"tos_uri",
|
|
738
|
-
"policy_uri",
|
|
739
|
-
"jwks",
|
|
740
|
-
"jwks_uri",
|
|
741
|
-
"software_id",
|
|
742
|
-
"software_version",
|
|
743
|
-
"software_statement",
|
|
744
|
-
"redirect_uris",
|
|
745
|
-
"post_logout_redirect_uris",
|
|
746
|
-
"backchannel_logout_uri",
|
|
747
|
-
"backchannel_logout_session_required",
|
|
748
|
-
"token_endpoint_auth_method",
|
|
749
|
-
"grant_types",
|
|
750
|
-
"response_types",
|
|
751
|
-
"application_type",
|
|
752
|
-
"disabled",
|
|
753
|
-
"skip_consent",
|
|
754
|
-
"enable_end_session",
|
|
755
|
-
"require_pkce",
|
|
756
|
-
"dpop_bound_access_tokens",
|
|
757
|
-
"subject_type",
|
|
758
|
-
"reference_id"
|
|
759
|
-
];
|
|
760
|
-
const OPAQUE_METADATA_RESERVED_FIELDS = /* @__PURE__ */ new Set([
|
|
761
|
-
...OAUTH_CLIENT_RECORD_FIELD_NAMES,
|
|
762
|
-
...OAUTH_CLIENT_WIRE_FIELD_NAMES,
|
|
763
|
-
"public",
|
|
764
|
-
"type",
|
|
765
|
-
"resources",
|
|
766
|
-
"client_credentials_scopes"
|
|
767
|
-
]);
|
|
768
|
-
const CIMD_FORBIDDEN_SERVER_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
769
|
-
"disabled",
|
|
770
|
-
"client_secret",
|
|
771
|
-
"client_secret_expires_at",
|
|
772
|
-
"client_id_issued_at",
|
|
773
|
-
"skip_consent",
|
|
774
|
-
"enable_end_session",
|
|
775
|
-
"require_pkce",
|
|
776
|
-
"reference_id",
|
|
777
|
-
"user_id",
|
|
778
|
-
"resources",
|
|
779
|
-
"clientSecret",
|
|
780
|
-
"clientDiscoveryId",
|
|
781
|
-
"skipConsent",
|
|
782
|
-
"enableEndSession",
|
|
783
|
-
"requirePKCE",
|
|
784
|
-
"referenceId",
|
|
785
|
-
"userId",
|
|
786
|
-
"clientId",
|
|
787
|
-
"applicationType",
|
|
788
|
-
"tokenEndpointAuthMethod",
|
|
789
|
-
"redirectUris",
|
|
790
|
-
"postLogoutRedirectUris",
|
|
791
|
-
"grantTypes",
|
|
792
|
-
"responseTypes",
|
|
793
|
-
"scopes",
|
|
794
|
-
"expiresAt",
|
|
795
|
-
"createdAt",
|
|
796
|
-
"updatedAt",
|
|
797
|
-
"softwareId",
|
|
798
|
-
"softwareVersion",
|
|
799
|
-
"softwareStatement",
|
|
800
|
-
"backchannelLogoutUri",
|
|
801
|
-
"backchannelLogoutSessionRequired",
|
|
802
|
-
"jwksUri",
|
|
803
|
-
"dpopBoundAccessTokens",
|
|
804
|
-
"subjectType"
|
|
805
|
-
]);
|
|
806
|
-
function isForbiddenCimdClientMetadataField(field) {
|
|
807
|
-
return CIMD_FORBIDDEN_SERVER_FIELD_NAMES.has(field);
|
|
808
|
-
}
|
|
809
|
-
function stripReservedOAuthClientMetadataExtensions(metadata) {
|
|
810
|
-
if (!metadata) return void 0;
|
|
811
|
-
return Object.fromEntries(Object.entries(metadata).filter(([field]) => !OPAQUE_METADATA_RESERVED_FIELDS.has(field)));
|
|
812
|
-
}
|
|
813
|
-
//#endregion
|
|
814
|
-
//#region src/oauthClient/privileges.ts
|
|
815
|
-
/**
|
|
816
|
-
* Authorizes a client action against the configured `clientPrivileges` hook.
|
|
817
|
-
*
|
|
818
|
-
* This is the single authorization helper for every OAuth client mutation. The
|
|
819
|
-
* create path enforces it at the shared creation chokepoint so that no
|
|
820
|
-
* registration route can reach client persistence without it.
|
|
821
|
-
*
|
|
822
|
-
* @throws APIError UNAUTHORIZED when there is no session or the hook denies the action.
|
|
823
|
-
* @throws APIError BAD_REQUEST when the request carries no headers.
|
|
824
|
-
*/
|
|
825
|
-
async function assertClientPrivileges(ctx, session, opts, action) {
|
|
826
|
-
if (!session) throw new APIError("UNAUTHORIZED");
|
|
827
|
-
if (!ctx.headers) throw new APIError("BAD_REQUEST");
|
|
828
|
-
if (action === "configure-client-credentials-scopes" && !opts.clientPrivileges) throw new APIError("UNAUTHORIZED");
|
|
829
|
-
if (opts.clientPrivileges && !await opts.clientPrivileges({
|
|
830
|
-
headers: ctx.headers,
|
|
831
|
-
action,
|
|
832
|
-
session: session.session,
|
|
833
|
-
user: session.user
|
|
834
|
-
})) throw new APIError("UNAUTHORIZED");
|
|
835
|
-
}
|
|
836
|
-
//#endregion
|
|
837
|
-
//#region src/utils/initial-access-token.ts
|
|
838
|
-
/**
|
|
839
|
-
* Builds an RFC 6750 §3 Bearer challenge for the registration endpoint. The
|
|
840
|
-
* `error` code maps to the status per RFC 6750 §3.1 (`invalid_request` → 400,
|
|
841
|
-
* `invalid_token` → 401) and is echoed in both the `WWW-Authenticate` challenge
|
|
842
|
-
* and the JSON body.
|
|
843
|
-
*
|
|
844
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
845
|
-
*/
|
|
846
|
-
function registrationBearerError(status, error, errorDescription) {
|
|
847
|
-
return new APIError$1(status, {
|
|
848
|
-
error,
|
|
849
|
-
error_description: errorDescription
|
|
850
|
-
}, {
|
|
851
|
-
"WWW-Authenticate": `Bearer error="${error}"`,
|
|
852
|
-
...NO_STORE_HEADERS
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Resolves an RFC 7591 initial access token from the request and authorizes it
|
|
857
|
-
* against the deployment-supplied validator.
|
|
858
|
-
*
|
|
859
|
-
* Returns the authorization (optionally carrying a `referenceId`) when a valid
|
|
860
|
-
* token is present, or `undefined` when no Bearer credentials were sent so the
|
|
861
|
-
* caller can fall back to session or open registration. Throws an RFC 6750
|
|
862
|
-
* Bearer challenge when the header is malformed, no validator is configured, or
|
|
863
|
-
* the token is rejected.
|
|
864
|
-
*
|
|
865
|
-
* @see https://datatracker.ietf.org/doc/html/rfc7591#section-3
|
|
866
|
-
*/
|
|
867
|
-
async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
|
|
868
|
-
const headers = ctx.headers;
|
|
869
|
-
let initialAccessToken;
|
|
870
|
-
try {
|
|
871
|
-
initialAccessToken = parseBearerToken(headers?.get("authorization"));
|
|
872
|
-
} catch {
|
|
873
|
-
throw registrationBearerError("BAD_REQUEST", "invalid_request", "Malformed initial access token Authorization header");
|
|
874
|
-
}
|
|
875
|
-
if (!initialAccessToken || !headers) return;
|
|
876
|
-
if (!opts.validateInitialAccessToken) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
|
|
877
|
-
let authorization;
|
|
878
|
-
try {
|
|
879
|
-
authorization = await opts.validateInitialAccessToken({
|
|
880
|
-
initialAccessToken,
|
|
881
|
-
headers,
|
|
882
|
-
clientMetadata
|
|
883
|
-
});
|
|
884
|
-
} catch (error) {
|
|
885
|
-
if (error instanceof APIError$1) throw error;
|
|
886
|
-
throw new APIError$1("INTERNAL_SERVER_ERROR", {
|
|
887
|
-
error: "server_error",
|
|
888
|
-
error_description: "Initial access token validation failed"
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
if (!authorization) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
|
|
892
|
-
return authorization;
|
|
893
|
-
}
|
|
894
|
-
//#endregion
|
|
895
|
-
//#region src/register.ts
|
|
896
|
-
const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
|
|
897
|
-
function resolveClientRegistrationScopes(opts) {
|
|
898
|
-
return [.../* @__PURE__ */ new Set([...opts.clientRegistrationDefaultScopes ?? opts.scopes ?? [], ...opts.clientRegistrationAllowedScopes ?? []])];
|
|
899
|
-
}
|
|
900
|
-
function resolveRegistrationGrantTypes(client) {
|
|
901
|
-
const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
|
|
902
|
-
if (grantTypes.length > 0) return grantTypes;
|
|
903
|
-
throw new APIError("BAD_REQUEST", {
|
|
904
|
-
error: "invalid_client_metadata",
|
|
905
|
-
error_description: "grant_types must contain at least one grant type"
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
function resolveRegistrationResponseTypes(client, grantTypes) {
|
|
909
|
-
if (client.response_types) return client.response_types;
|
|
910
|
-
return grantTypes.includes("authorization_code") ? ["code"] : void 0;
|
|
911
|
-
}
|
|
912
|
-
function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") {
|
|
913
|
-
const grantTypes = resolveRegistrationGrantTypes(client);
|
|
914
|
-
return {
|
|
915
|
-
...client,
|
|
916
|
-
token_endpoint_auth_method: client.token_endpoint_auth_method ?? "client_secret_basic",
|
|
917
|
-
application_type: client.application_type ?? (defaultApplicationType === null ? void 0 : defaultApplicationType),
|
|
918
|
-
grant_types: grantTypes,
|
|
919
|
-
response_types: resolveRegistrationResponseTypes(client, grantTypes)
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
const FORBIDDEN_NATIVE_REDIRECT_SCHEMES = /* @__PURE__ */ new Set([
|
|
923
|
-
"file:",
|
|
924
|
-
"ftp:",
|
|
925
|
-
"mailto:",
|
|
926
|
-
"javascript:",
|
|
927
|
-
"data:",
|
|
928
|
-
"vbscript:"
|
|
929
|
-
]);
|
|
930
|
-
function invalidRedirectUri(description) {
|
|
931
|
-
throw new APIError("BAD_REQUEST", {
|
|
932
|
-
error: "invalid_redirect_uri",
|
|
933
|
-
error_description: description
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
function getRawHttpHostname(redirectUri) {
|
|
937
|
-
const authority = /^http:\/\/([^/?#]*)/i.exec(redirectUri)?.[1];
|
|
938
|
-
if (!authority) return null;
|
|
939
|
-
const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
940
|
-
if (hostAndPort.startsWith("[")) {
|
|
941
|
-
const bracketEnd = hostAndPort.indexOf("]");
|
|
942
|
-
return bracketEnd < 0 ? null : hostAndPort.slice(0, bracketEnd + 1).toLowerCase();
|
|
943
|
-
}
|
|
944
|
-
return (hostAndPort.split(":")[0] ?? "").toLowerCase();
|
|
945
|
-
}
|
|
946
|
-
async function resolveClientRegistrationResources(ctx, opts, requestedResources) {
|
|
947
|
-
const defaultResources = opts.clientRegistrationDefaultResources ?? [];
|
|
948
|
-
const allowedResources = /* @__PURE__ */ new Set([...defaultResources, ...opts.clientRegistrationAllowedResources ?? []]);
|
|
949
|
-
for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
|
|
950
|
-
error: "invalid_target",
|
|
951
|
-
error_description: `requested resource ${identifier} is not allowed for client registration`
|
|
952
|
-
});
|
|
953
|
-
const resources = [.../* @__PURE__ */ new Set([...defaultResources, ...requestedResources])];
|
|
954
|
-
for (const identifier of resources) {
|
|
955
|
-
const row = await getResource(ctx, opts, identifier);
|
|
956
|
-
if (!row) throw new APIError("BAD_REQUEST", {
|
|
957
|
-
error: "invalid_target",
|
|
958
|
-
error_description: `requested resource ${identifier} does not exist`
|
|
959
|
-
});
|
|
960
|
-
if (row.disabled) throw new APIError("BAD_REQUEST", {
|
|
961
|
-
error: "invalid_target",
|
|
962
|
-
error_description: `requested resource ${identifier} is disabled`
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
return resources;
|
|
966
|
-
}
|
|
967
|
-
function validateClientRedirectUri(redirectUri, applicationType) {
|
|
968
|
-
let url;
|
|
969
|
-
try {
|
|
970
|
-
url = new URL(redirectUri);
|
|
971
|
-
} catch {
|
|
972
|
-
invalidRedirectUri(`redirect URI must be an absolute URI: ${redirectUri}`);
|
|
973
|
-
}
|
|
974
|
-
if (redirectUri.includes("#") || url.username.length > 0 || url.password.length > 0) invalidRedirectUri(`redirect URI must not include credentials or a fragment: ${redirectUri}`);
|
|
975
|
-
const isHttp = url.protocol === "http:";
|
|
976
|
-
const isHttps = url.protocol === "https:";
|
|
977
|
-
if (/^localhost\.+$/i.test(url.hostname)) invalidRedirectUri(`redirect URI localhost must not include trailing dots: ${redirectUri}`);
|
|
978
|
-
const isRedirectLoopback = isLoopbackIP(url.hostname) || url.hostname === "localhost";
|
|
979
|
-
const rawHttpHostname = getRawHttpHostname(redirectUri);
|
|
980
|
-
const isAllowedNativeHttpLoopback = rawHttpHostname === "localhost" || rawHttpHostname === "127.0.0.1" || rawHttpHostname === "[::1]";
|
|
981
|
-
if (applicationType === "web") {
|
|
982
|
-
if (!isHttps || isRedirectLoopback) invalidRedirectUri(`web clients require https redirect URIs on non-loopback hosts: ${redirectUri}`);
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if (isHttps) {
|
|
986
|
-
if (isRedirectLoopback) invalidRedirectUri(`native clients must not use https loopback redirect URIs: ${redirectUri}`);
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
if (isHttp) {
|
|
990
|
-
if (!isAllowedNativeHttpLoopback) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
if (FORBIDDEN_NATIVE_REDIRECT_SCHEMES.has(url.protocol) || !isReverseDomainPrivateUseRedirectUri(url)) invalidRedirectUri(`native private-use redirect URI schemes must be well-formed reverse-domain names, omit the naming authority, and must not use a reserved scheme: ${redirectUri}`);
|
|
994
|
-
}
|
|
995
|
-
function assertValidRegistrationJwks(jwks) {
|
|
996
|
-
const result = validatePublicClientJwks(jwks);
|
|
997
|
-
if (!result.valid) throw new APIError("BAD_REQUEST", {
|
|
998
|
-
error: "invalid_client_metadata",
|
|
999
|
-
error_description: result.error
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
async function registerEndpoint(ctx, opts) {
|
|
1003
|
-
const body = ctx.body;
|
|
1004
|
-
if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
|
|
1005
|
-
error: "access_denied",
|
|
1006
|
-
error_description: "Client registration is disabled"
|
|
1007
|
-
});
|
|
1008
|
-
const session = await getSessionFromCtx(ctx);
|
|
1009
|
-
const tokenAuthorization = session ? void 0 : await authorizeInitialAccessToken(ctx, opts, body);
|
|
1010
|
-
const isTokenAuthorized = Boolean(tokenAuthorization);
|
|
1011
|
-
if (!(session || isTokenAuthorized || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", { error_description: "Authentication required for client registration" }, {
|
|
1012
|
-
"WWW-Authenticate": "Bearer",
|
|
1013
|
-
...NO_STORE_HEADERS
|
|
1014
|
-
});
|
|
1015
|
-
if (!session && !isTokenAuthorized) {
|
|
1016
|
-
if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
|
|
1017
|
-
error: "invalid_client_metadata",
|
|
1018
|
-
error_description: "client_credentials grant requires authenticated registration"
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
const requestedResources = Array.isArray(body.resources) ? body.resources.filter((resource) => typeof resource === "string" && resource.length > 0) : [];
|
|
1022
|
-
if (session) await assertClientPrivileges(ctx, session, opts, "create");
|
|
1023
|
-
const referenceId = tokenAuthorization?.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
|
|
1024
|
-
user: session.user,
|
|
1025
|
-
session: session.session
|
|
1026
|
-
}) : void 0);
|
|
1027
|
-
const response = await createOAuthClientRegistration(ctx, opts, {
|
|
1028
|
-
metadata: body,
|
|
1029
|
-
registrationSource: "dynamic",
|
|
1030
|
-
userId: referenceId ? void 0 : session?.session.userId,
|
|
1031
|
-
referenceId,
|
|
1032
|
-
requestedResources
|
|
1033
|
-
});
|
|
1034
|
-
ctx.setStatus(201);
|
|
1035
|
-
return ctx.json(response);
|
|
1036
|
-
}
|
|
1037
|
-
async function checkOAuthClient(client, opts, settings) {
|
|
1038
|
-
const isClientMetadataDocument = settings?.registrationSource === "clientMetadataDocument";
|
|
1039
|
-
const clientWithDefaults = applyOAuthClientRegistrationDefaults(client, isClientMetadataDocument ? null : "web");
|
|
1040
|
-
const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
|
|
1041
|
-
if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
|
|
1042
|
-
error: "invalid_client_metadata",
|
|
1043
|
-
error_description: `unsupported token_endpoint_auth_method ${tokenEndpointAuthMethod}`
|
|
1044
|
-
});
|
|
1045
|
-
if (clientWithDefaults.dpop_bound_access_tokens !== void 0 && typeof clientWithDefaults.dpop_bound_access_tokens !== "boolean") throw new APIError("BAD_REQUEST", {
|
|
1046
|
-
error: "invalid_client_metadata",
|
|
1047
|
-
error_description: "dpop_bound_access_tokens must be a boolean"
|
|
1048
|
-
});
|
|
1049
|
-
const grantTypes = clientWithDefaults.grant_types ?? [];
|
|
1050
|
-
const responseTypes = clientWithDefaults.response_types;
|
|
1051
|
-
const applicationType = clientWithDefaults.application_type;
|
|
1052
|
-
if (applicationType !== void 0 && applicationType !== "web" && applicationType !== "native") throw new APIError("BAD_REQUEST", {
|
|
1053
|
-
error: "invalid_client_metadata",
|
|
1054
|
-
error_description: "application_type must be web or native"
|
|
1055
|
-
});
|
|
1056
|
-
if (grantTypes.includes("authorization_code") && (!clientWithDefaults.redirect_uris || clientWithDefaults.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
|
|
1057
|
-
error: "invalid_redirect_uri",
|
|
1058
|
-
error_description: "Redirect URIs are required for authorization_code and implicit grant types"
|
|
1059
|
-
});
|
|
1060
|
-
for (const uri of clientWithDefaults.redirect_uris ?? []) validateClientRedirectUri(uri, applicationType ?? (isClientMetadataDocument ? "native" : "web"));
|
|
1061
|
-
const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
|
|
1062
|
-
for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
|
|
1063
|
-
error: "invalid_client_metadata",
|
|
1064
|
-
error_description: `unsupported grant_type ${grantType}`
|
|
1065
|
-
});
|
|
1066
|
-
if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
1067
|
-
error: "invalid_client_metadata",
|
|
1068
|
-
error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
|
|
1069
|
-
});
|
|
1070
|
-
if (!grantTypes.includes("authorization_code") && responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
|
|
1071
|
-
error: "invalid_client_metadata",
|
|
1072
|
-
error_description: "When 'code' response type is used, 'authorization_code' grant type must be included"
|
|
1073
|
-
});
|
|
1074
|
-
if (clientWithDefaults.subject_type !== void 0) {
|
|
1075
|
-
if (clientWithDefaults.subject_type !== "public" && clientWithDefaults.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
|
|
1076
|
-
error: "invalid_client_metadata",
|
|
1077
|
-
error_description: `subject_type must be "public" or "pairwise"`
|
|
1078
|
-
});
|
|
1079
|
-
if (clientWithDefaults.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
|
|
1080
|
-
error: "invalid_client_metadata",
|
|
1081
|
-
error_description: "pairwise subject_type requires server pairwiseSecret configuration"
|
|
1082
|
-
});
|
|
1083
|
-
if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
|
|
1084
|
-
if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
|
|
1085
|
-
error: "invalid_client_metadata",
|
|
1086
|
-
error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
|
|
1087
|
-
});
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
const requestedScopes = (clientWithDefaults?.scope)?.split(" ").filter((v) => v.length);
|
|
1091
|
-
const validatesRegistrationMetadata = settings?.registrationSource === "dynamic" || settings?.registrationSource === "clientMetadataDocument";
|
|
1092
|
-
const allowedScopes = validatesRegistrationMetadata ? resolveClientRegistrationScopes(opts) : opts.scopes;
|
|
1093
|
-
if (allowedScopes) {
|
|
1094
|
-
const validScopes = new Set(allowedScopes);
|
|
1095
|
-
for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError("BAD_REQUEST", {
|
|
1096
|
-
error: "invalid_scope",
|
|
1097
|
-
error_description: `cannot request scope ${requestedScope}`
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
if (validatesRegistrationMetadata && clientWithDefaults.require_pkce === false) throw new APIError("BAD_REQUEST", {
|
|
1101
|
-
error: "invalid_client_metadata",
|
|
1102
|
-
error_description: `pkce is required for registered clients.`
|
|
1103
|
-
});
|
|
1104
|
-
if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
|
|
1105
|
-
if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1106
|
-
error: "invalid_client_metadata",
|
|
1107
|
-
error_description: "jwks and jwks_uri are mutually exclusive"
|
|
1108
|
-
});
|
|
1109
|
-
if (clientWithDefaults.jwks_uri) try {
|
|
1110
|
-
const uri = new URL(clientWithDefaults.jwks_uri);
|
|
1111
|
-
if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
|
|
1112
|
-
error: "invalid_client_metadata",
|
|
1113
|
-
error_description: "jwks_uri must use HTTPS"
|
|
1114
|
-
});
|
|
1115
|
-
if (uri.username || uri.password) throw new APIError("BAD_REQUEST", {
|
|
1116
|
-
error: "invalid_client_metadata",
|
|
1117
|
-
error_description: "jwks_uri must not contain credentials"
|
|
1118
|
-
});
|
|
1119
|
-
if (clientWithDefaults.jwks_uri.includes("#")) throw new APIError("BAD_REQUEST", {
|
|
1120
|
-
error: "invalid_client_metadata",
|
|
1121
|
-
error_description: "jwks_uri must not include a fragment component"
|
|
1122
|
-
});
|
|
1123
|
-
if (isPrivateHostname(uri.hostname)) throw new APIError("BAD_REQUEST", {
|
|
1124
|
-
error: "invalid_client_metadata",
|
|
1125
|
-
error_description: "jwks_uri must not point to a private or reserved address"
|
|
1126
|
-
});
|
|
1127
|
-
if (settings?.ctx && !settings.ctx.context.isTrustedOrigin(uri.href)) {
|
|
1128
|
-
const clientId = typeof clientWithDefaults.client_id === "string" ? clientWithDefaults.client_id : void 0;
|
|
1129
|
-
if (!(isClientMetadataDocument && clientId !== void 0 && URL.canParse(clientId) && new URL(clientId).origin === uri.origin)) throw new APIError("BAD_REQUEST", {
|
|
1130
|
-
error: "invalid_client_metadata",
|
|
1131
|
-
error_description: "jwks_uri must belong to a trusted origin or the Client ID Metadata Document origin"
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
} catch (e) {
|
|
1135
|
-
if (e instanceof APIError) throw e;
|
|
1136
|
-
throw new APIError("BAD_REQUEST", {
|
|
1137
|
-
error: "invalid_client_metadata",
|
|
1138
|
-
error_description: "jwks_uri must be a valid URL"
|
|
1139
|
-
});
|
|
1140
|
-
}
|
|
1141
|
-
if (clientWithDefaults.jwks) assertValidRegistrationJwks(clientWithDefaults.jwks);
|
|
1142
|
-
}
|
|
1143
|
-
if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
|
|
1144
|
-
error: "invalid_client_metadata",
|
|
1145
|
-
error_description: "private_key_jwt requires either jwks or jwks_uri"
|
|
1146
|
-
});
|
|
1147
|
-
if (clientWithDefaults.backchannel_logout_uri !== void 0) {
|
|
1148
|
-
if (opts.disableJwtPlugin) throw new APIError("BAD_REQUEST", {
|
|
1149
|
-
error: "invalid_client_metadata",
|
|
1150
|
-
error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
|
|
1151
|
-
});
|
|
1152
|
-
let url;
|
|
1153
|
-
try {
|
|
1154
|
-
url = new URL(clientWithDefaults.backchannel_logout_uri);
|
|
1155
|
-
} catch {
|
|
1156
|
-
throw new APIError("BAD_REQUEST", {
|
|
1157
|
-
error: "invalid_client_metadata",
|
|
1158
|
-
error_description: "backchannel_logout_uri must be an absolute URL"
|
|
1159
|
-
});
|
|
1160
|
-
}
|
|
1161
|
-
if (clientWithDefaults.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
|
|
1162
|
-
error: "invalid_client_metadata",
|
|
1163
|
-
error_description: "backchannel_logout_uri must not include a fragment component"
|
|
1164
|
-
});
|
|
1165
|
-
if (url.protocol !== "https:") throw new APIError("BAD_REQUEST", {
|
|
1166
|
-
error: "invalid_client_metadata",
|
|
1167
|
-
error_description: "backchannel_logout_uri must use https"
|
|
1168
|
-
});
|
|
1169
|
-
if (url.username || url.password) throw new APIError("BAD_REQUEST", {
|
|
1170
|
-
error: "invalid_client_metadata",
|
|
1171
|
-
error_description: "backchannel_logout_uri must not contain credentials"
|
|
1172
|
-
});
|
|
1173
|
-
if (isPrivateHostname(url.hostname)) throw new APIError("BAD_REQUEST", {
|
|
1174
|
-
error: "invalid_client_metadata",
|
|
1175
|
-
error_description: "backchannel_logout_uri must not point to a private or reserved address"
|
|
1176
|
-
});
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
const CLIENT_REGISTRATION_COLLISION = Symbol("client-registration-collision");
|
|
1180
|
-
function isClientRegistrationCollision(error) {
|
|
1181
|
-
if (!error || typeof error !== "object") return false;
|
|
1182
|
-
const candidate = error;
|
|
1183
|
-
return candidate[CLIENT_REGISTRATION_COLLISION] === true && (candidate.kind === "oauth-client-row-unique" || candidate.kind === "oauth-client-resource-link-unique") && "cause" in candidate;
|
|
1184
|
-
}
|
|
1185
|
-
/**
|
|
1186
|
-
* Creates and persists one canonical OAuth client registration.
|
|
1187
|
-
*
|
|
1188
|
-
* Authorization and ownership resolution belong to endpoint adapters. This
|
|
1189
|
-
* operation owns metadata normalization, validation, credentials, persistence,
|
|
1190
|
-
* and resource links so canonical creation paths share one transactional
|
|
1191
|
-
* boundary.
|
|
1192
|
-
*/
|
|
1193
|
-
async function persistOAuthClientRegistration(ctx, opts, input) {
|
|
1194
|
-
const body = applyOAuthClientRegistrationDefaults(input.registrationSource === "dynamic" && !input.metadata.scope ? {
|
|
1195
|
-
...input.metadata,
|
|
1196
|
-
scope: (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ")
|
|
1197
|
-
} : input.metadata, input.registrationSource === "clientMetadataDocument" ? null : "web");
|
|
1198
|
-
const isPublic = body.token_endpoint_auth_method === "none";
|
|
1199
|
-
const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
|
|
1200
|
-
const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
|
|
1201
|
-
await checkOAuthClient(body, opts, {
|
|
1202
|
-
registrationSource: input.registrationSource,
|
|
1203
|
-
ctx
|
|
1204
|
-
});
|
|
1205
|
-
const clientId = input.clientId ?? opts.generateClientId?.() ?? generateRandomString(32, "a-z", "A-Z");
|
|
1206
|
-
const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
|
|
1207
|
-
const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
|
|
1208
|
-
const isPKCEOptionalForRegisteredClient = input.registrationSource === "dynamic" && !isPublic && opts.clientRegistrationRequirePKCE === false;
|
|
1209
|
-
const requirePKCE = body.require_pkce ?? (isPKCEOptionalForRegisteredClient ? false : void 0);
|
|
1210
|
-
const iat = Math.floor(Date.now() / 1e3);
|
|
1211
|
-
const { resources: _requestedResources, ...persistableBody } = input.registrationSource === "dynamic" || input.registrationSource === "clientMetadataDocument" ? {
|
|
1212
|
-
...body,
|
|
1213
|
-
scope: resolveClientRegistrationScopes(opts).join(" ")
|
|
1214
|
-
} : body;
|
|
1215
|
-
const owner = input.registrationSource === "clientMetadataDocument" ? void 0 : {
|
|
1216
|
-
userId: input.userId,
|
|
1217
|
-
referenceId: input.referenceId
|
|
1218
|
-
};
|
|
1219
|
-
const schema = oauthToSchema({
|
|
1220
|
-
...persistableBody,
|
|
1221
|
-
redirect_uris: body.redirect_uris ?? [],
|
|
1222
|
-
disabled: void 0,
|
|
1223
|
-
client_secret_expires_at: storedClientSecret ? input.registrationSource === "dynamic" && opts.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
|
|
1224
|
-
client_id: clientId,
|
|
1225
|
-
client_secret: storedClientSecret,
|
|
1226
|
-
client_id_issued_at: iat,
|
|
1227
|
-
require_pkce: requirePKCE,
|
|
1228
|
-
user_id: owner?.referenceId ? void 0 : owner?.userId,
|
|
1229
|
-
reference_id: owner?.referenceId
|
|
1230
|
-
});
|
|
1231
|
-
schema.clientCredentialsScopes = input.registrationSource === "clientMetadataDocument" ? input.existingClient?.clientCredentialsScopes ?? [] : input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
|
|
1232
|
-
schema.clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
|
|
1233
|
-
if (input.registrationSource === "clientMetadataDocument" && body.application_type === void 0) schema.applicationType = null;
|
|
1234
|
-
const resources = await resolveClientRegistrationResources(ctx, opts, input.registrationSource === "dynamic" ? input.requestedResources ?? [] : []);
|
|
1235
|
-
const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
1236
|
-
const clientResourceModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
1237
|
-
const existingClient = input.registrationSource === "clientMetadataDocument" ? input.existingClient : void 0;
|
|
1238
|
-
const clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
|
|
1239
|
-
const persistenceResult = await runWithTransaction(ctx.context.adapter, async () => {
|
|
1240
|
-
const adapter = await getCurrentAdapter(ctx.context.adapter);
|
|
1241
|
-
let storedClient;
|
|
1242
|
-
let created = false;
|
|
1243
|
-
if (existingClient) {
|
|
1244
|
-
const updatedClient = await adapter.update({
|
|
1245
|
-
model: clientModel,
|
|
1246
|
-
where: [{
|
|
1247
|
-
field: "clientId",
|
|
1248
|
-
value: clientId
|
|
1249
|
-
}, {
|
|
1250
|
-
field: "clientDiscoveryId",
|
|
1251
|
-
value: clientDiscoveryId
|
|
1252
|
-
}],
|
|
1253
|
-
update: {
|
|
1254
|
-
...schema,
|
|
1255
|
-
disabled: existingClient.disabled,
|
|
1256
|
-
skipConsent: existingClient.skipConsent,
|
|
1257
|
-
enableEndSession: existingClient.enableEndSession,
|
|
1258
|
-
createdAt: existingClient.createdAt,
|
|
1259
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
1260
|
-
}
|
|
1261
|
-
});
|
|
1262
|
-
if (!updatedClient) throw new APIError("BAD_REQUEST", {
|
|
1263
|
-
error: "invalid_client",
|
|
1264
|
-
error_description: "client no longer exists"
|
|
1265
|
-
});
|
|
1266
|
-
storedClient = updatedClient;
|
|
1267
|
-
} else {
|
|
1268
|
-
try {
|
|
1269
|
-
storedClient = await adapter.create({
|
|
1270
|
-
model: clientModel,
|
|
1271
|
-
data: {
|
|
1272
|
-
...schema,
|
|
1273
|
-
createdAt: /* @__PURE__ */ new Date(iat * 1e3),
|
|
1274
|
-
updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
if (input.registrationSource === "clientMetadataDocument" && isUniqueConstraintError(error)) throw {
|
|
1279
|
-
[CLIENT_REGISTRATION_COLLISION]: true,
|
|
1280
|
-
kind: "oauth-client-row-unique",
|
|
1281
|
-
clientId,
|
|
1282
|
-
cause: error
|
|
1283
|
-
};
|
|
1284
|
-
throw error;
|
|
1285
|
-
}
|
|
1286
|
-
created = true;
|
|
1287
|
-
}
|
|
1288
|
-
const linkedResources = resources.length === 0 ? [] : await adapter.findMany({
|
|
1289
|
-
model: clientResourceModel,
|
|
1290
|
-
where: [{
|
|
1291
|
-
field: "clientId",
|
|
1292
|
-
value: clientId
|
|
1293
|
-
}]
|
|
1294
|
-
});
|
|
1295
|
-
const linkedResourceIds = new Set(linkedResources.map((link) => link.resourceId));
|
|
1296
|
-
const now = /* @__PURE__ */ new Date();
|
|
1297
|
-
for (const resourceId of resources) {
|
|
1298
|
-
if (linkedResourceIds.has(resourceId)) continue;
|
|
1299
|
-
try {
|
|
1300
|
-
await adapter.create({
|
|
1301
|
-
model: clientResourceModel,
|
|
1302
|
-
data: {
|
|
1303
|
-
clientId,
|
|
1304
|
-
resourceId,
|
|
1305
|
-
createdAt: now
|
|
1306
|
-
}
|
|
1307
|
-
});
|
|
1308
|
-
} catch (error) {
|
|
1309
|
-
if (input.registrationSource === "clientMetadataDocument" && isUniqueConstraintError(error)) throw {
|
|
1310
|
-
[CLIENT_REGISTRATION_COLLISION]: true,
|
|
1311
|
-
kind: "oauth-client-resource-link-unique",
|
|
1312
|
-
clientId,
|
|
1313
|
-
resourceId,
|
|
1314
|
-
cause: error
|
|
1315
|
-
};
|
|
1316
|
-
throw error;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
return {
|
|
1320
|
-
client: storedClient,
|
|
1321
|
-
created
|
|
1322
|
-
};
|
|
1323
|
-
});
|
|
1324
|
-
return {
|
|
1325
|
-
client: persistenceResult.client,
|
|
1326
|
-
clientSecret,
|
|
1327
|
-
resources,
|
|
1328
|
-
created: persistenceResult.created
|
|
1329
|
-
};
|
|
1330
|
-
}
|
|
1331
|
-
function createOAuthClientRegistrationResponse(result, opts) {
|
|
1332
|
-
const responseBody = schemaToOAuth({
|
|
1333
|
-
...result.client,
|
|
1334
|
-
clientSecret: result.clientSecret ? (opts.prefix?.clientSecret ?? "") + result.clientSecret : void 0
|
|
1335
|
-
});
|
|
1336
|
-
if (result.resources.length > 0) responseBody.resources = result.resources;
|
|
1337
|
-
return responseBody;
|
|
1338
|
-
}
|
|
1339
|
-
function createOAuthClientAdministrativeResponse(result, opts) {
|
|
1340
|
-
return {
|
|
1341
|
-
...createOAuthClientRegistrationResponse(result, opts),
|
|
1342
|
-
client_credentials_scopes: [...result.client.clientCredentialsScopes ?? []]
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
/**
|
|
1346
|
-
* Creates and persists one canonical OAuth client registration.
|
|
1347
|
-
*/
|
|
1348
|
-
async function createOAuthClientRegistration(ctx, opts, input) {
|
|
1349
|
-
return createOAuthClientRegistrationResponse(await persistOAuthClientRegistration(ctx, opts, input), opts);
|
|
1350
|
-
}
|
|
1351
|
-
async function createOAuthClientAdministrativeRegistration(ctx, opts, input) {
|
|
1352
|
-
return createOAuthClientAdministrativeResponse(await persistOAuthClientRegistration(ctx, opts, input), opts);
|
|
1353
|
-
}
|
|
1354
|
-
function assertClientDiscoveryOwnership(client, clientDiscoveryId) {
|
|
1355
|
-
if (!client || client.clientDiscoveryId === clientDiscoveryId) return;
|
|
1356
|
-
throw new APIError("BAD_REQUEST", {
|
|
1357
|
-
error: "invalid_client",
|
|
1358
|
-
error_description: "client_id is already owned by a different registration source"
|
|
1359
|
-
});
|
|
1360
|
-
}
|
|
1361
|
-
/**
|
|
1362
|
-
* First-party persistence seam for a validated Client ID Metadata Document.
|
|
1363
|
-
*
|
|
1364
|
-
* This operation owns both initial creation and atomic replacement. It never
|
|
1365
|
-
* generates a client secret, preserves operator-controlled flags on
|
|
1366
|
-
* replacement, and ensures server-default resource links in the same
|
|
1367
|
-
* transaction.
|
|
1368
|
-
*
|
|
1369
|
-
* @internal
|
|
1370
|
-
*/
|
|
1371
|
-
async function registerClientMetadataDocument(ctx, opts, input) {
|
|
1372
|
-
assertClientDiscoveryOwnership(input.existingClient, input.clientDiscoveryId);
|
|
1373
|
-
if (input.metadata.backchannel_logout_uri !== void 0 || input.metadata.backchannel_logout_session_required !== void 0) throw new APIError("BAD_REQUEST", {
|
|
1374
|
-
error: "invalid_client_metadata",
|
|
1375
|
-
error_description: "Client ID Metadata Documents cannot register a back-channel logout target"
|
|
1376
|
-
});
|
|
1377
|
-
if ((input.metadata.token_endpoint_auth_method ?? "none") !== "none" && input.metadata.token_endpoint_auth_method !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
|
|
1378
|
-
error: "invalid_client_metadata",
|
|
1379
|
-
error_description: "Client ID Metadata Document clients cannot use a shared secret"
|
|
1380
|
-
});
|
|
1381
|
-
const registrationInput = {
|
|
1382
|
-
...input,
|
|
1383
|
-
metadata: {
|
|
1384
|
-
...input.metadata,
|
|
1385
|
-
token_endpoint_auth_method: input.metadata.token_endpoint_auth_method ?? "none",
|
|
1386
|
-
client_secret: void 0,
|
|
1387
|
-
client_secret_expires_at: void 0
|
|
1388
|
-
},
|
|
1389
|
-
registrationSource: "clientMetadataDocument"
|
|
1390
|
-
};
|
|
1391
|
-
try {
|
|
1392
|
-
return await persistOAuthClientRegistration(ctx, opts, registrationInput);
|
|
1393
|
-
} catch (error) {
|
|
1394
|
-
if (!isClientRegistrationCollision(error)) throw error;
|
|
1395
|
-
const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
|
|
1396
|
-
const clientResourceModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
|
|
1397
|
-
const currentClient = await ctx.context.adapter.findOne({
|
|
1398
|
-
model: clientModel,
|
|
1399
|
-
where: [{
|
|
1400
|
-
field: "clientId",
|
|
1401
|
-
value: input.clientId
|
|
1402
|
-
}]
|
|
1403
|
-
});
|
|
1404
|
-
if (error.clientId !== input.clientId || !currentClient || currentClient.clientDiscoveryId !== input.clientDiscoveryId) throw error.cause;
|
|
1405
|
-
if (error.kind === "oauth-client-resource-link-unique") {
|
|
1406
|
-
if (!await ctx.context.adapter.findOne({
|
|
1407
|
-
model: clientResourceModel,
|
|
1408
|
-
where: [{
|
|
1409
|
-
field: "clientId",
|
|
1410
|
-
value: error.clientId
|
|
1411
|
-
}, {
|
|
1412
|
-
field: "resourceId",
|
|
1413
|
-
value: error.resourceId
|
|
1414
|
-
}]
|
|
1415
|
-
})) throw error.cause;
|
|
1416
|
-
}
|
|
1417
|
-
try {
|
|
1418
|
-
return await persistOAuthClientRegistration(ctx, opts, {
|
|
1419
|
-
...registrationInput,
|
|
1420
|
-
existingClient: currentClient
|
|
1421
|
-
});
|
|
1422
|
-
} catch (retryError) {
|
|
1423
|
-
if (isClientRegistrationCollision(retryError)) throw retryError.cause;
|
|
1424
|
-
throw retryError;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
function isUniqueConstraintError(error) {
|
|
1429
|
-
if (!error || typeof error !== "object") return false;
|
|
1430
|
-
const candidate = error;
|
|
1431
|
-
const code = String(candidate.code ?? candidate.errno ?? "");
|
|
1432
|
-
if ([
|
|
1433
|
-
"1062",
|
|
1434
|
-
"11000",
|
|
1435
|
-
"23505",
|
|
1436
|
-
"ER_DUP_ENTRY",
|
|
1437
|
-
"P2002",
|
|
1438
|
-
"SQLITE_CONSTRAINT_UNIQUE"
|
|
1439
|
-
].includes(code)) return true;
|
|
1440
|
-
if (candidate.name === "MongoServerError" && candidate.code === 11e3) return true;
|
|
1441
|
-
return typeof candidate.message === "string" && /(?:duplicate|unique constraint|unique key)/i.test(candidate.message);
|
|
1442
|
-
}
|
|
1443
|
-
async function createOAuthClientEndpoint(ctx, opts, settings) {
|
|
1444
|
-
const session = await getSessionFromCtx(ctx);
|
|
1445
|
-
await assertClientPrivileges(ctx, session, opts, "create");
|
|
1446
|
-
if (!session) throw new APIError("UNAUTHORIZED");
|
|
1447
|
-
const referenceId = opts.clientReference ? await opts.clientReference({
|
|
1448
|
-
user: session.user,
|
|
1449
|
-
session: session.session
|
|
1450
|
-
}) : void 0;
|
|
1451
|
-
const { client_credentials_scopes: rawClientCredentialsScopes, ...metadata } = ctx.body;
|
|
1452
|
-
const clientCredentialsScopes = settings?.admin ? normalizeClientCredentialsScopes(rawClientCredentialsScopes ?? []) : [];
|
|
1453
|
-
validateClientCredentialsScopes(clientCredentialsScopes, resolveRegistrationGrantTypes(metadata), metadata.token_endpoint_auth_method, opts);
|
|
1454
|
-
if (clientCredentialsScopes.length > 0) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes");
|
|
1455
|
-
const registrationInput = {
|
|
1456
|
-
metadata,
|
|
1457
|
-
registrationSource: "managed",
|
|
1458
|
-
userId: referenceId ? void 0 : session.session.userId,
|
|
1459
|
-
referenceId,
|
|
1460
|
-
clientCredentialsScopes
|
|
1461
|
-
};
|
|
1462
|
-
const responseBody = settings?.admin ? await createOAuthClientAdministrativeRegistration(ctx, opts, registrationInput) : await createOAuthClientRegistration(ctx, opts, registrationInput);
|
|
1463
|
-
ctx.setStatus(201);
|
|
1464
|
-
return ctx.json(responseBody);
|
|
1465
|
-
}
|
|
1466
|
-
function oauthToSchema(input) {
|
|
1467
|
-
const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, application_type: applicationType, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata } = input;
|
|
1468
|
-
const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(Number(_expiresAt) * 1e3) : void 0;
|
|
1469
|
-
const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
|
|
1470
|
-
const scopes = _scope?.split(" ");
|
|
1471
|
-
const metadataObj = stripReservedOAuthClientMetadataExtensions(inputMetadata ?? {});
|
|
1472
|
-
const metadata = metadataObj && Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0;
|
|
1473
|
-
return {
|
|
1474
|
-
clientId,
|
|
1475
|
-
clientSecret,
|
|
1476
|
-
disabled,
|
|
1477
|
-
scopes,
|
|
1478
|
-
userId,
|
|
1479
|
-
createdAt,
|
|
1480
|
-
expiresAt,
|
|
1481
|
-
name,
|
|
1482
|
-
uri,
|
|
1483
|
-
icon,
|
|
1484
|
-
contacts,
|
|
1485
|
-
tos,
|
|
1486
|
-
policy,
|
|
1487
|
-
softwareId,
|
|
1488
|
-
softwareVersion,
|
|
1489
|
-
softwareStatement,
|
|
1490
|
-
redirectUris,
|
|
1491
|
-
postLogoutRedirectUris,
|
|
1492
|
-
backchannelLogoutUri,
|
|
1493
|
-
backchannelLogoutSessionRequired,
|
|
1494
|
-
tokenEndpointAuthMethod,
|
|
1495
|
-
grantTypes,
|
|
1496
|
-
responseTypes,
|
|
1497
|
-
jwks: inputJwks ? JSON.stringify(inputJwks) : void 0,
|
|
1498
|
-
jwksUri,
|
|
1499
|
-
applicationType,
|
|
1500
|
-
skipConsent,
|
|
1501
|
-
enableEndSession,
|
|
1502
|
-
requirePKCE,
|
|
1503
|
-
dpopBoundAccessTokens,
|
|
1504
|
-
subjectType,
|
|
1505
|
-
referenceId,
|
|
1506
|
-
metadata
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
/**
|
|
1510
|
-
* Converts a Database Schema to an OAuth 2.0 Dynamic Client Schema
|
|
1511
|
-
* @param input
|
|
1512
|
-
* @param cleaned - default true, determines if the output has only Oauth 2.0 compatible data
|
|
1513
|
-
* @returns
|
|
1514
|
-
*/
|
|
1515
|
-
function schemaToOAuth(input) {
|
|
1516
|
-
const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, applicationType, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
|
|
1517
|
-
const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
|
|
1518
|
-
const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
|
|
1519
|
-
const _scopes = scopes?.join(" ");
|
|
1520
|
-
return {
|
|
1521
|
-
...stripReservedOAuthClientMetadataExtensions(parseClientMetadata(metadata)),
|
|
1522
|
-
client_id: clientId,
|
|
1523
|
-
client_secret: clientSecret ?? void 0,
|
|
1524
|
-
client_secret_expires_at: clientSecret ? _expiresAt ?? 0 : void 0,
|
|
1525
|
-
scope: _scopes ?? void 0,
|
|
1526
|
-
user_id: userId ?? void 0,
|
|
1527
|
-
client_id_issued_at: _createdAt ?? void 0,
|
|
1528
|
-
client_name: name ?? void 0,
|
|
1529
|
-
client_uri: uri ?? void 0,
|
|
1530
|
-
logo_uri: icon ?? void 0,
|
|
1531
|
-
contacts: contacts ?? void 0,
|
|
1532
|
-
tos_uri: tos ?? void 0,
|
|
1533
|
-
policy_uri: policy ?? void 0,
|
|
1534
|
-
jwks: jwks ? JSON.parse(jwks) : void 0,
|
|
1535
|
-
jwks_uri: jwksUri ?? void 0,
|
|
1536
|
-
software_id: softwareId ?? void 0,
|
|
1537
|
-
software_version: softwareVersion ?? void 0,
|
|
1538
|
-
software_statement: softwareStatement ?? void 0,
|
|
1539
|
-
redirect_uris: redirectUris ?? [],
|
|
1540
|
-
post_logout_redirect_uris: postLogoutRedirectUris ?? void 0,
|
|
1541
|
-
backchannel_logout_uri: backchannelLogoutUri ?? void 0,
|
|
1542
|
-
backchannel_logout_session_required: backchannelLogoutSessionRequired ?? void 0,
|
|
1543
|
-
token_endpoint_auth_method: tokenEndpointAuthMethod ?? void 0,
|
|
1544
|
-
grant_types: grantTypes ?? void 0,
|
|
1545
|
-
response_types: responseTypes ?? void 0,
|
|
1546
|
-
application_type: applicationType ?? void 0,
|
|
1547
|
-
disabled: disabled ?? void 0,
|
|
1548
|
-
skip_consent: skipConsent ?? void 0,
|
|
1549
|
-
enable_end_session: enableEndSession ?? void 0,
|
|
1550
|
-
require_pkce: requirePKCE ?? void 0,
|
|
1551
|
-
dpop_bound_access_tokens: dpopBoundAccessTokens ?? void 0,
|
|
1552
|
-
subject_type: subjectType ?? void 0,
|
|
1553
|
-
reference_id: referenceId ?? void 0
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
//#endregion
|
|
1557
|
-
export { isAudienceClaimAllowed as _, registerEndpoint as a, resolveResourcePolicy as b, isForbiddenCimdClientMetadataField as c, validateClientCredentialsScopes as d, JWS_ALGORITHMS as f, invalidateResourceCache as g, getResourceCustomClaims as h, registerClientMetadataDocument as i, isUserDelegatedScope as l, extractRepeatedResourceFromForm as m, createOAuthClientEndpoint as n, schemaToOAuth as o, assertIdentifierValid as p, oauthToSchema as r, assertClientPrivileges as s, checkOAuthClient as t, normalizeClientCredentialsScopes as u, isClientLinkedToAnyResource as v, seedResources as x, logEnforcePerClientResourcesResolution as y };
|