@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.
@@ -16,7 +16,7 @@ function canonicalizeOAuthQueryParams(params) {
16
16
  }
17
17
  function setSignedOAuthQueryParameterNames(params) {
18
18
  params.delete(signedQueryParameterNameParam);
19
- const signedParameterNames = [.../* @__PURE__ */ new Set([...params.keys(), signedQueryParameterNameParam])].sort();
19
+ const signedParameterNames = [...new Set([...params.keys(), signedQueryParameterNameParam])].sort();
20
20
  for (const parameterName of signedParameterNames) params.append(signedQueryParameterNameParam, parameterName);
21
21
  }
22
22
  function getSignedOAuthQueryParameterNames(params) {
@@ -1,4 +1,4 @@
1
- import { n as canonicalizeOAuthQueryParams } from "./signed-query-BQAwsV_w.mjs";
1
+ import { n as canonicalizeOAuthQueryParams } from "./signed-query-Df1MNiSH.mjs";
2
2
  import { constantTimeEqual, makeSignature, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
3
3
  import { APIError } from "better-call";
4
4
  import { logger } from "@better-auth/core/env";
@@ -151,7 +151,7 @@ function getExtensionGrantTypes(opts) {
151
151
  return getOAuthProviderExtensions(opts).flatMap((extension) => Object.keys(extension.grants ?? {}));
152
152
  }
153
153
  function getSupportedGrantTypes(opts) {
154
- return Array.from(/* @__PURE__ */ new Set([...opts.grantTypes ?? DEFAULT_GRANT_TYPES, ...getExtensionGrantTypes(opts)]));
154
+ return Array.from(new Set([...opts.grantTypes ?? DEFAULT_GRANT_TYPES, ...getExtensionGrantTypes(opts)]));
155
155
  }
156
156
  function getExtensionGrantHandler(opts, grantType) {
157
157
  for (const extension of getOAuthProviderExtensions(opts)) {
@@ -169,7 +169,7 @@ function getExtensionTokenEndpointAuthMethods(opts) {
169
169
  * endpoints, which never accept public clients, omit it (the default).
170
170
  */
171
171
  function getSupportedAuthMethods(opts, settings) {
172
- return Array.from(/* @__PURE__ */ new Set([
172
+ return Array.from(new Set([
173
173
  ...settings?.includeNone ? ["none"] : [],
174
174
  ...BUILT_IN_CONFIDENTIAL_AUTH_METHODS,
175
175
  ...getExtensionTokenEndpointAuthMethods(opts)
@@ -707,7 +707,7 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
707
707
  confirmation: result.confirmation
708
708
  };
709
709
  }
710
- const { verifyClientAssertion: verify } = await import("./client-assertion-Cy8ZcH7P.mjs").then((n) => n.t);
710
+ const { verifyClientAssertion: verify } = await import("./authorize-BP2_wo6r.mjs").then((n) => n.o);
711
711
  return {
712
712
  kind: "pre_verified",
713
713
  method: "private_key_jwt",
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-rc.5";
3
+ const PACKAGE_VERSION = "1.7.0";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/oauth-provider",
3
- "version": "1.7.0-rc.5",
3
+ "version": "1.7.0",
4
4
  "description": "An oauth provider plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -70,17 +70,17 @@
70
70
  },
71
71
  "devDependencies": {
72
72
  "listhen": "^1.9.0",
73
- "tsdown": "0.22.7",
74
- "@better-auth/core": "1.7.0-rc.5",
75
- "better-auth": "1.7.0-rc.5",
76
- "@better-auth/memory-adapter": "1.7.0-rc.5"
73
+ "tsdown": "0.21.10",
74
+ "@better-auth/core": "1.7.0",
75
+ "@better-auth/memory-adapter": "1.7.0",
76
+ "better-auth": "1.7.0"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@better-auth/utils": "0.4.2",
80
80
  "@better-fetch/fetch": "1.3.1",
81
81
  "better-call": "1.4.0",
82
- "@better-auth/core": "^1.7.0-rc.5",
83
- "better-auth": "^1.7.0-rc.5"
82
+ "@better-auth/core": "^1.7.0",
83
+ "better-auth": "^1.7.0"
84
84
  },
85
85
  "scripts": {
86
86
  "build": "tsdown",
@@ -1,469 +0,0 @@
1
- import { P as getClientDiscoveries, a as getClient } from "./utils-jZEA4KQb.mjs";
2
- import { isPublicRoutableHost } from "@better-auth/core/utils/host";
3
- import { APIError } from "better-call";
4
- import { CLIENT_ASSERTION_TYPE, PRIVATE_KEY_JWT_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
5
- import { base64Url } from "@better-auth/utils/base64";
6
- import { createHash } from "@better-auth/utils/hash";
7
- import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
8
- //#region \0rolldown/runtime.js
9
- var __defProp = Object.defineProperty;
10
- var __exportAll = (all, no_symbols) => {
11
- let target = {};
12
- for (var name in all) __defProp(target, name, {
13
- get: all[name],
14
- enumerable: true
15
- });
16
- if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
17
- return target;
18
- };
19
- //#endregion
20
- //#region src/client-jwks.ts
21
- const EC_PRIVATE_KEY_JWT_ALGORITHM_BY_CURVE = {
22
- "P-256": "ES256",
23
- "P-384": "ES384",
24
- "P-521": "ES512"
25
- };
26
- const OKP_PRIVATE_KEY_JWT_SIGNING_CURVES = ["Ed25519"];
27
- const PRIVATE_JWK_MEMBER_NAMES = [
28
- "d",
29
- "p",
30
- "q",
31
- "dp",
32
- "dq",
33
- "qi",
34
- "oth"
35
- ];
36
- function isRecord(value) {
37
- return typeof value === "object" && value !== null && !Array.isArray(value);
38
- }
39
- function hasStringMember(key, memberName) {
40
- return typeof key[memberName] === "string" && key[memberName].length > 0;
41
- }
42
- function isSupportedEcSigningCurve(curve) {
43
- return typeof curve === "string" && Object.prototype.hasOwnProperty.call(EC_PRIVATE_KEY_JWT_ALGORITHM_BY_CURVE, curve);
44
- }
45
- function isSupportedOkpSigningCurve(curve) {
46
- return OKP_PRIVATE_KEY_JWT_SIGNING_CURVES.some((signingCurve) => signingCurve === curve);
47
- }
48
- function isSupportedPublicJwk(key) {
49
- switch (key.kty) {
50
- case "RSA": return hasStringMember(key, "n") && hasStringMember(key, "e");
51
- case "EC": return isSupportedEcSigningCurve(key.crv) && hasStringMember(key, "x") && hasStringMember(key, "y");
52
- case "OKP": return isSupportedOkpSigningCurve(key.crv) && hasStringMember(key, "x");
53
- default: return false;
54
- }
55
- }
56
- function hasSupportedPrivateKeyJwtAlgorithm(key) {
57
- if (key.alg === void 0) return true;
58
- if (typeof key.alg !== "string" || !PRIVATE_KEY_JWT_SIGNING_ALGORITHMS.some((algorithm) => algorithm === key.alg)) return false;
59
- switch (key.kty) {
60
- case "RSA": return key.alg.startsWith("RS") || key.alg.startsWith("PS");
61
- case "EC": return isSupportedEcSigningCurve(key.crv) && EC_PRIVATE_KEY_JWT_ALGORITHM_BY_CURVE[key.crv] === key.alg;
62
- case "OKP": return isSupportedOkpSigningCurve(key.crv) && key.alg === "EdDSA";
63
- default: return false;
64
- }
65
- }
66
- /**
67
- * Validates an OAuth client's public asymmetric JWK set.
68
- *
69
- * This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
70
- * It performs no I/O and returns the validated set for downstream JOSE
71
- * verification.
72
- *
73
- * @internal
74
- */
75
- function validatePublicClientJwks(input) {
76
- const keys = isRecord(input) && Array.isArray(input.keys) ? input.keys : void 0;
77
- if (!keys?.length) return {
78
- valid: false,
79
- error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array"
80
- };
81
- for (const key of keys) {
82
- if (!isRecord(key)) return {
83
- valid: false,
84
- error: "jwks keys must be supported public JWKs with required key parameters"
85
- };
86
- if (key.kty === "oct" || "k" in key || PRIVATE_JWK_MEMBER_NAMES.some((name) => name in key)) return {
87
- valid: false,
88
- error: "jwks must contain only public asymmetric keys"
89
- };
90
- if (!isSupportedPublicJwk(key)) return {
91
- valid: false,
92
- error: "jwks keys must be supported public JWKs with required key parameters"
93
- };
94
- if (!hasSupportedPrivateKeyJwtAlgorithm(key)) return {
95
- valid: false,
96
- error: "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve"
97
- };
98
- }
99
- return {
100
- valid: true,
101
- jwks: { keys }
102
- };
103
- }
104
- //#endregion
105
- //#region src/utils/client-assertion.ts
106
- var client_assertion_exports = /* @__PURE__ */ __exportAll({
107
- consumeClientAssertion: () => consumeClientAssertion,
108
- isPrivateHostname: () => isPrivateHostname,
109
- verifyClientAssertion: () => verifyClientAssertion
110
- });
111
- const jwksCaches = /* @__PURE__ */ new WeakMap();
112
- const JWKS_CACHE_TTL_MS = 300 * 1e3;
113
- const JWKS_CACHE_MAX_ENTRIES = 500;
114
- const JWKS_FETCH_TIMEOUT_MS = 5e3;
115
- const MAX_JWKS_RESPONSE_BYTES = 64 * 1024;
116
- const JSON_CONTENT_TYPE = /^application\/(?:[-\w.]+\+)?json\s*(?:;|$)/i;
117
- function setJwksCache(jwksCache, cacheKey, jwks, fetchedAt) {
118
- jwksCache.set(cacheKey, {
119
- jwks,
120
- fetchedAt
121
- });
122
- if (jwksCache.size > JWKS_CACHE_MAX_ENTRIES) {
123
- const oldest = jwksCache.keys().next().value;
124
- if (oldest !== void 0) jwksCache.delete(oldest);
125
- }
126
- }
127
- function getJwksCache(opts) {
128
- const existingCache = jwksCaches.get(opts);
129
- if (existingCache) return existingCache;
130
- const cache = /* @__PURE__ */ new Map();
131
- jwksCaches.set(opts, cache);
132
- return cache;
133
- }
134
- function getJwksCacheKey(client) {
135
- return `${client.clientDiscoveryId ?? "managed"}:${client.jwksUri ?? ""}`;
136
- }
137
- const ALGORITHMS_LIST = [...PRIVATE_KEY_JWT_SIGNING_ALGORITHMS];
138
- /**
139
- * SSRF gate for user-supplied server-side fetch targets (`jwks_uri`,
140
- * `backchannel_logout_uri`): returns true when the host is NOT publicly
141
- * routable. That covers loopback, RFC 1918 private, link-local (including AWS
142
- * IMDS `169.254.169.254`), shared-address-space (carrier-grade NAT),
143
- * IPv4-mapped IPv6, 6to4/NAT64/Teredo tunnels, every other RFC 6890
144
- * special-purpose range, and cloud-metadata FQDNs.
145
- *
146
- * Delegates to the audited single source of truth so this check cannot drift
147
- * into the kind of encoding bypass that bespoke regexes invite. This is a
148
- * syntactic check only: it does not resolve DNS, so a public name that
149
- * resolves to a private address at fetch time is not caught here.
150
- */
151
- function isPrivateHostname(hostname) {
152
- return !isPublicRoutableHost(hostname);
153
- }
154
- function validateJwksUri(ctx, jwksUri, clientIdUrlOrigin) {
155
- const parsed = new URL(jwksUri);
156
- if (parsed.protocol !== "https:") throw new APIError("BAD_REQUEST", {
157
- error_description: "jwks_uri must use HTTPS",
158
- error: "invalid_client"
159
- });
160
- if (parsed.username || parsed.password) throw new APIError("BAD_REQUEST", {
161
- error_description: "jwks_uri must not contain credentials",
162
- error: "invalid_client"
163
- });
164
- if (jwksUri.includes("#")) throw new APIError("BAD_REQUEST", {
165
- error_description: "jwks_uri must not include a fragment component",
166
- error: "invalid_client"
167
- });
168
- if (isPrivateHostname(parsed.hostname)) throw new APIError("BAD_REQUEST", {
169
- error_description: "jwks_uri must not point to a private or reserved address",
170
- error: "invalid_client"
171
- });
172
- if (clientIdUrlOrigin && parsed.origin === clientIdUrlOrigin) return;
173
- if (!ctx.context.isTrustedOrigin(parsed.href)) throw new APIError("BAD_REQUEST", {
174
- error_description: "client jwks_uri is not trusted",
175
- error: "invalid_client"
176
- });
177
- }
178
- function urlClientIdOrigin(clientId) {
179
- try {
180
- const parsed = new URL(clientId);
181
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return;
182
- return parsed.origin;
183
- } catch {
184
- return;
185
- }
186
- }
187
- async function readBoundedResponseBody(response) {
188
- const contentLength = response.headers.get("content-length");
189
- if (contentLength !== null && Number.isFinite(Number(contentLength)) && Number(contentLength) > MAX_JWKS_RESPONSE_BYTES) {
190
- await response.body?.cancel();
191
- throw new Error("JWKS response exceeds 64 KiB");
192
- }
193
- if (!response.body) return "";
194
- const reader = response.body.getReader();
195
- const chunks = [];
196
- let totalBytes = 0;
197
- while (true) {
198
- const { done, value } = await reader.read();
199
- if (done) break;
200
- totalBytes += value.byteLength;
201
- if (totalBytes > MAX_JWKS_RESPONSE_BYTES) {
202
- await reader.cancel();
203
- throw new Error("JWKS response exceeds 64 KiB");
204
- }
205
- chunks.push(value);
206
- }
207
- const bytes = new Uint8Array(totalBytes);
208
- let offset = 0;
209
- for (const chunk of chunks) {
210
- bytes.set(chunk, offset);
211
- offset += chunk.byteLength;
212
- }
213
- return new TextDecoder().decode(bytes);
214
- }
215
- async function fetchJwksFromUri(jwksUri, fetchClientMetadataResource = globalThis.fetch) {
216
- const controller = new AbortController();
217
- const timeout = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
218
- try {
219
- const response = await fetchClientMetadataResource(jwksUri, {
220
- signal: controller.signal,
221
- headers: { accept: "application/json" },
222
- redirect: "error"
223
- });
224
- if (response.redirected) throw new Error("JWKS fetch redirected");
225
- if (response.status !== 200) throw new Error(`JWKS fetch returned ${response.status}`);
226
- const contentType = response.headers.get("content-type");
227
- if (!contentType || !JSON_CONTENT_TYPE.test(contentType)) throw new Error("JWKS response must use a JSON media type");
228
- const responseBody = await readBoundedResponseBody(response);
229
- let parsedBody;
230
- try {
231
- parsedBody = JSON.parse(responseBody);
232
- } catch {
233
- return { valid: false };
234
- }
235
- const result = validatePublicClientJwks(parsedBody);
236
- if (!result.valid) return { valid: false };
237
- return {
238
- valid: true,
239
- jwks: result.jwks
240
- };
241
- } finally {
242
- clearTimeout(timeout);
243
- }
244
- }
245
- function createClientJwksFetchError() {
246
- return new APIError("BAD_REQUEST", {
247
- error_description: "failed to fetch client JWKS",
248
- error: "invalid_client"
249
- });
250
- }
251
- async function fetchClientJwks(ctx, opts, client) {
252
- if (client.jwks) return JSON.parse(client.jwks);
253
- if (!client.jwksUri) throw new APIError("BAD_REQUEST", {
254
- error_description: "client has no JWKS configured",
255
- error: "invalid_client"
256
- });
257
- const discovery = client.clientDiscoveryId ? getClientDiscoveries(opts).find((candidate) => candidate.id === client.clientDiscoveryId) : void 0;
258
- if (client.clientDiscoveryId && !discovery?.fetchClientMetadataResource) throw new APIError("BAD_REQUEST", {
259
- error_description: "client discovery does not provide a metadata resource transport",
260
- error: "invalid_client"
261
- });
262
- validateJwksUri(ctx, client.jwksUri, client.clientDiscoveryId ? urlClientIdOrigin(client.clientId) : void 0);
263
- const now = Date.now();
264
- const cacheKey = getJwksCacheKey(client);
265
- const jwksCache = getJwksCache(opts);
266
- const cached = jwksCache.get(cacheKey);
267
- if (cached && now - cached.fetchedAt < JWKS_CACHE_TTL_MS) return cached.jwks;
268
- let result;
269
- try {
270
- result = await fetchJwksFromUri(client.jwksUri, discovery?.fetchClientMetadataResource);
271
- } catch {
272
- const staleLimitMs = JWKS_CACHE_TTL_MS * 2;
273
- if (cached && now - cached.fetchedAt < staleLimitMs) return cached.jwks;
274
- throw createClientJwksFetchError();
275
- }
276
- if (!result.valid) throw createClientJwksFetchError();
277
- setJwksCache(jwksCache, cacheKey, result.jwks, now);
278
- return result.jwks;
279
- }
280
- /**
281
- * Refetch JWKS from jwks_uri when signature verification fails with cached keys.
282
- * Handles key rotation: the client may have published a new key that isn't in our cache yet.
283
- */
284
- async function refetchClientJwks(opts, client) {
285
- if (!client.jwksUri) return null;
286
- const discovery = client.clientDiscoveryId ? getClientDiscoveries(opts).find((candidate) => candidate.id === client.clientDiscoveryId) : void 0;
287
- if (client.clientDiscoveryId && !discovery?.fetchClientMetadataResource) return null;
288
- try {
289
- const result = await fetchJwksFromUri(client.jwksUri, discovery?.fetchClientMetadataResource);
290
- if (!result.valid) return null;
291
- setJwksCache(getJwksCache(opts), getJwksCacheKey(client), result.jwks, Date.now());
292
- return result.jwks;
293
- } catch {
294
- return null;
295
- }
296
- }
297
- /**
298
- * Enforces the assertion-hygiene claims every client-assertion authentication
299
- * method must check, independent of how the signature is verified or where the
300
- * verification keys come from:
301
- * - `aud` MUST include `expectedAudience` (RFC 7523 §3 rule 3),
302
- * - `exp` MUST be present, unexpired, and at most `assertionMaxLifetime`
303
- * seconds away (RFC 7523 §3 rule 4),
304
- * - `iat`, when present, MUST be within `assertionMaxLifetime`,
305
- * - `jti` MUST be present and single-use; this consumes a replay tombstone keyed
306
- * by `` `${namespace}:${jti}` ``, inserted under the adapter's primary key so a
307
- * replay across workers fails atomically.
308
- *
309
- * A custom {@link OAuthClientAuthenticationStrategy} should call this after
310
- * verifying the assertion signature, so an extension method inherits the same
311
- * replay, lifetime, and audience guarantees as the built-in `private_key_jwt`
312
- * path, which calls it too.
313
- *
314
- * @param params.namespace Scopes the replay tombstone to the method and client,
315
- * e.g. `` `${method}:${clientId}` ``, so the same `jti` can recur across
316
- * distinct methods or clients but never within one.
317
- */
318
- async function consumeClientAssertion(ctx, opts, params) {
319
- const { namespace, payload, expectedAudience } = params;
320
- if (!(Array.isArray(payload.aud) ? payload.aud : payload.aud != null ? [payload.aud] : []).includes(expectedAudience)) throw new APIError("BAD_REQUEST", {
321
- error_description: "client assertion aud does not match the endpoint",
322
- error: "invalid_client"
323
- });
324
- const maxLifetime = opts.assertionMaxLifetime ?? 300;
325
- const now = Math.floor(Date.now() / 1e3);
326
- if (typeof payload.exp !== "number") throw new APIError("BAD_REQUEST", {
327
- error_description: "client assertion must include exp claim",
328
- error: "invalid_client"
329
- });
330
- if (payload.exp <= now) throw new APIError("BAD_REQUEST", {
331
- error_description: "client assertion has expired",
332
- error: "invalid_client"
333
- });
334
- if (payload.exp - now > maxLifetime) throw new APIError("BAD_REQUEST", {
335
- error_description: `client assertion exp is too far in the future (max ${maxLifetime}s)`,
336
- error: "invalid_client"
337
- });
338
- if (typeof payload.iat === "number" && now - payload.iat > maxLifetime) throw new APIError("BAD_REQUEST", {
339
- error_description: `client assertion iat is too far in the past (max ${maxLifetime}s)`,
340
- error: "invalid_client"
341
- });
342
- if (typeof payload.jti !== "string" || payload.jti.length === 0) throw new APIError("BAD_REQUEST", {
343
- error_description: "client assertion must include jti claim",
344
- error: "invalid_client"
345
- });
346
- const jtiDigest = await createHash("SHA-256").digest(new TextEncoder().encode(`${namespace}:${payload.jti}`));
347
- const jtiId = base64Url.encode(new Uint8Array(jtiDigest).slice(0, 24), { padding: false });
348
- try {
349
- await ctx.context.adapter.create({
350
- model: "oauthClientAssertion",
351
- data: {
352
- id: jtiId,
353
- expiresAt: /* @__PURE__ */ new Date(payload.exp * 1e3)
354
- },
355
- forceAllowId: true
356
- });
357
- } catch (createErr) {
358
- let alreadyUsed = false;
359
- try {
360
- alreadyUsed = Boolean(await ctx.context.adapter.findOne({
361
- model: "oauthClientAssertion",
362
- where: [{
363
- field: "id",
364
- value: jtiId
365
- }]
366
- }));
367
- } catch {}
368
- if (alreadyUsed) throw new APIError("BAD_REQUEST", {
369
- error_description: "client assertion jti has already been used",
370
- error: "invalid_client"
371
- });
372
- throw createErr;
373
- }
374
- }
375
- /**
376
- * Verifies a client assertion JWT for `private_key_jwt` authentication.
377
- *
378
- * Validates: signature, iss=client_id, sub=client_id, aud=token_endpoint,
379
- * exp, assertion max lifetime, jti uniqueness (replay prevention).
380
- */
381
- async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
382
- if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError("BAD_REQUEST", {
383
- error_description: "unsupported client_assertion_type",
384
- error: "invalid_client"
385
- });
386
- let header;
387
- try {
388
- header = decodeProtectedHeader(clientAssertion);
389
- } catch {
390
- throw new APIError("BAD_REQUEST", {
391
- error_description: "malformed client assertion: invalid JWT header",
392
- error: "invalid_client"
393
- });
394
- }
395
- if (!header.alg || !ALGORITHMS_LIST.includes(header.alg)) throw new APIError("BAD_REQUEST", {
396
- error_description: `unsupported assertion signing algorithm: ${header.alg}`,
397
- error: "invalid_client"
398
- });
399
- let unverified;
400
- try {
401
- unverified = decodeJwt(clientAssertion);
402
- } catch {
403
- throw new APIError("BAD_REQUEST", {
404
- error_description: "malformed client assertion: invalid JWT payload",
405
- error: "invalid_client"
406
- });
407
- }
408
- const clientId = unverified.sub ?? unverified.iss;
409
- if (!clientId) throw new APIError("BAD_REQUEST", {
410
- error_description: "client assertion must contain sub or iss claim identifying the client",
411
- error: "invalid_client"
412
- });
413
- if (clientIdHint && clientIdHint !== clientId) throw new APIError("BAD_REQUEST", {
414
- error_description: "client_id in body does not match assertion sub/iss",
415
- error: "invalid_client"
416
- });
417
- const client = await getClient(ctx, opts, clientId);
418
- if (!client) throw new APIError("BAD_REQUEST", {
419
- error_description: "unknown client",
420
- error: "invalid_client"
421
- });
422
- if (client.disabled) throw new APIError("BAD_REQUEST", {
423
- error_description: "client is disabled",
424
- error: "invalid_client"
425
- });
426
- if (client.tokenEndpointAuthMethod !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
427
- error_description: "client is not registered for private_key_jwt authentication",
428
- error: "invalid_client"
429
- });
430
- const jwks = await fetchClientJwks(ctx, opts, client);
431
- const audience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
432
- const verifyOpts = {
433
- issuer: clientId,
434
- subject: clientId,
435
- audience,
436
- algorithms: ALGORITHMS_LIST
437
- };
438
- let payload;
439
- try {
440
- ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(jwks), verifyOpts));
441
- } catch (verifyErr) {
442
- if (verifyErr instanceof Error && /no matching key|no applicable key/i.test(verifyErr.message)) {
443
- const refreshed = await refetchClientJwks(opts, client);
444
- if (refreshed) try {
445
- ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(refreshed), verifyOpts));
446
- } catch {
447
- throw new APIError("UNAUTHORIZED", {
448
- error_description: "client assertion signature verification failed",
449
- error: "invalid_client"
450
- });
451
- }
452
- else throw new APIError("UNAUTHORIZED", {
453
- error_description: "client assertion signature verification failed",
454
- error: "invalid_client"
455
- });
456
- } else throw new APIError("UNAUTHORIZED", {
457
- error_description: "client assertion signature verification failed",
458
- error: "invalid_client"
459
- });
460
- }
461
- await consumeClientAssertion(ctx, opts, {
462
- namespace: `private_key_jwt:${clientId}`,
463
- payload,
464
- expectedAudience: audience
465
- });
466
- return { clientId };
467
- }
468
- //#endregion
469
- export { __exportAll as a, validatePublicClientJwks as i, consumeClientAssertion as n, isPrivateHostname as r, client_assertion_exports as t };