@better-auth/oauth-provider 1.6.1 → 1.7.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ import { a as getClient } from "./utils-CIbcUsZ5.mjs";
2
+ import { APIError } from "better-call";
3
+ import { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE } from "@better-auth/core/oauth2";
4
+ import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ return target;
15
+ };
16
+ //#endregion
17
+ //#region src/utils/client-assertion.ts
18
+ var client_assertion_exports = /* @__PURE__ */ __exportAll({
19
+ isPrivateHostname: () => isPrivateHostname,
20
+ verifyClientAssertion: () => verifyClientAssertion
21
+ });
22
+ const jwksCache = /* @__PURE__ */ new Map();
23
+ const JWKS_CACHE_TTL_MS = 300 * 1e3;
24
+ const JWKS_CACHE_MAX_ENTRIES = 500;
25
+ const JWKS_FETCH_TIMEOUT_MS = 5e3;
26
+ function setJwksCache(uri, jwks, fetchedAt) {
27
+ jwksCache.set(uri, {
28
+ jwks,
29
+ fetchedAt
30
+ });
31
+ if (jwksCache.size > JWKS_CACHE_MAX_ENTRIES) {
32
+ const oldest = jwksCache.keys().next().value;
33
+ if (oldest !== void 0) jwksCache.delete(oldest);
34
+ }
35
+ }
36
+ const ALGORITHMS_LIST = [...ASSERTION_SIGNING_ALGORITHMS];
37
+ const pendingAssertionIds = /* @__PURE__ */ new Set();
38
+ /**
39
+ * Block SSRF: reject jwks_uri pointing at private/reserved IP ranges.
40
+ * Only HTTPS with public hostnames is allowed.
41
+ */
42
+ function isPrivateIpv4(hostname) {
43
+ const parts = hostname.split(".");
44
+ if (parts.length !== 4 || parts.some((p) => !/^\d{1,3}$/.test(p))) return false;
45
+ const octets = parts.map(Number);
46
+ const a = octets[0];
47
+ const b = octets[1];
48
+ return a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254 || a === 127;
49
+ }
50
+ function isPrivateHostname(hostname) {
51
+ const lower = hostname.toLowerCase();
52
+ const host = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
53
+ if (host === "localhost" || host === "::1") return true;
54
+ if (isPrivateIpv4(host)) return true;
55
+ if (host.includes(":")) {
56
+ const v4MappedMatch = host.match(/^(?:0{0,4}:){0,4}:?(?:0{0,4}:)?ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
57
+ if (v4MappedMatch && isPrivateIpv4(v4MappedMatch[1])) return true;
58
+ const isLinkLocal = host.startsWith("fe8") || host.startsWith("fe9") || host.startsWith("fea") || host.startsWith("feb");
59
+ const isUniqueLocal = host.startsWith("fc") || host.startsWith("fd");
60
+ if (isLinkLocal || isUniqueLocal) return true;
61
+ }
62
+ if (host === "metadata.google.internal") return true;
63
+ return false;
64
+ }
65
+ function validateJwksUri(ctx, jwksUri) {
66
+ const parsed = new URL(jwksUri);
67
+ if (parsed.protocol !== "https:") throw new APIError("BAD_REQUEST", {
68
+ error_description: "jwks_uri must use HTTPS",
69
+ error: "invalid_client"
70
+ });
71
+ if (isPrivateHostname(parsed.hostname)) throw new APIError("BAD_REQUEST", {
72
+ error_description: "jwks_uri must not point to a private or reserved address",
73
+ error: "invalid_client"
74
+ });
75
+ if (!ctx.context.isTrustedOrigin(parsed.href)) throw new APIError("BAD_REQUEST", {
76
+ error_description: "client jwks_uri is not trusted",
77
+ error: "invalid_client"
78
+ });
79
+ }
80
+ async function fetchJwksFromUri(jwksUri) {
81
+ const controller = new AbortController();
82
+ const timeout = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
83
+ try {
84
+ const response = await fetch(jwksUri, {
85
+ signal: controller.signal,
86
+ headers: { accept: "application/json" },
87
+ redirect: "error"
88
+ });
89
+ if (!response.ok) throw new Error(`JWKS fetch returned ${response.status}`);
90
+ const jwks = await response.json();
91
+ if (!jwks.keys || !Array.isArray(jwks.keys)) throw new Error("JWKS response missing keys array");
92
+ return jwks;
93
+ } finally {
94
+ clearTimeout(timeout);
95
+ }
96
+ }
97
+ async function fetchClientJwks(ctx, client) {
98
+ if (client.jwks) return JSON.parse(client.jwks);
99
+ if (!client.jwksUri) throw new APIError("BAD_REQUEST", {
100
+ error_description: "client has no JWKS configured",
101
+ error: "invalid_client"
102
+ });
103
+ validateJwksUri(ctx, client.jwksUri);
104
+ const now = Date.now();
105
+ const cached = jwksCache.get(client.jwksUri);
106
+ if (cached && now - cached.fetchedAt < JWKS_CACHE_TTL_MS) return cached.jwks;
107
+ try {
108
+ const jwks = await fetchJwksFromUri(client.jwksUri);
109
+ setJwksCache(client.jwksUri, jwks, now);
110
+ return jwks;
111
+ } catch {
112
+ const staleLimitMs = JWKS_CACHE_TTL_MS * 2;
113
+ if (cached && now - cached.fetchedAt < staleLimitMs) return cached.jwks;
114
+ throw new APIError("BAD_REQUEST", {
115
+ error_description: "failed to fetch client JWKS",
116
+ error: "invalid_client"
117
+ });
118
+ }
119
+ }
120
+ /**
121
+ * Refetch JWKS from jwks_uri when signature verification fails with cached keys.
122
+ * Handles key rotation: the client may have published a new key that isn't in our cache yet.
123
+ */
124
+ async function refetchClientJwks(client) {
125
+ if (!client.jwksUri) return null;
126
+ try {
127
+ const jwks = await fetchJwksFromUri(client.jwksUri);
128
+ setJwksCache(client.jwksUri, jwks, Date.now());
129
+ return jwks;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * Verifies a client assertion JWT for `private_key_jwt` authentication.
136
+ *
137
+ * Validates: signature, iss=client_id, sub=client_id, aud=token_endpoint,
138
+ * exp, assertion max lifetime, jti uniqueness (replay prevention).
139
+ */
140
+ async function verifyClientAssertion(ctx, opts, clientAssertion, clientAssertionType, clientIdHint, expectedAudience) {
141
+ if (clientAssertionType !== CLIENT_ASSERTION_TYPE) throw new APIError("BAD_REQUEST", {
142
+ error_description: "unsupported client_assertion_type",
143
+ error: "invalid_client"
144
+ });
145
+ let header;
146
+ try {
147
+ header = decodeProtectedHeader(clientAssertion);
148
+ } catch {
149
+ throw new APIError("BAD_REQUEST", {
150
+ error_description: "malformed client assertion: invalid JWT header",
151
+ error: "invalid_client"
152
+ });
153
+ }
154
+ if (!header.alg || !ASSERTION_SIGNING_ALGORITHMS.includes(header.alg)) throw new APIError("BAD_REQUEST", {
155
+ error_description: `unsupported assertion signing algorithm: ${header.alg}`,
156
+ error: "invalid_client"
157
+ });
158
+ let unverified;
159
+ try {
160
+ unverified = decodeJwt(clientAssertion);
161
+ } catch {
162
+ throw new APIError("BAD_REQUEST", {
163
+ error_description: "malformed client assertion: invalid JWT payload",
164
+ error: "invalid_client"
165
+ });
166
+ }
167
+ const clientId = unverified.sub ?? unverified.iss;
168
+ if (!clientId) throw new APIError("BAD_REQUEST", {
169
+ error_description: "client assertion must contain sub or iss claim identifying the client",
170
+ error: "invalid_client"
171
+ });
172
+ if (clientIdHint && clientIdHint !== clientId) throw new APIError("BAD_REQUEST", {
173
+ error_description: "client_id in body does not match assertion sub/iss",
174
+ error: "invalid_client"
175
+ });
176
+ const client = await getClient(ctx, opts, clientId);
177
+ if (!client) throw new APIError("BAD_REQUEST", {
178
+ error_description: "unknown client",
179
+ error: "invalid_client"
180
+ });
181
+ if (client.disabled) throw new APIError("BAD_REQUEST", {
182
+ error_description: "client is disabled",
183
+ error: "invalid_client"
184
+ });
185
+ if (client.tokenEndpointAuthMethod !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
186
+ error_description: "client is not registered for private_key_jwt authentication",
187
+ error: "invalid_client"
188
+ });
189
+ const jwks = await fetchClientJwks(ctx, client);
190
+ const audience = expectedAudience ?? `${ctx.context.baseURL}/oauth2/token`;
191
+ const maxLifetime = opts.assertionMaxLifetime ?? 300;
192
+ const verifyOpts = {
193
+ issuer: clientId,
194
+ subject: clientId,
195
+ audience,
196
+ algorithms: ALGORITHMS_LIST
197
+ };
198
+ let payload;
199
+ try {
200
+ ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(jwks), verifyOpts));
201
+ } catch (verifyErr) {
202
+ if (verifyErr instanceof Error && /no matching key|no applicable key/i.test(verifyErr.message)) {
203
+ const refreshed = await refetchClientJwks(client);
204
+ if (refreshed) try {
205
+ ({payload} = await jwtVerify(clientAssertion, createLocalJWKSet(refreshed), verifyOpts));
206
+ } catch {
207
+ throw new APIError("UNAUTHORIZED", {
208
+ error_description: "client assertion signature verification failed",
209
+ error: "invalid_client"
210
+ });
211
+ }
212
+ else throw new APIError("UNAUTHORIZED", {
213
+ error_description: "client assertion signature verification failed",
214
+ error: "invalid_client"
215
+ });
216
+ } else throw new APIError("UNAUTHORIZED", {
217
+ error_description: "client assertion signature verification failed",
218
+ error: "invalid_client"
219
+ });
220
+ }
221
+ const now = Math.floor(Date.now() / 1e3);
222
+ if (typeof payload.exp !== "number") throw new APIError("BAD_REQUEST", {
223
+ error_description: "client assertion must include exp claim",
224
+ error: "invalid_client"
225
+ });
226
+ if (payload.exp - now > maxLifetime) throw new APIError("BAD_REQUEST", {
227
+ error_description: `client assertion exp is too far in the future (max ${maxLifetime}s)`,
228
+ error: "invalid_client"
229
+ });
230
+ if (typeof payload.iat === "number" && now - payload.iat > maxLifetime) throw new APIError("BAD_REQUEST", {
231
+ error_description: `client assertion iat is too far in the past (max ${maxLifetime}s)`,
232
+ error: "invalid_client"
233
+ });
234
+ if (!payload.jti) throw new APIError("BAD_REQUEST", {
235
+ error_description: "client assertion must include jti claim",
236
+ error: "invalid_client"
237
+ });
238
+ const jtiIdentifier = `private_key_jwt:${clientId}:${payload.jti}`;
239
+ if (pendingAssertionIds.has(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
240
+ error_description: "client assertion jti has already been used",
241
+ error: "invalid_client"
242
+ });
243
+ pendingAssertionIds.add(jtiIdentifier);
244
+ try {
245
+ if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
246
+ error_description: "client assertion jti has already been used",
247
+ error: "invalid_client"
248
+ });
249
+ const jtiExpiry = /* @__PURE__ */ new Date(payload.exp * 1e3);
250
+ try {
251
+ await ctx.context.internalAdapter.createVerificationValue({
252
+ identifier: jtiIdentifier,
253
+ value: clientId,
254
+ expiresAt: jtiExpiry
255
+ });
256
+ } catch (createErr) {
257
+ if (await ctx.context.internalAdapter.findVerificationValue(jtiIdentifier)) throw new APIError("BAD_REQUEST", {
258
+ error_description: "client assertion jti has already been used",
259
+ error: "invalid_client"
260
+ });
261
+ throw createErr;
262
+ }
263
+ } finally {
264
+ pendingAssertionIds.delete(jtiIdentifier);
265
+ }
266
+ return {
267
+ clientId,
268
+ client
269
+ };
270
+ }
271
+ //#endregion
272
+ export { isPrivateHostname as n, client_assertion_exports as t };
@@ -1,4 +1,4 @@
1
- import { a as ResourceServerMetadata } from "./oauth-Cc0nzj5Q.mjs";
1
+ import { a as ResourceServerMetadata } from "./oauth-C8aTlaAC.mjs";
2
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
3
  import { Auth } from "better-auth/types";
4
4
 
@@ -1,5 +1,6 @@
1
- import { a as getJwtPlugin, o as getOAuthProviderPlugin, v as handleMcpErrors } from "./utils-sQ4gYeh3.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-xqVKoocI.mjs";
1
+ import { t as handleMcpErrors } from "./mcp-CYnz-MXn.mjs";
2
+ import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-CIbcUsZ5.mjs";
3
+ import { t as PACKAGE_VERSION } from "./version-BGWhjYBb.mjs";
3
4
  import { verifyAccessToken } from "better-auth/oauth2";
4
5
  import { APIError } from "better-call";
5
6
  import { logger } from "@better-auth/core/env";
package/dist/client.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as oauthProvider } from "./oauth-CYgzO8Am.mjs";
1
+ import { n as oauthProvider } from "./oauth-Dh4YXCXY.mjs";
2
2
  import * as _better_fetch_fetch0 from "@better-fetch/fetch";
3
3
 
4
4
  //#region src/client.d.ts
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-xqVKoocI.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-BGWhjYBb.mjs";
2
2
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
3
  //#region src/client.ts
4
4
  function parseSignedQuery(search) {
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as Awaitable, a as ResourceServerMetadata, c as OAuthConsent, d as OAuthRefreshToken, f as Prompt, g as VerificationValue, h as StoreTokenType, i as OIDCMetadata, l as OAuthOpaqueAccessToken, m as Scope, n as GrantType, o as AuthorizePrompt, p as SchemaClient, r as OAuthClient, s as OAuthAuthorizationQuery, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-Cc0nzj5Q.mjs";
2
- import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-CYgzO8Am.mjs";
1
+ import { _ as Awaitable, a as ResourceServerMetadata, c as OAuthConsent, d as OAuthRefreshToken, f as Prompt, g as VerificationValue, h as StoreTokenType, i as OIDCMetadata, l as OAuthOpaqueAccessToken, m as Scope, n as GrantType, o as AuthorizePrompt, p as SchemaClient, r as OAuthClient, s as OAuthAuthorizationQuery, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-C8aTlaAC.mjs";
2
+ import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-Dh4YXCXY.mjs";
3
3
  import { verifyAccessToken } from "better-auth/oauth2";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { JWTPayload } from "jose";
package/dist/index.mjs CHANGED
@@ -1,8 +1,11 @@
1
- import { _ as verifyOAuthQueryParams, a as getJwtPlugin, c as isPKCERequired, d as parsePrompt, f as resolveSessionAuthTime, g as validateClientCredentials, h as storeToken, i as getClient, l as normalizeTimestampValue, m as storeClientSecret, n as decryptStoredClientSecret, p as resolveSubjectIdentifier, r as deleteFromPrompt, s as getStoredToken, t as basicToClientCredentials, u as parseClientMetadata, y as mcpHandler } from "./utils-sQ4gYeh3.mjs";
2
- import { t as PACKAGE_VERSION } from "./version-xqVKoocI.mjs";
1
+ import { n as isPrivateHostname } from "./client-assertion-DZqo-L5j.mjs";
2
+ import { n as mcpHandler } from "./mcp-CYnz-MXn.mjs";
3
+ import { _ as storeToken, a as getClient, c as getStoredToken, d as parseClientMetadata, f as parsePrompt, g as storeClientSecret, h as searchParamsToQuery, i as extractClientCredentials, l as isPKCERequired, m as resolveSubjectIdentifier, n as deleteFromPrompt, o as getJwtPlugin, p as resolveSessionAuthTime, r as destructureCredentials, t as decryptStoredClientSecret, u as normalizeTimestampValue, v as validateClientCredentials, y as verifyOAuthQueryParams } from "./utils-CIbcUsZ5.mjs";
4
+ import { t as PACKAGE_VERSION } from "./version-BGWhjYBb.mjs";
3
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
4
6
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
5
7
  import { APIError as APIError$1 } from "better-call";
8
+ import { ASSERTION_SIGNING_ALGORITHMS } from "@better-auth/core/oauth2";
6
9
  import { isBrowserFetchRequest } from "@better-auth/core/utils/fetch-metadata";
7
10
  import { generateRandomString, makeSignature } from "better-auth/crypto";
8
11
  import { defineRequestState } from "@better-auth/core/context";
@@ -145,7 +148,7 @@ async function postLogin(ctx, opts) {
145
148
  });
146
149
  const query = new URLSearchParams(_query);
147
150
  ctx.headers?.set("accept", "application/json");
148
- ctx.query = Object.fromEntries(query);
151
+ ctx.query = searchParamsToQuery(query);
149
152
  const { url } = await authorizeEndpoint(ctx, opts, { postLogin: true });
150
153
  return {
151
154
  redirect: true,
@@ -483,13 +486,8 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
483
486
  * Obtains new Session Jwt and Refresh Tokens using a code
484
487
  */
485
488
  async function handleAuthorizationCodeGrant(ctx, opts) {
486
- let { client_id, client_secret, code, code_verifier, redirect_uri } = ctx.body;
487
- const authorization = ctx.request?.headers.get("authorization") || null;
488
- if (authorization?.startsWith("Basic ")) {
489
- const res = basicToClientCredentials(authorization);
490
- client_id = res?.client_id;
491
- client_secret = res?.client_secret;
492
- }
489
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
490
+ const { code, code_verifier, redirect_uri } = ctx.body;
493
491
  if (!client_id) throw new APIError("BAD_REQUEST", {
494
492
  error_description: "client_id is required",
495
493
  error: "invalid_request"
@@ -504,7 +502,7 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
504
502
  });
505
503
  const isAuthCodeWithSecret = client_id && client_secret;
506
504
  const isAuthCodeWithPkce = client_id && code && code_verifier;
507
- if (!isAuthCodeWithSecret && !isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
505
+ if (!isAuthCodeWithSecret && !isAuthCodeWithPkce && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
508
506
  error_description: "Either code_verifier or client_secret is required",
509
507
  error: "invalid_request"
510
508
  });
@@ -516,14 +514,14 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
516
514
  error: "invalid_scope"
517
515
  });
518
516
  /** Verify Client */
519
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes);
517
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, scopes, preVerifiedClient);
520
518
  if (isPKCERequired(client, (verificationValue.query?.scope)?.split(" ") || [])) {
521
519
  if (!isAuthCodeWithPkce) throw new APIError("BAD_REQUEST", {
522
520
  error_description: "PKCE is required for this client",
523
521
  error: "invalid_request"
524
522
  });
525
- } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret)) throw new APIError("BAD_REQUEST", {
526
- error_description: "Either PKCE (code_verifier) or client authentication (client_secret) is required",
523
+ } else if (!(isAuthCodeWithPkce || isAuthCodeWithSecret || preVerifiedClient)) throw new APIError("BAD_REQUEST", {
524
+ error_description: "Either PKCE (code_verifier) or client authentication (client_secret or client_assertion) is required",
527
525
  error: "invalid_request"
528
526
  });
529
527
  /** Check PKCE challenge if verifier is provided */
@@ -574,22 +572,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
574
572
  * MUST follow https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
575
573
  */
576
574
  async function handleClientCredentialsGrant(ctx, opts) {
577
- let { client_id, client_secret, scope } = ctx.body;
578
- const authorization = ctx.request?.headers.get("authorization") || null;
579
- if (authorization?.startsWith("Basic ")) {
580
- const res = basicToClientCredentials(authorization);
581
- client_id = res?.client_id;
582
- client_secret = res?.client_secret;
583
- }
575
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
576
+ const { scope } = ctx.body;
584
577
  if (!client_id) throw new APIError("BAD_REQUEST", {
585
578
  error_description: "Missing required client_id",
586
579
  error: "invalid_grant"
587
580
  });
588
- if (!client_secret) throw new APIError("BAD_REQUEST", {
581
+ if (!client_secret && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
589
582
  error_description: "Missing a required client_secret",
590
583
  error: "invalid_grant"
591
584
  });
592
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
585
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
593
586
  let requestedScopes = scope?.split(" ");
594
587
  if (requestedScopes) {
595
588
  const validScopes = new Set(client.scopes ?? opts.scopes);
@@ -653,13 +646,8 @@ async function handleClientCredentialsGrant(ctx, opts) {
653
646
  * To add scopes, you must restart the authorize process again.
654
647
  */
655
648
  async function handleRefreshTokenGrant(ctx, opts) {
656
- let { client_id, client_secret, refresh_token, scope } = ctx.body;
657
- const authorization = ctx.request?.headers.get("authorization") || null;
658
- if (authorization?.startsWith("Basic ")) {
659
- const res = basicToClientCredentials(authorization);
660
- client_id = res?.client_id;
661
- client_secret = res?.client_secret;
662
- }
649
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/token`));
650
+ const { refresh_token, scope } = ctx.body;
663
651
  if (!client_id) throw new APIError("BAD_REQUEST", {
664
652
  error_description: "Missing required client_id",
665
653
  error: "invalid_grant"
@@ -713,7 +701,7 @@ async function handleRefreshTokenGrant(ctx, opts) {
713
701
  error: "invalid_scope"
714
702
  });
715
703
  }
716
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes);
704
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, requestedScopes ?? scopes, preVerifiedClient);
717
705
  const user = await ctx.context.internalAdapter.findUserById(refreshToken.userId);
718
706
  if (!user) throw new APIError("BAD_REQUEST", {
719
707
  error_description: "user not found",
@@ -931,14 +919,9 @@ async function resolveIntrospectionSub(opts, payload, client) {
931
919
  return payload;
932
920
  }
933
921
  async function introspectEndpoint(ctx, opts) {
934
- let { client_id, client_secret, token, token_type_hint } = ctx.body;
935
- const authorization = ctx.request?.headers.get("authorization") || null;
936
- if (authorization?.startsWith("Basic ")) {
937
- const res = basicToClientCredentials(authorization);
938
- client_id = res?.client_id;
939
- client_secret = res?.client_secret;
940
- }
941
- if (!client_id || !client_secret) throw new APIError$1("UNAUTHORIZED", {
922
+ let { token, token_type_hint } = ctx.body;
923
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/introspect`));
924
+ if (!client_id || !client_secret && !preVerifiedClient) throw new APIError$1("UNAUTHORIZED", {
942
925
  error_description: "missing required credentials",
943
926
  error: "invalid_client"
944
927
  });
@@ -947,7 +930,7 @@ async function introspectEndpoint(ctx, opts) {
947
930
  error_description: "missing a required token for introspection",
948
931
  error: "invalid_request"
949
932
  });
950
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
933
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
951
934
  try {
952
935
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
953
936
  return resolveIntrospectionSub(opts, await validateAccessToken(ctx, opts, token, client.clientId), client);
@@ -1172,18 +1155,59 @@ async function checkOAuthClient(client, opts, settings) {
1172
1155
  error: "invalid_client_metadata",
1173
1156
  error_description: `pkce is required for registered clients.`
1174
1157
  });
1175
- if (settings?.isRegister && client.skip_consent) throw new APIError("BAD_REQUEST", {
1158
+ if (client.token_endpoint_auth_method === "private_key_jwt") {
1159
+ if (client.jwks && client.jwks_uri) throw new APIError("BAD_REQUEST", {
1160
+ error: "invalid_client_metadata",
1161
+ error_description: "jwks and jwks_uri are mutually exclusive"
1162
+ });
1163
+ if (!client.jwks && !client.jwks_uri) throw new APIError("BAD_REQUEST", {
1164
+ error: "invalid_client_metadata",
1165
+ error_description: "private_key_jwt requires either jwks or jwks_uri"
1166
+ });
1167
+ if (client.jwks_uri) try {
1168
+ const uri = new URL(client.jwks_uri);
1169
+ if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1170
+ error: "invalid_client_metadata",
1171
+ error_description: "jwks_uri must use HTTPS"
1172
+ });
1173
+ if (isPrivateHostname(uri.hostname)) throw new APIError("BAD_REQUEST", {
1174
+ error: "invalid_client_metadata",
1175
+ error_description: "jwks_uri must not point to a private or reserved address"
1176
+ });
1177
+ if (settings?.ctx && !settings.ctx.context.isTrustedOrigin(uri.href)) throw new APIError("BAD_REQUEST", {
1178
+ error: "invalid_client_metadata",
1179
+ error_description: "jwks_uri must belong to a trusted origin"
1180
+ });
1181
+ } catch (e) {
1182
+ if (e instanceof APIError) throw e;
1183
+ throw new APIError("BAD_REQUEST", {
1184
+ error: "invalid_client_metadata",
1185
+ error_description: "jwks_uri must be a valid URL"
1186
+ });
1187
+ }
1188
+ if (client.jwks) {
1189
+ const keys = Array.isArray(client.jwks) ? client.jwks : client.jwks.keys;
1190
+ if (!Array.isArray(keys) || keys.length === 0) throw new APIError("BAD_REQUEST", {
1191
+ error: "invalid_client_metadata",
1192
+ error_description: "jwks must be a non-empty array of JWK objects or a JWKS document {keys:[...]}"
1193
+ });
1194
+ }
1195
+ } else if (client.jwks || client.jwks_uri) throw new APIError("BAD_REQUEST", {
1176
1196
  error: "invalid_client_metadata",
1177
- error_description: "skip_consent cannot be set during dynamic client registration"
1197
+ error_description: "jwks and jwks_uri are only allowed with private_key_jwt authentication"
1178
1198
  });
1179
1199
  }
1180
1200
  async function createOAuthClientEndpoint(ctx, opts, settings) {
1181
1201
  const body = ctx.body;
1182
1202
  const session = await getSessionFromCtx(ctx);
1183
1203
  const isPublic = body.token_endpoint_auth_method === "none";
1184
- await checkOAuthClient(ctx.body, opts, settings);
1204
+ const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1205
+ await checkOAuthClient(ctx.body, opts, {
1206
+ ...settings,
1207
+ ctx
1208
+ });
1185
1209
  const clientId = opts.generateClientId?.() || generateRandomString(32, "a-z", "A-Z");
1186
- const clientSecret = isPublic ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1210
+ const clientSecret = isPublic || isPrivateKeyJwt ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1187
1211
  const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1188
1212
  const iat = Math.floor(Date.now() / 1e3);
1189
1213
  const referenceId = opts.clientReference ? await opts.clientReference({
@@ -1193,8 +1217,6 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1193
1217
  const schema = oauthToSchema({
1194
1218
  ...body ?? {},
1195
1219
  disabled: void 0,
1196
- jwks: void 0,
1197
- jwks_uri: void 0,
1198
1220
  client_secret_expires_at: storedClientSecret ? settings.isRegister && opts?.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1199
1221
  client_id: clientId,
1200
1222
  client_secret: storedClientSecret,
@@ -1229,7 +1251,7 @@ async function createOAuthClientEndpoint(ctx, opts, settings) {
1229
1251
  * @returns
1230
1252
  */
1231
1253
  function oauthToSchema(input) {
1232
- 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: _jwks, jwks_uri: _jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1254
+ 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, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, public: _public, type, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata, ...rest } = input;
1233
1255
  const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(_expiresAt * 1e3) : void 0;
1234
1256
  const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1235
1257
  const scopes = _scope?.split(" ");
@@ -1237,6 +1259,7 @@ function oauthToSchema(input) {
1237
1259
  ...rest && Object.keys(rest).length ? rest : {},
1238
1260
  ...inputMetadata && typeof inputMetadata === "object" ? inputMetadata : {}
1239
1261
  };
1262
+ const metadata = Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0;
1240
1263
  return {
1241
1264
  clientId,
1242
1265
  clientSecret,
@@ -1259,6 +1282,8 @@ function oauthToSchema(input) {
1259
1282
  tokenEndpointAuthMethod,
1260
1283
  grantTypes,
1261
1284
  responseTypes,
1285
+ jwks: inputJwks ? JSON.stringify({ keys: Array.isArray(inputJwks) ? inputJwks : inputJwks.keys }) : void 0,
1286
+ jwksUri,
1262
1287
  public: _public,
1263
1288
  type,
1264
1289
  skipConsent,
@@ -1266,7 +1291,7 @@ function oauthToSchema(input) {
1266
1291
  requirePKCE,
1267
1292
  subjectType,
1268
1293
  referenceId,
1269
- metadata: Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0
1294
+ metadata
1270
1295
  };
1271
1296
  }
1272
1297
  /**
@@ -1276,7 +1301,7 @@ function oauthToSchema(input) {
1276
1301
  * @returns
1277
1302
  */
1278
1303
  function schemaToOAuth(input) {
1279
- const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1304
+ const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, tokenEndpointAuthMethod, grantTypes, responseTypes, public: _public, type, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, subjectType, referenceId, metadata } = input;
1280
1305
  const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1281
1306
  const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1282
1307
  const _scopes = scopes?.join(" ");
@@ -1294,6 +1319,8 @@ function schemaToOAuth(input) {
1294
1319
  contacts: contacts ?? void 0,
1295
1320
  tos_uri: tos ?? void 0,
1296
1321
  policy_uri: policy ?? void 0,
1322
+ jwks: jwks ? JSON.parse(jwks).keys : void 0,
1323
+ jwks_uri: jwksUri ?? void 0,
1297
1324
  software_id: softwareId ?? void 0,
1298
1325
  software_version: softwareVersion ?? void 0,
1299
1326
  software_statement: softwareStatement ?? void 0,
@@ -1510,7 +1537,13 @@ async function updateClientEndpoint(ctx, opts) {
1510
1537
  await checkOAuthClient({
1511
1538
  ...schemaToOAuth(client),
1512
1539
  ...updates
1513
- }, opts);
1540
+ }, opts, { ctx });
1541
+ const schemaUpdates = { ...oauthToSchema(updates) };
1542
+ if (updates.token_endpoint_auth_method) if (updates.token_endpoint_auth_method === "private_key_jwt") schemaUpdates.clientSecret = null;
1543
+ else {
1544
+ schemaUpdates.jwks = null;
1545
+ schemaUpdates.jwksUri = null;
1546
+ }
1514
1547
  const updatedClient = await ctx.context.adapter.update({
1515
1548
  model: "oauthClient",
1516
1549
  where: [{
@@ -1518,7 +1551,7 @@ async function updateClientEndpoint(ctx, opts) {
1518
1551
  value: clientId
1519
1552
  }],
1520
1553
  update: {
1521
- ...oauthToSchema(updates),
1554
+ ...schemaUpdates,
1522
1555
  updatedAt: /* @__PURE__ */ new Date(Math.floor(Date.now() / 1e3) * 1e3)
1523
1556
  }
1524
1557
  });
@@ -1556,7 +1589,7 @@ async function rotateClientSecretEndpoint(ctx, opts) {
1556
1589
  if (client.referenceId !== await opts.clientReference(session)) throw new APIError("UNAUTHORIZED");
1557
1590
  } else throw new APIError("UNAUTHORIZED");
1558
1591
  if (client.public || !client.clientSecret) throw new APIError("BAD_REQUEST", {
1559
- error_description: "public clients cannot be updated",
1592
+ error_description: "secret rotation is only available for clients using client_secret authentication",
1560
1593
  error: "invalid_client"
1561
1594
  });
1562
1595
  const clientSecret = opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
@@ -1601,8 +1634,11 @@ const adminCreateOAuthClient = (opts) => createAuthEndpoint("/admin/oauth2/creat
1601
1634
  token_endpoint_auth_method: z.enum([
1602
1635
  "none",
1603
1636
  "client_secret_basic",
1604
- "client_secret_post"
1637
+ "client_secret_post",
1638
+ "private_key_jwt"
1605
1639
  ]).default("client_secret_basic").optional(),
1640
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
1641
+ jwks_uri: z.string().optional(),
1606
1642
  grant_types: z.array(z.enum([
1607
1643
  "authorization_code",
1608
1644
  "client_credentials",
@@ -1784,8 +1820,11 @@ const createOAuthClient = (opts) => createAuthEndpoint("/oauth2/create-client",
1784
1820
  token_endpoint_auth_method: z.enum([
1785
1821
  "none",
1786
1822
  "client_secret_basic",
1787
- "client_secret_post"
1823
+ "client_secret_post",
1824
+ "private_key_jwt"
1788
1825
  ]).default("client_secret_basic").optional(),
1826
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
1827
+ jwks_uri: z.string().optional(),
1789
1828
  grant_types: z.array(z.enum([
1790
1829
  "authorization_code",
1791
1830
  "client_credentials",
@@ -2343,13 +2382,8 @@ async function revokeAccessToken(ctx, opts, clientId, token) {
2343
2382
  });
2344
2383
  }
2345
2384
  async function revokeEndpoint(ctx, opts) {
2346
- let { client_id, client_secret, token, token_type_hint } = ctx.body;
2347
- const authorization = ctx.request?.headers.get("authorization") || null;
2348
- if (authorization?.startsWith("Basic ")) {
2349
- const res = basicToClientCredentials(authorization);
2350
- client_id = res?.client_id;
2351
- client_secret = res?.client_secret;
2352
- }
2385
+ let { token, token_type_hint } = ctx.body;
2386
+ const { clientId: client_id, clientSecret: client_secret, preVerifiedClient } = destructureCredentials(await extractClientCredentials(ctx, opts, `${ctx.context.baseURL}/oauth2/revoke`));
2353
2387
  if (!client_id) throw new APIError$1("UNAUTHORIZED", {
2354
2388
  error_description: "missing required credentials",
2355
2389
  error: "invalid_client"
@@ -2359,7 +2393,7 @@ async function revokeEndpoint(ctx, opts) {
2359
2393
  error_description: "missing a required token for introspection",
2360
2394
  error: "invalid_request"
2361
2395
  });
2362
- const client = await validateClientCredentials(ctx, opts, client_id, client_secret);
2396
+ const client = await validateClientCredentials(ctx, opts, client_id, client_secret, void 0, preVerifiedClient);
2363
2397
  try {
2364
2398
  if (token_type_hint === void 0 || token_type_hint === "access_token") try {
2365
2399
  return await revokeAccessToken(ctx, opts, client.clientId, token);
@@ -2494,6 +2528,14 @@ const schema = {
2494
2528
  type: "string",
2495
2529
  required: false
2496
2530
  },
2531
+ jwks: {
2532
+ type: "string",
2533
+ required: false
2534
+ },
2535
+ jwksUri: {
2536
+ type: "string",
2537
+ required: false
2538
+ },
2497
2539
  grantTypes: {
2498
2540
  type: "string[]",
2499
2541
  required: false
@@ -2996,6 +3038,8 @@ const oauthProvider = (options) => {
2996
3038
  ]),
2997
3039
  client_id: z.string().optional(),
2998
3040
  client_secret: z.string().optional(),
3041
+ client_assertion: z.string().optional(),
3042
+ client_assertion_type: z.string().optional(),
2999
3043
  code: z.string().optional(),
3000
3044
  code_verifier: z.string().optional(),
3001
3045
  redirect_uri: SafeUrlSchema.optional(),
@@ -3120,6 +3164,8 @@ const oauthProvider = (options) => {
3120
3164
  body: z.object({
3121
3165
  client_id: z.string().optional(),
3122
3166
  client_secret: z.string().optional(),
3167
+ client_assertion: z.string().optional(),
3168
+ client_assertion_type: z.string().optional(),
3123
3169
  token: z.string(),
3124
3170
  token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3125
3171
  }),
@@ -3238,6 +3284,8 @@ const oauthProvider = (options) => {
3238
3284
  body: z.object({
3239
3285
  client_id: z.string().optional(),
3240
3286
  client_secret: z.string().optional(),
3287
+ client_assertion: z.string().optional(),
3288
+ client_assertion_type: z.string().optional(),
3241
3289
  token: z.string(),
3242
3290
  token_type_hint: z.enum(["access_token", "refresh_token"]).optional()
3243
3291
  }),
@@ -3435,8 +3483,11 @@ const oauthProvider = (options) => {
3435
3483
  token_endpoint_auth_method: z.enum([
3436
3484
  "none",
3437
3485
  "client_secret_basic",
3438
- "client_secret_post"
3486
+ "client_secret_post",
3487
+ "private_key_jwt"
3439
3488
  ]).default("client_secret_basic").optional(),
3489
+ jwks: z.union([z.array(z.record(z.string(), z.unknown())), z.object({ keys: z.array(z.record(z.string(), z.unknown())) })]).optional(),
3490
+ jwks_uri: z.string().optional(),
3440
3491
  grant_types: z.array(z.enum([
3441
3492
  "authorization_code",
3442
3493
  "client_credentials",
@@ -3449,7 +3500,7 @@ const oauthProvider = (options) => {
3449
3500
  "user-agent-based"
3450
3501
  ]).optional(),
3451
3502
  subject_type: z.enum(["public", "pairwise"]).optional(),
3452
- skip_consent: z.boolean().optional()
3503
+ skip_consent: z.never({ error: "skip_consent cannot be set during dynamic client registration" }).optional()
3453
3504
  }),
3454
3505
  metadata: { openapi: {
3455
3506
  description: "Register an OAuth2 application",
@@ -3541,7 +3592,8 @@ const oauthProvider = (options) => {
3541
3592
  enum: [
3542
3593
  "none",
3543
3594
  "client_secret_basic",
3544
- "client_secret_post"
3595
+ "client_secret_post",
3596
+ "private_key_jwt"
3545
3597
  ]
3546
3598
  },
3547
3599
  grant_types: {
@@ -3850,7 +3902,11 @@ async function authorizeEndpoint(ctx, opts, settings) {
3850
3902
  }
3851
3903
  function serializeAuthorizationQuery(query) {
3852
3904
  const params = new URLSearchParams();
3853
- for (const [key, value] of Object.entries(query)) if (value != null) params.set(key, String(value));
3905
+ for (const [key, value] of Object.entries(query)) {
3906
+ if (value == null) continue;
3907
+ if (Array.isArray(value)) for (const v of value) params.append(key, String(v));
3908
+ else params.set(key, String(value));
3909
+ }
3854
3910
  return params;
3855
3911
  }
3856
3912
  async function redirectWithAuthorizationCode(ctx, opts, verificationValue) {
@@ -3922,10 +3978,22 @@ function authServerMetadata(ctx, opts, overrides) {
3922
3978
  token_endpoint_auth_methods_supported: [
3923
3979
  ...overrides?.public_client_supported ? ["none"] : [],
3924
3980
  "client_secret_basic",
3925
- "client_secret_post"
3981
+ "client_secret_post",
3982
+ "private_key_jwt"
3983
+ ],
3984
+ token_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
3985
+ introspection_endpoint_auth_methods_supported: [
3986
+ "client_secret_basic",
3987
+ "client_secret_post",
3988
+ "private_key_jwt"
3989
+ ],
3990
+ introspection_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
3991
+ revocation_endpoint_auth_methods_supported: [
3992
+ "client_secret_basic",
3993
+ "client_secret_post",
3994
+ "private_key_jwt"
3926
3995
  ],
3927
- introspection_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
3928
- revocation_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post"],
3996
+ revocation_endpoint_auth_signing_alg_values_supported: [...ASSERTION_SIGNING_ALGORITHMS],
3929
3997
  code_challenge_methods_supported: ["S256"],
3930
3998
  authorization_response_iss_parameter_supported: true
3931
3999
  };
@@ -0,0 +1,56 @@
1
+ import { isAPIError } from "better-auth/api";
2
+ import { verifyAccessToken } from "better-auth/oauth2";
3
+ import { APIError as APIError$1 } from "better-call";
4
+ //#region src/mcp.ts
5
+ /**
6
+ * A request middleware handler that checks and responds with
7
+ * a WWW-Authenticate header for unauthenticated responses.
8
+ *
9
+ * @external
10
+ */
11
+ const mcpHandler = (verifyOptions, handler, opts) => {
12
+ return async (req) => {
13
+ const authorization = req.headers?.get("authorization") ?? void 0;
14
+ const accessToken = authorization?.startsWith("Bearer ") ? authorization.replace("Bearer ", "") : authorization;
15
+ try {
16
+ if (!accessToken?.length) throw new APIError$1("UNAUTHORIZED", { message: "missing authorization header" });
17
+ return handler(req, await verifyAccessToken(accessToken, verifyOptions));
18
+ } catch (error) {
19
+ try {
20
+ handleMcpErrors(error, verifyOptions.verifyOptions.audience, opts);
21
+ } catch (err) {
22
+ if (err instanceof APIError$1) return new Response(err.message, {
23
+ ...err,
24
+ status: err.statusCode
25
+ });
26
+ throw new Error(String(err));
27
+ }
28
+ throw new Error(String(error));
29
+ }
30
+ };
31
+ };
32
+ /**
33
+ * The following handles all MCP errors and API errors
34
+ *
35
+ * @internal
36
+ */
37
+ function handleMcpErrors(error, resource, opts) {
38
+ if (isAPIError(error) && error.status === "UNAUTHORIZED") {
39
+ const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((v) => {
40
+ let audiencePath;
41
+ if (URL.canParse?.(v)) {
42
+ const url = new URL(v);
43
+ audiencePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
44
+ return `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${audiencePath}"`;
45
+ } else {
46
+ const resourceMetadata = opts?.resourceMetadataMappings?.[v];
47
+ if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${v}` });
48
+ return `Bearer resource_metadata=${resourceMetadata}`;
49
+ }
50
+ }).join(", ");
51
+ throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
52
+ } else if (error instanceof Error) throw error;
53
+ else throw new Error(error);
54
+ }
55
+ //#endregion
56
+ export { mcpHandler as n, handleMcpErrors as t };
@@ -1,3 +1,4 @@
1
+ import { AssertionSigningAlgorithm } from "@better-auth/core/oauth2";
1
2
  import { JWSAlgorithms } from "better-auth/plugins";
2
3
  import { JWTPayload } from "jose";
3
4
  import { InferOptionSchema, Session, User } from "better-auth/types";
@@ -102,6 +103,14 @@ declare const schema: {
102
103
  type: "string";
103
104
  required: false;
104
105
  };
106
+ jwks: {
107
+ type: "string";
108
+ required: false;
109
+ };
110
+ jwksUri: {
111
+ type: "string";
112
+ required: false;
113
+ };
105
114
  grantTypes: {
106
115
  type: "string[]";
107
116
  required: false;
@@ -388,6 +397,13 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
388
397
  * { "write:payments": "5m", "read:payments": "30m" }
389
398
  */
390
399
  scopeExpirations?: { [K in Scopes[number]]?: number | string | Date };
400
+ /**
401
+ * Maximum lifetime in seconds for client assertion JWTs
402
+ * used with `private_key_jwt` authentication.
403
+ *
404
+ * @default 300 (5 minutes)
405
+ */
406
+ assertionMaxLifetime?: number;
391
407
  /**
392
408
  * Allows /oauth2/public-client-prelogin endpoint to be
393
409
  * requestable prior to login via a valid oauth_query.
@@ -1158,9 +1174,13 @@ interface SchemaClient<Scopes extends readonly Scope[] = InternallySupportedScop
1158
1174
  * For example, `https://example.com/logout/callback`
1159
1175
  */
1160
1176
  postLogoutRedirectUris?: string[];
1161
- tokenEndpointAuthMethod?: "none" | "client_secret_basic" | "client_secret_post";
1177
+ tokenEndpointAuthMethod?: "none" | "client_secret_basic" | "client_secret_post" | "private_key_jwt";
1162
1178
  grantTypes?: GrantType[];
1163
1179
  responseTypes?: "code"[];
1180
+ /** Client's JSON Web Key Set for `private_key_jwt` authentication. Mutually exclusive with `jwksUri`. */
1181
+ jwks?: string;
1182
+ /** URI for the client's JSON Web Key Set. Mutually exclusive with `jwks`. Must be HTTPS. */
1183
+ jwksUri?: string;
1164
1184
  /**
1165
1185
  * Indicates whether the client is public or confidential.
1166
1186
  * If public, refreshing tokens doesn't require
@@ -1297,7 +1317,7 @@ type OAuthConsent<Scopes extends readonly Scope[] = InternallySupportedScopes[]>
1297
1317
  * Supported grant types of the token endpoint
1298
1318
  */
1299
1319
  type GrantType = "authorization_code" | "client_credentials" | "refresh_token";
1300
- type AuthMethod = "client_secret_basic" | "client_secret_post";
1320
+ type AuthMethod = "client_secret_basic" | "client_secret_post" | "private_key_jwt";
1301
1321
  type TokenEndpointAuthMethod = AuthMethod | "none";
1302
1322
  type BearerMethodsSupported = "header" | "body";
1303
1323
  /**
@@ -1374,7 +1394,7 @@ interface AuthServerMetadata {
1374
1394
  * token endpoint for the "private_key_jwt" and "client_secret_jwt"
1375
1395
  * authentication methods (see field token_endpoint_auth_methods_supported).
1376
1396
  */
1377
- token_endpoint_auth_signing_alg_values_supported?: JWSAlgorithms[];
1397
+ token_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
1378
1398
  /**
1379
1399
  * URL of a page containing human-readable information
1380
1400
  * that developers might want or need to know when using the
@@ -1420,7 +1440,7 @@ interface AuthServerMetadata {
1420
1440
  * token endpoint for the "private_key_jwt" and "client_secret_jwt"
1421
1441
  * authentication methods (see field revocation_endpoint_auth_methods_supported).
1422
1442
  */
1423
- revocation_endpoint_auth_signing_alg_values_supported?: JWSAlgorithms[];
1443
+ revocation_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
1424
1444
  /**
1425
1445
  * URL of the authorization server's OAuth 2.0
1426
1446
  * introspection endpoint [RFC7662](https://datatracker.ietf.org/doc/html/rfc7662)
@@ -1441,7 +1461,7 @@ interface AuthServerMetadata {
1441
1461
  * the "private_key_jwt" and "client_secret_jwt" authentication methods
1442
1462
  * (see field introspection_endpoint_auth_methods_supported).
1443
1463
  */
1444
- introspection_endpoint_auth_signing_alg_values_supported?: JWSAlgorithms[];
1464
+ introspection_endpoint_auth_signing_alg_values_supported?: AssertionSigningAlgorithm[];
1445
1465
  /**
1446
1466
  * Supported code challenge methods.
1447
1467
  *
@@ -1548,14 +1568,17 @@ interface OAuthClient {
1548
1568
  contacts?: string[];
1549
1569
  tos_uri?: string;
1550
1570
  policy_uri?: string;
1551
- jwks?: string[];
1571
+ /** JWK Set — accepts either a bare key array or an RFC 7517 JWKS object `{"keys":[...]}` */
1572
+ jwks?: Record<string, unknown>[] | {
1573
+ keys: Record<string, unknown>[];
1574
+ };
1552
1575
  jwks_uri?: string;
1553
1576
  software_id?: string;
1554
1577
  software_version?: string;
1555
1578
  software_statement?: string;
1556
1579
  redirect_uris: string[];
1557
1580
  post_logout_redirect_uris?: string[];
1558
- token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post";
1581
+ token_endpoint_auth_method?: "none" | "client_secret_basic" | "client_secret_post" | "private_key_jwt";
1559
1582
  grant_types?: GrantType[];
1560
1583
  response_types?: "code"[];
1561
1584
  public?: boolean;
@@ -1,4 +1,4 @@
1
- import { c as OAuthConsent, i as OIDCMetadata, m as Scope, r as OAuthClient, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-Cc0nzj5Q.mjs";
1
+ import { c as OAuthConsent, i as OIDCMetadata, m as Scope, r as OAuthClient, t as AuthServerMetadata, u as OAuthOptions } from "./oauth-C8aTlaAC.mjs";
2
2
  import * as better_call0 from "better-call";
3
3
  import * as z from "zod";
4
4
  import * as better_auth_plugins0 from "better-auth/plugins";
@@ -298,6 +298,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
298
298
  }>;
299
299
  client_id: z.ZodOptional<z.ZodString>;
300
300
  client_secret: z.ZodOptional<z.ZodString>;
301
+ client_assertion: z.ZodOptional<z.ZodString>;
302
+ client_assertion_type: z.ZodOptional<z.ZodString>;
301
303
  code: z.ZodOptional<z.ZodString>;
302
304
  code_verifier: z.ZodOptional<z.ZodString>;
303
305
  redirect_uri: z.ZodOptional<z.ZodURL>;
@@ -436,6 +438,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
436
438
  body: z.ZodObject<{
437
439
  client_id: z.ZodOptional<z.ZodString>;
438
440
  client_secret: z.ZodOptional<z.ZodString>;
441
+ client_assertion: z.ZodOptional<z.ZodString>;
442
+ client_assertion_type: z.ZodOptional<z.ZodString>;
439
443
  token: z.ZodString;
440
444
  token_type_hint: z.ZodOptional<z.ZodEnum<{
441
445
  refresh_token: "refresh_token";
@@ -573,6 +577,8 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
573
577
  body: z.ZodObject<{
574
578
  client_id: z.ZodOptional<z.ZodString>;
575
579
  client_secret: z.ZodOptional<z.ZodString>;
580
+ client_assertion: z.ZodOptional<z.ZodString>;
581
+ client_assertion_type: z.ZodOptional<z.ZodString>;
576
582
  token: z.ZodString;
577
583
  token_type_hint: z.ZodOptional<z.ZodEnum<{
578
584
  refresh_token: "refresh_token";
@@ -831,7 +837,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
831
837
  none: "none";
832
838
  client_secret_basic: "client_secret_basic";
833
839
  client_secret_post: "client_secret_post";
840
+ private_key_jwt: "private_key_jwt";
834
841
  }>>>;
842
+ jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
843
+ keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
844
+ }, z.core.$strip>]>>;
845
+ jwks_uri: z.ZodOptional<z.ZodString>;
835
846
  grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
836
847
  authorization_code: "authorization_code";
837
848
  client_credentials: "client_credentials";
@@ -849,7 +860,7 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
849
860
  public: "public";
850
861
  pairwise: "pairwise";
851
862
  }>>;
852
- skip_consent: z.ZodOptional<z.ZodBoolean>;
863
+ skip_consent: z.ZodOptional<z.ZodNever>;
853
864
  }, z.core.$strip>;
854
865
  metadata: {
855
866
  openapi: {
@@ -1004,7 +1015,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
1004
1015
  none: "none";
1005
1016
  client_secret_basic: "client_secret_basic";
1006
1017
  client_secret_post: "client_secret_post";
1018
+ private_key_jwt: "private_key_jwt";
1007
1019
  }>>>;
1020
+ jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
1021
+ keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1022
+ }, z.core.$strip>]>>;
1023
+ jwks_uri: z.ZodOptional<z.ZodString>;
1008
1024
  grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
1009
1025
  authorization_code: "authorization_code";
1010
1026
  client_credentials: "client_credentials";
@@ -1208,7 +1224,12 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
1208
1224
  none: "none";
1209
1225
  client_secret_basic: "client_secret_basic";
1210
1226
  client_secret_post: "client_secret_post";
1227
+ private_key_jwt: "private_key_jwt";
1211
1228
  }>>>;
1229
+ jwks: z.ZodOptional<z.ZodUnion<readonly [z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>, z.ZodObject<{
1230
+ keys: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1231
+ }, z.core.$strip>]>>;
1232
+ jwks_uri: z.ZodOptional<z.ZodString>;
1212
1233
  grant_types: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodEnum<{
1213
1234
  authorization_code: "authorization_code";
1214
1235
  client_credentials: "client_credentials";
@@ -1874,6 +1895,14 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
1874
1895
  type: "string";
1875
1896
  required: false;
1876
1897
  };
1898
+ jwks: {
1899
+ type: "string";
1900
+ required: false;
1901
+ };
1902
+ jwksUri: {
1903
+ type: "string";
1904
+ required: false;
1905
+ };
1877
1906
  grantTypes: {
1878
1907
  type: "string[]";
1879
1908
  required: false;
@@ -1,62 +1,8 @@
1
- import { isAPIError } from "better-auth/api";
2
- import { verifyAccessToken } from "better-auth/oauth2";
3
- import { APIError as APIError$1 } from "better-call";
1
+ import { APIError } from "better-call";
4
2
  import { constantTimeEqual, makeSignature, symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
5
3
  import { BetterAuthError } from "@better-auth/core/error";
6
4
  import { base64, base64Url } from "@better-auth/utils/base64";
7
5
  import { createHash } from "@better-auth/utils/hash";
8
- //#region src/mcp.ts
9
- /**
10
- * A request middleware handler that checks and responds with
11
- * a WWW-Authenticate header for unauthenticated responses.
12
- *
13
- * @external
14
- */
15
- const mcpHandler = (verifyOptions, handler, opts) => {
16
- return async (req) => {
17
- const authorization = req.headers?.get("authorization") ?? void 0;
18
- const accessToken = authorization?.startsWith("Bearer ") ? authorization.replace("Bearer ", "") : authorization;
19
- try {
20
- if (!accessToken?.length) throw new APIError$1("UNAUTHORIZED", { message: "missing authorization header" });
21
- return handler(req, await verifyAccessToken(accessToken, verifyOptions));
22
- } catch (error) {
23
- try {
24
- handleMcpErrors(error, verifyOptions.verifyOptions.audience, opts);
25
- } catch (err) {
26
- if (err instanceof APIError$1) return new Response(err.message, {
27
- ...err,
28
- status: err.statusCode
29
- });
30
- throw new Error(String(err));
31
- }
32
- throw new Error(String(error));
33
- }
34
- };
35
- };
36
- /**
37
- * The following handles all MCP errors and API errors
38
- *
39
- * @internal
40
- */
41
- function handleMcpErrors(error, resource, opts) {
42
- if (isAPIError(error) && error.status === "UNAUTHORIZED") {
43
- const wwwAuthenticateValue = (Array.isArray(resource) ? resource : [resource]).map((v) => {
44
- let audiencePath;
45
- if (URL.canParse?.(v)) {
46
- const url = new URL(v);
47
- audiencePath = url.pathname.endsWith("/") ? url.pathname.slice(0, -1) : url.pathname;
48
- return `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource${audiencePath}"`;
49
- } else {
50
- const resourceMetadata = opts?.resourceMetadataMappings?.[v];
51
- if (!resourceMetadata) throw new APIError$1("INTERNAL_SERVER_ERROR", { message: `missing resource_metadata mapping for ${v}` });
52
- return `Bearer resource_metadata=${resourceMetadata}`;
53
- }
54
- }).join(", ");
55
- throw new APIError$1("UNAUTHORIZED", { message: error.message }, { "WWW-Authenticate": wwwAuthenticateValue });
56
- } else if (error instanceof Error) throw error;
57
- else throw new Error(error);
58
- }
59
- //#endregion
60
6
  //#region src/utils/index.ts
61
7
  var TTLCache = class {
62
8
  cache = /* @__PURE__ */ new Map();
@@ -183,7 +129,7 @@ async function decryptStoredClientSecret(ctx, storageMethod, storedClientSecret)
183
129
  async function verifyStoredClientSecret(ctx, opts, storedClientSecret, clientSecret) {
184
130
  const storageMethod = opts.storeClientSecret ?? (opts.disableJwtPlugin ? "encrypted" : "hashed");
185
131
  if (clientSecret && opts.prefix?.clientSecret) if (clientSecret.startsWith(opts.prefix?.clientSecret)) clientSecret = clientSecret.replace(opts.prefix.clientSecret, "");
186
- else throw new APIError$1("UNAUTHORIZED", {
132
+ else throw new APIError("UNAUTHORIZED", {
187
133
  error_description: "invalid client_secret",
188
134
  error: "invalid_client"
189
135
  });
@@ -254,12 +200,12 @@ function basicToClientCredentials(authorization) {
254
200
  if (authorization.startsWith("Basic ")) {
255
201
  const encoded = authorization.replace("Basic ", "");
256
202
  const decoded = new TextDecoder().decode(base64.decode(encoded));
257
- if (!decoded.includes(":")) throw new APIError$1("BAD_REQUEST", {
203
+ if (!decoded.includes(":")) throw new APIError("BAD_REQUEST", {
258
204
  error_description: "invalid authorization header format",
259
205
  error: "invalid_client"
260
206
  });
261
207
  const [id, secret] = decoded.split(":", 2);
262
- if (!id || !secret) throw new APIError$1("BAD_REQUEST", {
208
+ if (!id || !secret) throw new APIError("BAD_REQUEST", {
263
209
  error_description: "invalid authorization header format",
264
210
  error: "invalid_client"
265
211
  });
@@ -275,31 +221,37 @@ function basicToClientCredentials(authorization) {
275
221
  *
276
222
  * @internal
277
223
  */
278
- async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes) {
279
- const client = await getClient(ctx, options, clientId);
280
- if (!client) throw new APIError$1("BAD_REQUEST", {
224
+ async function validateClientCredentials(ctx, options, clientId, clientSecret, scopes, preVerifiedClient) {
225
+ const client = preVerifiedClient ?? await getClient(ctx, options, clientId);
226
+ if (!client) throw new APIError("BAD_REQUEST", {
281
227
  error_description: "missing client",
282
228
  error: "invalid_client"
283
229
  });
284
- if (client.disabled) throw new APIError$1("BAD_REQUEST", {
230
+ if (client.disabled) throw new APIError("BAD_REQUEST", {
285
231
  error_description: "client is disabled",
286
232
  error: "invalid_client"
287
233
  });
288
- if (!client.public && !clientSecret) throw new APIError$1("BAD_REQUEST", {
289
- error_description: "client secret must be provided",
290
- error: "invalid_client"
291
- });
292
- if (clientSecret && !client.clientSecret) throw new APIError$1("BAD_REQUEST", {
293
- error_description: "public client, client secret should not be received",
294
- error: "invalid_client"
295
- });
296
- if (clientSecret && !await verifyStoredClientSecret(ctx, options, client.clientSecret, clientSecret)) throw new APIError$1("UNAUTHORIZED", {
297
- error_description: "invalid client_secret",
234
+ if (client.tokenEndpointAuthMethod === "private_key_jwt" && !preVerifiedClient) throw new APIError("BAD_REQUEST", {
235
+ error_description: "client registered for private_key_jwt must use client_assertion",
298
236
  error: "invalid_client"
299
237
  });
238
+ if (!preVerifiedClient) {
239
+ if (!client.public && !clientSecret) throw new APIError("BAD_REQUEST", {
240
+ error_description: "client secret must be provided",
241
+ error: "invalid_client"
242
+ });
243
+ if (clientSecret && !client.clientSecret) throw new APIError("BAD_REQUEST", {
244
+ error_description: "public client, client secret should not be received",
245
+ error: "invalid_client"
246
+ });
247
+ if (clientSecret && !await verifyStoredClientSecret(ctx, options, client.clientSecret, clientSecret)) throw new APIError("UNAUTHORIZED", {
248
+ error_description: "invalid client_secret",
249
+ error: "invalid_client"
250
+ });
251
+ }
300
252
  if (scopes && client.scopes) {
301
253
  const validScopes = new Set(client.scopes);
302
- for (const sc of scopes) if (!validScopes.has(sc)) throw new APIError$1("BAD_REQUEST", {
254
+ for (const sc of scopes) if (!validScopes.has(sc)) throw new APIError("BAD_REQUEST", {
303
255
  error_description: `client does not allow scope ${sc}`,
304
256
  error: "invalid_scope"
305
257
  });
@@ -316,6 +268,57 @@ function parseClientMetadata(metadata) {
316
268
  if (!metadata) return void 0;
317
269
  return typeof metadata === "string" ? JSON.parse(metadata) : metadata;
318
270
  }
271
+ /** Unwraps ExtractedCredentials into the fields each grant handler needs. */
272
+ function destructureCredentials(credentials) {
273
+ return {
274
+ clientId: credentials?.clientId,
275
+ clientSecret: credentials?.method === "client_secret_basic" || credentials?.method === "client_secret_post" ? credentials.clientSecret : void 0,
276
+ preVerifiedClient: credentials?.method === "private_key_jwt" ? credentials.client : void 0
277
+ };
278
+ }
279
+ /**
280
+ * Extracts and resolves client credentials from the request.
281
+ * Supports: client_secret_basic, client_secret_post, private_key_jwt, and none (public).
282
+ */
283
+ async function extractClientCredentials(ctx, opts, expectedAudience) {
284
+ const body = ctx.body ?? {};
285
+ const authorization = ctx.request?.headers.get("authorization") ?? void 0;
286
+ if (body.client_assertion_type || body.client_assertion) {
287
+ if (!body.client_assertion || !body.client_assertion_type) throw new APIError("BAD_REQUEST", {
288
+ error_description: "client_assertion and client_assertion_type must both be provided",
289
+ error: "invalid_client"
290
+ });
291
+ if (body.client_secret || authorization?.startsWith("Basic ")) throw new APIError("BAD_REQUEST", {
292
+ error_description: "client_assertion cannot be combined with client_secret or Basic auth",
293
+ error: "invalid_client"
294
+ });
295
+ const { verifyClientAssertion: verify } = await import("./client-assertion-DZqo-L5j.mjs").then((n) => n.t);
296
+ const result = await verify(ctx, opts, body.client_assertion, body.client_assertion_type, body.client_id, expectedAudience);
297
+ return {
298
+ method: "private_key_jwt",
299
+ clientId: result.clientId,
300
+ client: result.client
301
+ };
302
+ }
303
+ if (authorization?.startsWith("Basic ")) {
304
+ const res = basicToClientCredentials(authorization);
305
+ if (res) return {
306
+ method: "client_secret_basic",
307
+ clientId: res.client_id,
308
+ clientSecret: res.client_secret
309
+ };
310
+ }
311
+ if (body.client_id && body.client_secret) return {
312
+ method: "client_secret_post",
313
+ clientId: body.client_id,
314
+ clientSecret: body.client_secret
315
+ };
316
+ if (body.client_id) return {
317
+ method: "none",
318
+ clientId: body.client_id
319
+ };
320
+ return null;
321
+ }
319
322
  /**
320
323
  * Parse space-separated prompt string into a set of prompts
321
324
  *
@@ -358,6 +361,18 @@ async function resolveSubjectIdentifier(userId, client, opts) {
358
361
  return userId;
359
362
  }
360
363
  /**
364
+ * Converts URLSearchParams to a plain object, preserving
365
+ * multi-valued keys as arrays instead of discarding duplicates.
366
+ */
367
+ function searchParamsToQuery(params) {
368
+ const result = Object.create(null);
369
+ for (const key of new Set(params.keys())) {
370
+ const values = params.getAll(key);
371
+ result[key] = values.length === 1 ? values[0] : values;
372
+ }
373
+ return result;
374
+ }
375
+ /**
361
376
  * Deletes a prompt value
362
377
  *
363
378
  * @param ctx
@@ -370,7 +385,7 @@ function deleteFromPrompt(query, prompt) {
370
385
  prompts?.splice(foundPrompt, 1);
371
386
  prompts?.length ? query.set("prompt", prompts.join(" ")) : query.delete("prompt");
372
387
  }
373
- return Object.fromEntries(query);
388
+ return searchParamsToQuery(query);
374
389
  }
375
390
  var PKCERequirementErrors = /* @__PURE__ */ function(PKCERequirementErrors) {
376
391
  PKCERequirementErrors["PUBLIC_CLIENT"] = "pkce is required for public clients";
@@ -399,4 +414,4 @@ function isPKCERequired(client, requestedScopes) {
399
414
  return false;
400
415
  }
401
416
  //#endregion
402
- export { verifyOAuthQueryParams as _, getJwtPlugin as a, isPKCERequired as c, parsePrompt as d, resolveSessionAuthTime as f, validateClientCredentials as g, storeToken as h, getClient as i, normalizeTimestampValue as l, storeClientSecret as m, decryptStoredClientSecret as n, getOAuthProviderPlugin as o, resolveSubjectIdentifier as p, deleteFromPrompt as r, getStoredToken as s, basicToClientCredentials as t, parseClientMetadata as u, handleMcpErrors as v, mcpHandler as y };
417
+ export { storeToken as _, getClient as a, getStoredToken as c, parseClientMetadata as d, parsePrompt as f, storeClientSecret as g, searchParamsToQuery as h, extractClientCredentials as i, isPKCERequired as l, resolveSubjectIdentifier as m, deleteFromPrompt as n, getJwtPlugin as o, resolveSessionAuthTime as p, destructureCredentials as r, getOAuthProviderPlugin as s, decryptStoredClientSecret as t, normalizeTimestampValue as u, validateClientCredentials as v, verifyOAuthQueryParams as y };
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.6.1";
3
+ const PACKAGE_VERSION = "1.7.0-beta.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.6.1",
3
+ "version": "1.7.0-beta.0",
4
4
  "description": "An oauth provider plugin for Better Auth",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,15 +64,15 @@
64
64
  "@modelcontextprotocol/sdk": "^1.27.1",
65
65
  "listhen": "^1.9.0",
66
66
  "tsdown": "0.21.1",
67
- "better-auth": "1.6.1",
68
- "@better-auth/core": "1.6.1"
67
+ "@better-auth/core": "1.7.0-beta.0",
68
+ "better-auth": "1.7.0-beta.0"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "@better-auth/utils": "0.4.0",
72
72
  "@better-fetch/fetch": "1.1.21",
73
73
  "better-call": "1.3.5",
74
- "@better-auth/core": "^1.6.1",
75
- "better-auth": "^1.6.1"
74
+ "@better-auth/core": "^1.7.0-beta.0",
75
+ "better-auth": "^1.7.0-beta.0"
76
76
  },
77
77
  "scripts": {
78
78
  "build": "tsdown",