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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { a as getClient } from "./utils-CIbcUsZ5.mjs";
1
+ import { a as getClient } from "./utils-Cx_XnD9i.mjs";
2
2
  import { APIError } from "better-call";
3
3
  import { ASSERTION_SIGNING_ALGORITHMS, CLIENT_ASSERTION_TYPE } from "@better-auth/core/oauth2";
4
4
  import { createLocalJWKSet, decodeJwt, decodeProtectedHeader, jwtVerify } from "jose";
@@ -62,7 +62,7 @@ function isPrivateHostname(hostname) {
62
62
  if (host === "metadata.google.internal") return true;
63
63
  return false;
64
64
  }
65
- function validateJwksUri(ctx, jwksUri) {
65
+ function validateJwksUri(ctx, jwksUri, clientIdUrlOrigin) {
66
66
  const parsed = new URL(jwksUri);
67
67
  if (parsed.protocol !== "https:") throw new APIError("BAD_REQUEST", {
68
68
  error_description: "jwks_uri must use HTTPS",
@@ -72,11 +72,20 @@ function validateJwksUri(ctx, jwksUri) {
72
72
  error_description: "jwks_uri must not point to a private or reserved address",
73
73
  error: "invalid_client"
74
74
  });
75
+ if (clientIdUrlOrigin && parsed.origin === clientIdUrlOrigin) return;
75
76
  if (!ctx.context.isTrustedOrigin(parsed.href)) throw new APIError("BAD_REQUEST", {
76
77
  error_description: "client jwks_uri is not trusted",
77
78
  error: "invalid_client"
78
79
  });
79
80
  }
81
+ function urlClientIdOrigin(clientId) {
82
+ if (!clientId.startsWith("https://") && !clientId.startsWith("http://")) return;
83
+ try {
84
+ return new URL(clientId).origin;
85
+ } catch {
86
+ return;
87
+ }
88
+ }
80
89
  async function fetchJwksFromUri(jwksUri) {
81
90
  const controller = new AbortController();
82
91
  const timeout = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
@@ -100,7 +109,7 @@ async function fetchClientJwks(ctx, client) {
100
109
  error_description: "client has no JWKS configured",
101
110
  error: "invalid_client"
102
111
  });
103
- validateJwksUri(ctx, client.jwksUri);
112
+ validateJwksUri(ctx, client.jwksUri, urlClientIdOrigin(client.clientId));
104
113
  const now = Date.now();
105
114
  const cached = jwksCache.get(client.jwksUri);
106
115
  if (cached && now - cached.fetchedAt < JWKS_CACHE_TTL_MS) return cached.jwks;
@@ -1,4 +1,4 @@
1
- import { a as ResourceServerMetadata } from "./oauth-C8aTlaAC.mjs";
1
+ import { o as ResourceServerMetadata } from "./oauth-CU79t-eG.mjs";
2
2
  import { JWTPayload, JWTVerifyOptions } from "jose";
3
3
  import { Auth } from "better-auth/types";
4
4
 
@@ -1,6 +1,6 @@
1
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";
2
+ import { o as getJwtPlugin, s as getOAuthProviderPlugin } from "./utils-Cx_XnD9i.mjs";
3
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.mjs";
4
4
  import { verifyAccessToken } from "better-auth/oauth2";
5
5
  import { APIError } from "better-call";
6
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-Dh4YXCXY.mjs";
1
+ import { n as oauthProvider } from "./oauth-B_qonG53.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-BGWhjYBb.mjs";
1
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.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,9 +1,10 @@
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";
1
+ import { _ as Scope, a as OIDCMetadata, b as Awaitable, c as AuthorizePrompt, d as OAuthConsent, f as OAuthOpaqueAccessToken, g as SchemaClient, h as Prompt, i as OAuthClient, l as ClientDiscovery, m as OAuthRefreshToken, n as AuthServerMetadata, o as ResourceServerMetadata, p as OAuthOptions, r as GrantType, s as TokenEndpointAuthMethod, t as AuthMethod, u as OAuthAuthorizationQuery, v as StoreTokenType, y as VerificationValue } from "./oauth-CU79t-eG.mjs";
2
+ import { n as oauthProvider, t as getOAuthProviderState } from "./oauth-B_qonG53.mjs";
3
3
  import { verifyAccessToken } from "better-auth/oauth2";
4
4
  import { JWSAlgorithms, JwtOptions } from "better-auth/plugins";
5
5
  import { JWTPayload } from "jose";
6
6
  import { GenericEndpointContext } from "@better-auth/core";
7
+ import * as better_auth0 from "better-auth";
7
8
 
8
9
  //#region src/mcp.d.ts
9
10
  /**
@@ -27,7 +28,37 @@ declare function authServerMetadata(ctx: GenericEndpointContext, opts?: JwtOptio
27
28
  }): AuthServerMetadata;
28
29
  declare function oidcServerMetadata(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]> & {
29
30
  claims?: string[];
30
- }): Omit<OIDCMetadata, "id_token_signing_alg_values_supported"> & {
31
+ }): {
32
+ jwks_uri?: string | undefined;
33
+ userinfo_endpoint: string;
34
+ acr_values_supported: string[];
35
+ subject_types_supported: ("public" | "pairwise")[];
36
+ claims_supported: string[];
37
+ end_session_endpoint: string;
38
+ prompt_values_supported: Prompt[];
39
+ issuer: string;
40
+ authorization_endpoint: string;
41
+ token_endpoint: string;
42
+ registration_endpoint: string;
43
+ scopes_supported?: string[] | undefined;
44
+ response_types_supported: "code"[];
45
+ response_modes_supported: "query"[];
46
+ grant_types_supported: GrantType[];
47
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
48
+ token_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
49
+ service_documentation?: string | undefined;
50
+ ui_locales_supported?: string[] | undefined;
51
+ op_policy_uri?: string | undefined;
52
+ op_tos_uri?: string | undefined;
53
+ revocation_endpoint?: string | undefined;
54
+ revocation_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
55
+ revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
56
+ introspection_endpoint?: string | undefined;
57
+ introspection_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
58
+ introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
59
+ code_challenge_methods_supported: "S256"[];
60
+ authorization_response_iss_parameter_supported?: boolean | undefined;
61
+ client_id_metadata_document_supported?: boolean | undefined;
31
62
  id_token_signing_alg_values_supported: JWSAlgorithms[] | ["HS256"];
32
63
  };
33
64
  /**
@@ -44,7 +75,7 @@ declare const oauthProviderAuthServerMetadata: <Auth extends {
44
75
  };
45
76
  }>(auth: Auth, opts?: {
46
77
  headers?: HeadersInit;
47
- }) => (_request: Request) => Promise<Response>;
78
+ }) => (request: Request) => Promise<Response>;
48
79
  /**
49
80
  * Provides an exportable `/.well-known/openid-configuration`.
50
81
  *
@@ -59,6 +90,19 @@ declare const oauthProviderOpenIdConfigMetadata: <Auth extends {
59
90
  };
60
91
  }>(auth: Auth, opts?: {
61
92
  headers?: HeadersInit;
62
- }) => (_request: Request) => Promise<Response>;
93
+ }) => (request: Request) => Promise<Response>;
63
94
  //#endregion
64
- export { AuthServerMetadata, AuthorizePrompt, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, OAuthOpaqueAccessToken, OAuthOptions, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
95
+ //#region src/register.d.ts
96
+ declare function checkOAuthClient(client: OAuthClient, opts: OAuthOptions<Scope[]>, settings?: {
97
+ isRegister?: boolean;
98
+ ctx?: GenericEndpointContext;
99
+ }): Promise<void>;
100
+ /**
101
+ * Converts an OAuth 2.0 Dynamic Client Schema to a Database Schema
102
+ *
103
+ * @param input
104
+ * @returns
105
+ */
106
+ declare function oauthToSchema(input: OAuthClient): SchemaClient<Scope[]>;
107
+ //#endregion
108
+ export { AuthServerMetadata, AuthorizePrompt, ClientDiscovery, OAuthAuthorizationQuery, OAuthClient, OAuthConsent, OAuthOpaqueAccessToken, OAuthOptions, OAuthRefreshToken, OIDCMetadata, Prompt, ResourceServerMetadata, SchemaClient, Scope, StoreTokenType, VerificationValue, authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { n as isPrivateHostname } from "./client-assertion-DZqo-L5j.mjs";
1
+ import { n as isPrivateHostname } from "./client-assertion-CderPEmR.mjs";
2
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
+ import { _ as storeClientSecret, a as getClient, b as validateClientCredentials, c as getStoredToken, d as normalizeTimestampValue, f as parseClientMetadata, g as searchParamsToQuery, h as resolveSubjectIdentifier, i as extractClientCredentials, l as isPKCERequired, m as resolveSessionAuthTime, n as deleteFromPrompt, o as getJwtPlugin, p as parsePrompt, r as destructureCredentials, t as decryptStoredClientSecret, u as mergeDiscoveryMetadata, v as storeToken, x as verifyOAuthQueryParams, y as toClientDiscoveryArray } from "./utils-Cx_XnD9i.mjs";
4
+ import { t as PACKAGE_VERSION } from "./version-DIwdpXrQ.mjs";
5
5
  import { APIError, createAuthEndpoint, createAuthMiddleware, getOAuthState, getSessionFromCtx, sessionMiddleware } from "better-auth/api";
6
6
  import { generateCodeChallenge, getJwks, verifyJwsAccessToken } from "better-auth/oauth2";
7
7
  import { APIError as APIError$1 } from "better-call";
@@ -14,8 +14,8 @@ import { BetterAuthError } from "@better-auth/core/error";
14
14
  import { parseSetCookieHeader } from "better-auth/cookies";
15
15
  import { mergeSchema } from "better-auth/db";
16
16
  import * as z from "zod";
17
- import { signJWT, toExpJWT } from "better-auth/plugins";
18
- import { SignJWT, compactVerify, createLocalJWKSet, decodeJwt } from "jose";
17
+ import { resolveSigningKey, signJWT, toExpJWT } from "better-auth/plugins";
18
+ import { SignJWT, base64url, compactVerify, createLocalJWKSet, decodeJwt, decodeProtectedHeader } from "jose";
19
19
  //#region src/consent.ts
20
20
  async function consentEndpoint(ctx, opts) {
21
21
  const _query = (await oAuthState.get())?.query;
@@ -156,6 +156,81 @@ async function postLogin(ctx, opts) {
156
156
  };
157
157
  }
158
158
  //#endregion
159
+ //#region src/types/zod.ts
160
+ const DANGEROUS_SCHEMES = [
161
+ "javascript:",
162
+ "data:",
163
+ "vbscript:"
164
+ ];
165
+ function isLocalhost(hostname) {
166
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
167
+ }
168
+ /**
169
+ * Runtime schema for OAuthAuthorizationQuery.
170
+ * Uses passthrough to tolerate fields added by future extensions (PAR, FPA, etc.)
171
+ */
172
+ const oauthAuthorizationQuerySchema = z.object({
173
+ response_type: z.literal("code").optional(),
174
+ request_uri: z.string().optional(),
175
+ redirect_uri: z.string(),
176
+ scope: z.string().optional(),
177
+ state: z.string().optional(),
178
+ client_id: z.string(),
179
+ prompt: z.string().optional(),
180
+ display: z.string().optional(),
181
+ ui_locales: z.string().optional(),
182
+ max_age: z.coerce.number().optional(),
183
+ acr_values: z.string().optional(),
184
+ login_hint: z.string().optional(),
185
+ id_token_hint: z.string().optional(),
186
+ code_challenge: z.string().optional(),
187
+ code_challenge_method: z.literal("S256").optional(),
188
+ nonce: z.string().optional()
189
+ }).passthrough();
190
+ /**
191
+ * Runtime schema for the authorization code verification value.
192
+ * Validates structure on deserialization from the JSON blob stored in the DB.
193
+ * Uses passthrough so future fields (e.g. from authorization challenge) don't break parsing.
194
+ */
195
+ const verificationValueSchema = z.object({
196
+ type: z.literal("authorization_code"),
197
+ query: oauthAuthorizationQuerySchema,
198
+ sessionId: z.string(),
199
+ userId: z.string(),
200
+ referenceId: z.string().optional(),
201
+ authTime: z.number().optional()
202
+ }).passthrough();
203
+ /**
204
+ * Reusable URL validation for OAuth redirect URIs.
205
+ * - Blocks dangerous schemes (javascript:, data:, vbscript:)
206
+ * - For http/https: requires HTTPS (HTTP allowed only for localhost)
207
+ * - Allows custom schemes for mobile apps (e.g., myapp://callback)
208
+ */
209
+ const SafeUrlSchema = z.url().superRefine((val, ctx) => {
210
+ if (!URL.canParse(val)) {
211
+ ctx.addIssue({
212
+ code: "custom",
213
+ message: "URL must be parseable",
214
+ fatal: true
215
+ });
216
+ return z.NEVER;
217
+ }
218
+ const u = new URL(val);
219
+ if (DANGEROUS_SCHEMES.includes(u.protocol)) {
220
+ ctx.addIssue({
221
+ code: "custom",
222
+ message: "URL cannot use javascript:, data:, or vbscript: scheme"
223
+ });
224
+ return;
225
+ }
226
+ if (u.protocol === "http:" || u.protocol === "https:") {
227
+ if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
228
+ code: "custom",
229
+ message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
230
+ });
231
+ }
232
+ });
233
+ //#endregion
159
234
  //#region src/userinfo.ts
160
235
  /**
161
236
  * Provides shared /userinfo and id_token claims functionality
@@ -268,7 +343,7 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
268
343
  options: jwtPluginOptions,
269
344
  payload: {
270
345
  ...customClaims,
271
- sub: user.id,
346
+ sub: user?.id,
272
347
  aud: typeof audience === "string" ? audience : audience?.length === 1 ? audience.at(0) : audience,
273
348
  azp: client.clientId,
274
349
  scope: scopes.join(" "),
@@ -280,10 +355,23 @@ async function createJwtAccessToken(ctx, opts, user, client, audience, scopes, r
280
355
  });
281
356
  }
282
357
  /**
358
+ * Computes an OIDC hash (at_hash, c_hash) per OIDC Core §3.1.3.6.
359
+ * Hashes the token, takes the left half, and base64url-encodes it.
360
+ */
361
+ async function computeOidcHash(token, signingAlg) {
362
+ let hashAlg;
363
+ if (signingAlg === "EdDSA") hashAlg = "SHA-512";
364
+ else if (signingAlg.endsWith("384")) hashAlg = "SHA-384";
365
+ else if (signingAlg.endsWith("512")) hashAlg = "SHA-512";
366
+ else hashAlg = "SHA-256";
367
+ const digest = new Uint8Array(await crypto.subtle.digest(hashAlg, new TextEncoder().encode(token)));
368
+ return base64url.encode(digest.slice(0, digest.length / 2));
369
+ }
370
+ /**
283
371
  * Creates a user id token in code_authorization with scope of 'openid'
284
372
  * and hybrid/implicit (not yet implemented) flows
285
373
  */
286
- async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) {
374
+ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) {
287
375
  const iat = Math.floor(Date.now() / 1e3);
288
376
  const exp = iat + (opts.idTokenExpiresIn ?? 36e3);
289
377
  const userClaims = userNormalClaims(user, scopes);
@@ -296,11 +384,15 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
296
384
  metadata: parseClientMetadata(client.metadata)
297
385
  }) : {};
298
386
  const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
387
+ const resolvedKey = !opts.disableJwtPlugin && !jwtPluginOptions?.jwt?.sign ? await resolveSigningKey(ctx, jwtPluginOptions) : void 0;
388
+ const signingAlg = opts.disableJwtPlugin ? "HS256" : resolvedKey?.alg ?? jwtPluginOptions?.jwks?.keyPairConfig?.alg;
389
+ const atHash = accessToken ? await computeOidcHash(accessToken, signingAlg) : void 0;
299
390
  const payload = {
300
391
  ...userClaims,
301
392
  auth_time: authTimeSec,
302
393
  acr,
303
394
  ...customClaims,
395
+ at_hash: atHash,
304
396
  iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
305
397
  sub: resolvedSub,
306
398
  aud: client.clientId,
@@ -310,10 +402,19 @@ async function createIdToken(ctx, opts, user, client, scopes, nonce, sessionId,
310
402
  sid: client.enableEndSession ? sessionId : void 0
311
403
  };
312
404
  if (opts.disableJwtPlugin && !client.clientSecret) return;
313
- return opts.disableJwtPlugin ? new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : signJWT(ctx, {
405
+ const idToken = opts.disableJwtPlugin ? await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(new TextEncoder().encode(await decryptStoredClientSecret(ctx, opts.storeClientSecret, client.clientSecret))) : await signJWT(ctx, {
314
406
  options: jwtPluginOptions,
315
- payload
407
+ payload,
408
+ resolvedKey: resolvedKey ?? void 0
316
409
  });
410
+ if (idToken && atHash && jwtPluginOptions?.jwt?.sign) {
411
+ const header = decodeProtectedHeader(idToken);
412
+ if (header.alg !== signingAlg) throw new APIError("INTERNAL_SERVER_ERROR", {
413
+ error_description: `ID token signed with "${header.alg}" but at_hash was computed for "${signingAlg}". Ensure jwt.sign uses the algorithm declared in keyPairConfig.alg.`,
414
+ error: "server_error"
415
+ });
416
+ }
417
+ return idToken;
317
418
  }
318
419
  /**
319
420
  * Encodes a refresh token for a client
@@ -402,39 +503,45 @@ async function checkResource(ctx, opts, scopes) {
402
503
  }
403
504
  return audience?.length === 1 ? audience.at(0) : audience;
404
505
  }
405
- async function createUserTokens(ctx, opts, client, scopes, user, referenceId, sessionId, nonce, additional, authTime) {
506
+ async function createUserTokens(ctx, opts, params) {
507
+ const { client, scopes, user, grantType, referenceId, sessionId, nonce, refreshToken: existingRefreshToken, authTime, verificationValue } = params;
406
508
  const iat = Math.floor(Date.now() / 1e3);
407
- const defaultExp = iat + (opts.accessTokenExpiresIn ?? 3600);
509
+ const defaultExp = iat + (user ? opts.accessTokenExpiresIn ?? 3600 : opts.m2mAccessTokenExpiresIn ?? 3600);
408
510
  const exp = opts.scopeExpirations ? scopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
409
511
  return prev < curr ? prev : curr;
410
512
  }, defaultExp) : defaultExp;
411
513
  const audience = await checkResource(ctx, opts, scopes);
412
- const isRefreshToken = additional?.refreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access");
514
+ const isRefreshToken = user && (existingRefreshToken?.scopes?.includes("offline_access") || scopes.includes("offline_access"));
413
515
  const isJwtAccessToken = audience && !opts.disableJwtPlugin;
414
- const isIdToken = scopes.includes("openid");
415
- const earlyRefreshToken = isRefreshToken && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
516
+ const isIdToken = user && scopes.includes("openid");
517
+ const customFields = opts.customTokenResponseFields ? await opts.customTokenResponseFields({
518
+ grantType,
519
+ user,
520
+ scopes,
521
+ metadata: parseClientMetadata(client.metadata),
522
+ verificationValue
523
+ }) : void 0;
524
+ const earlyRefreshToken = isRefreshToken && user && !isJwtAccessToken ? await createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
416
525
  iat,
417
526
  exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
418
527
  sid: sessionId
419
- }, additional?.refreshToken, authTime) : void 0;
420
- const [accessToken, refreshToken, idToken] = await Promise.all([
421
- isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
422
- iat,
423
- exp,
424
- sid: sessionId
425
- }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
426
- iat,
427
- exp,
428
- sid: sessionId
429
- }, referenceId, earlyRefreshToken?.id),
430
- earlyRefreshToken ? earlyRefreshToken : isRefreshToken ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
431
- iat,
432
- exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
433
- sid: sessionId
434
- }, additional?.refreshToken, authTime) : void 0,
435
- isIdToken ? createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime) : void 0
436
- ]);
528
+ }, existingRefreshToken, authTime) : void 0;
529
+ const [accessToken, refreshToken] = await Promise.all([isJwtAccessToken ? createJwtAccessToken(ctx, opts, user, client, audience, scopes, referenceId, {
530
+ iat,
531
+ exp,
532
+ sid: sessionId
533
+ }) : createOpaqueAccessToken(ctx, opts, user, client, scopes, {
534
+ iat,
535
+ exp,
536
+ sid: sessionId
537
+ }, referenceId, earlyRefreshToken?.id), earlyRefreshToken ? earlyRefreshToken : isRefreshToken && user ? createRefreshToken(ctx, opts, user, referenceId, client, scopes, {
538
+ iat,
539
+ exp: iat + (opts.refreshTokenExpiresIn ?? 2592e3),
540
+ sid: sessionId
541
+ }, existingRefreshToken, authTime) : void 0]);
542
+ const idToken = isIdToken ? await createIdToken(ctx, opts, user, client, scopes, nonce, sessionId, authTime, accessToken) : void 0;
437
543
  return ctx.json({
544
+ ...customFields,
438
545
  access_token: accessToken,
439
546
  expires_in: exp - iat,
440
547
  expires_at: exp,
@@ -450,7 +557,6 @@ async function createUserTokens(ctx, opts, client, scopes, user, referenceId, se
450
557
  /** Checks verification value */
451
558
  async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri) {
452
559
  const verification = await ctx.context.internalAdapter.findVerificationValue(await storeToken(opts.storeTokens, code, "authorization_code"));
453
- const verificationValue = verification ? JSON.parse(verification?.value) : void 0;
454
560
  if (!verification) throw new APIError("UNAUTHORIZED", {
455
561
  error_description: "Invalid code",
456
562
  error: "invalid_verification"
@@ -460,22 +566,25 @@ async function checkVerificationValue(ctx, opts, code, client_id, redirect_uri)
460
566
  error_description: "code expired",
461
567
  error: "invalid_verification"
462
568
  });
463
- if (!verificationValue) throw new APIError("UNAUTHORIZED", {
464
- error_description: "missing verification value content",
465
- error: "invalid_verification"
466
- });
467
- if (verificationValue.type !== "authorization_code") throw new APIError("UNAUTHORIZED", {
468
- error_description: "incorrect verification type",
569
+ let rawValue;
570
+ try {
571
+ rawValue = JSON.parse(verification.value);
572
+ } catch {
573
+ throw new APIError("UNAUTHORIZED", {
574
+ error_description: "malformed verification value",
575
+ error: "invalid_verification"
576
+ });
577
+ }
578
+ const parsed = verificationValueSchema.safeParse(rawValue);
579
+ if (!parsed.success) throw new APIError("UNAUTHORIZED", {
580
+ error_description: "malformed verification value",
469
581
  error: "invalid_verification"
470
582
  });
583
+ const verificationValue = parsed.data;
471
584
  if (verificationValue.query.client_id !== client_id) throw new APIError("UNAUTHORIZED", {
472
585
  error_description: "invalid client_id",
473
586
  error: "invalid_client"
474
587
  });
475
- if (!verificationValue.userId) throw new APIError("BAD_REQUEST", {
476
- error_description: "missing user_id on challenge",
477
- error: "invalid_user"
478
- });
479
588
  if (verificationValue.query?.redirect_uri && verificationValue.query?.redirect_uri !== redirect_uri) throw new APIError("BAD_REQUEST", {
480
589
  error_description: "redirect_uri mismatch",
481
590
  error: "invalid_request"
@@ -563,7 +672,17 @@ async function handleAuthorizationCodeGrant(ctx, opts) {
563
672
  error: "invalid_request"
564
673
  });
565
674
  const authTime = verificationValue.authTime != null ? normalizeTimestampValue(verificationValue.authTime) : resolveSessionAuthTime(session);
566
- return createUserTokens(ctx, opts, client, verificationValue.query.scope?.split(" ") ?? [], user, verificationValue.referenceId, session.id, verificationValue.query?.nonce, void 0, authTime);
675
+ return createUserTokens(ctx, opts, {
676
+ client,
677
+ scopes: verificationValue.query.scope?.split(" ") ?? [],
678
+ user,
679
+ grantType: "authorization_code",
680
+ referenceId: verificationValue.referenceId,
681
+ sessionId: session.id,
682
+ nonce: verificationValue.query?.nonce,
683
+ authTime,
684
+ verificationValue
685
+ });
567
686
  }
568
687
  /**
569
688
  * Grant that allows direct access to an API using the application's credentials
@@ -601,43 +720,11 @@ async function handleClientCredentialsGrant(ctx, opts) {
601
720
  });
602
721
  }
603
722
  if (!requestedScopes) requestedScopes = client.scopes ?? opts.clientCredentialGrantDefaultScopes ?? opts.scopes ?? [];
604
- const jwtPluginOptions = opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context).options;
605
- const audience = await checkResource(ctx, opts, requestedScopes);
606
- const iat = Math.floor(Date.now() / 1e3);
607
- const defaultExp = iat + (opts.m2mAccessTokenExpiresIn ?? 3600);
608
- const exp = opts.scopeExpirations && requestedScopes ? requestedScopes.map((sc) => opts.scopeExpirations?.[sc] ? toExpJWT(opts.scopeExpirations[sc], iat) : defaultExp).reduce((prev, curr) => {
609
- return prev < curr ? prev : curr;
610
- }, defaultExp) : defaultExp;
611
- const customClaims = opts.customAccessTokenClaims ? await opts.customAccessTokenClaims({
723
+ return createUserTokens(ctx, opts, {
724
+ client,
612
725
  scopes: requestedScopes,
613
- resource: ctx.body.resource,
614
- metadata: parseClientMetadata(client.metadata)
615
- }) : {};
616
- const accessToken = audience && !opts.disableJwtPlugin ? await signJWT(ctx, {
617
- options: jwtPluginOptions,
618
- payload: {
619
- ...customClaims,
620
- aud: audience,
621
- azp: client.clientId,
622
- scope: requestedScopes.join(" "),
623
- iss: jwtPluginOptions?.jwt?.issuer ?? ctx.context.baseURL,
624
- iat,
625
- exp
626
- }
627
- }) : await createOpaqueAccessToken(ctx, opts, void 0, client, requestedScopes, {
628
- iat,
629
- exp
726
+ grantType: "client_credentials"
630
727
  });
631
- return ctx.json({
632
- access_token: accessToken,
633
- expires_in: exp - iat,
634
- expires_at: exp,
635
- token_type: "Bearer",
636
- scope: requestedScopes.join(" ")
637
- }, { headers: {
638
- "Cache-Control": "no-store",
639
- Pragma: "no-cache"
640
- } });
641
728
  }
642
729
  /**
643
730
  * Obtains new Session Jwt and Refresh Tokens using a refresh token
@@ -708,7 +795,16 @@ async function handleRefreshTokenGrant(ctx, opts) {
708
795
  error: "invalid_request"
709
796
  });
710
797
  const authTime = refreshToken.authTime != null ? normalizeTimestampValue(refreshToken.authTime) : void 0;
711
- return createUserTokens(ctx, opts, client, requestedScopes ?? scopes, user, refreshToken.referenceId, refreshToken.sessionId, void 0, { refreshToken }, authTime);
798
+ return createUserTokens(ctx, opts, {
799
+ client,
800
+ scopes: requestedScopes ?? scopes,
801
+ user,
802
+ grantType: "refresh_token",
803
+ referenceId: refreshToken.referenceId,
804
+ sessionId: refreshToken.sessionId,
805
+ refreshToken,
806
+ authTime
807
+ });
712
808
  }
713
809
  //#endregion
714
810
  //#region src/introspect.ts
@@ -1085,6 +1181,21 @@ const publicSessionMiddleware = (opts) => createAuthMiddleware(async (ctx) => {
1085
1181
  });
1086
1182
  //#endregion
1087
1183
  //#region src/register.ts
1184
+ /**
1185
+ * Resolves the auth method and type for unauthenticated DCR.
1186
+ * Overrides confidential methods to "none" per RFC 7591 Section 3.2.1.
1187
+ * When overriding, clears type "web" since it is only valid for confidential clients.
1188
+ */
1189
+ function resolveUnauthenticatedAuth(body) {
1190
+ if (body.token_endpoint_auth_method === "none") return {
1191
+ tokenEndpointAuthMethod: "none",
1192
+ type: body.type
1193
+ };
1194
+ return {
1195
+ tokenEndpointAuthMethod: "none",
1196
+ type: body.type === "web" ? void 0 : body.type
1197
+ };
1198
+ }
1088
1199
  async function registerEndpoint(ctx, opts) {
1089
1200
  if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1090
1201
  error: "access_denied",
@@ -1096,12 +1207,16 @@ async function registerEndpoint(ctx, opts) {
1096
1207
  error: "invalid_token",
1097
1208
  error_description: "Authentication required for client registration"
1098
1209
  });
1099
- const isPublic = body.token_endpoint_auth_method === "none";
1100
- if (!session && !isPublic) throw new APIError("UNAUTHORIZED", {
1101
- error: "invalid_request",
1102
- error_description: "Authentication required for confidential client registration"
1103
- });
1104
- if (!ctx.body.scope) ctx.body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
1210
+ if (!session) {
1211
+ if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1212
+ error: "invalid_client_metadata",
1213
+ error_description: "client_credentials grant requires authenticated registration"
1214
+ });
1215
+ const resolved = resolveUnauthenticatedAuth(body);
1216
+ body.token_endpoint_auth_method = resolved.tokenEndpointAuthMethod;
1217
+ body.type = resolved.type;
1218
+ }
1219
+ if (!body.scope) body.scope = (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ");
1105
1220
  return createOAuthClientEndpoint(ctx, opts, { isRegister: true });
1106
1221
  }
1107
1222
  async function checkOAuthClient(client, opts, settings) {
@@ -1340,46 +1455,6 @@ function schemaToOAuth(input) {
1340
1455
  };
1341
1456
  }
1342
1457
  //#endregion
1343
- //#region src/types/zod.ts
1344
- const DANGEROUS_SCHEMES = [
1345
- "javascript:",
1346
- "data:",
1347
- "vbscript:"
1348
- ];
1349
- function isLocalhost(hostname) {
1350
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
1351
- }
1352
- /**
1353
- * Reusable URL validation for OAuth redirect URIs.
1354
- * - Blocks dangerous schemes (javascript:, data:, vbscript:)
1355
- * - For http/https: requires HTTPS (HTTP allowed only for localhost)
1356
- * - Allows custom schemes for mobile apps (e.g., myapp://callback)
1357
- */
1358
- const SafeUrlSchema = z.url().superRefine((val, ctx) => {
1359
- if (!URL.canParse(val)) {
1360
- ctx.addIssue({
1361
- code: "custom",
1362
- message: "URL must be parseable",
1363
- fatal: true
1364
- });
1365
- return z.NEVER;
1366
- }
1367
- const u = new URL(val);
1368
- if (DANGEROUS_SCHEMES.includes(u.protocol)) {
1369
- ctx.addIssue({
1370
- code: "custom",
1371
- message: "URL cannot use javascript:, data:, or vbscript: scheme"
1372
- });
1373
- return;
1374
- }
1375
- if (u.protocol === "http:" || u.protocol === "https:") {
1376
- if (u.protocol === "http:" && !isLocalhost(u.hostname)) ctx.addIssue({
1377
- code: "custom",
1378
- message: "Redirect URI must use HTTPS (HTTP allowed only for localhost)"
1379
- });
1380
- }
1381
- });
1382
- //#endregion
1383
1458
  //#region src/oauthClient/endpoints.ts
1384
1459
  async function getClientEndpoint(ctx, opts) {
1385
1460
  const session = await getSessionFromCtx(ctx);
@@ -1543,6 +1618,7 @@ async function updateClientEndpoint(ctx, opts) {
1543
1618
  else {
1544
1619
  schemaUpdates.jwks = null;
1545
1620
  schemaUpdates.jwksUri = null;
1621
+ if (!schemaUpdates.clientSecret) schemaUpdates.clientSecret = await storeClientSecret(ctx, opts, opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z"));
1546
1622
  }
1547
1623
  const updatedClient = await ctx.context.adapter.update({
1548
1624
  model: "oauthClient",
@@ -2800,7 +2876,7 @@ const oauthProvider = (options) => {
2800
2876
  queryParams.delete("sig");
2801
2877
  queryParams.delete("exp");
2802
2878
  await oAuthState.set({ query: queryParams.toString() });
2803
- if (ctx.path === "/sign-in/social" || ctx.path === "/sign-in/oauth2") {
2879
+ if (ctx.path === "/sign-in/social") {
2804
2880
  if (ctx.body.additionalData?.query) return;
2805
2881
  if (!ctx.body.additionalData) ctx.body.additionalData = {};
2806
2882
  ctx.body.additionalData.query = queryParams.toString();
@@ -2834,12 +2910,15 @@ const oauthProvider = (options) => {
2834
2910
  metadata: { SERVER_ONLY: true }
2835
2911
  }, async (ctx) => {
2836
2912
  if (opts.scopes && opts.scopes.includes("openid")) return oidcServerMetadata(ctx, opts);
2837
- else return authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
2838
- scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2839
- public_client_supported: opts.allowUnauthenticatedClientRegistration,
2840
- grant_types_supported: opts.grantTypes,
2841
- jwt_disabled: opts.disableJwtPlugin
2842
- });
2913
+ else return {
2914
+ ...authServerMetadata(ctx, opts.disableJwtPlugin ? void 0 : getJwtPlugin(ctx.context)?.options, {
2915
+ scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
2916
+ public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
2917
+ grant_types_supported: opts.grantTypes,
2918
+ jwt_disabled: opts.disableJwtPlugin
2919
+ }),
2920
+ ...mergeDiscoveryMetadata(opts.clientDiscovery)
2921
+ };
2843
2922
  }),
2844
2923
  getOpenIdConfig: createAuthEndpoint("/.well-known/openid-configuration", {
2845
2924
  method: "GET",
@@ -4004,7 +4083,7 @@ function oidcServerMetadata(ctx, opts) {
4004
4083
  return {
4005
4084
  ...authServerMetadata(ctx, jwtPluginOptions, {
4006
4085
  scopes_supported: opts.advertisedMetadata?.scopes_supported ?? opts.scopes,
4007
- public_client_supported: opts.allowUnauthenticatedClientRegistration,
4086
+ public_client_supported: opts.allowUnauthenticatedClientRegistration || toClientDiscoveryArray(opts.clientDiscovery).length > 0,
4008
4087
  grant_types_supported: opts.grantTypes,
4009
4088
  jwt_disabled: opts.disableJwtPlugin
4010
4089
  }),
@@ -4020,9 +4099,20 @@ function oidcServerMetadata(ctx, opts) {
4020
4099
  "create",
4021
4100
  "select_account",
4022
4101
  "none"
4023
- ]
4102
+ ],
4103
+ ...mergeDiscoveryMetadata(opts.clientDiscovery)
4024
4104
  };
4025
4105
  }
4106
+ const METADATA_CACHE_CONTROL = "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400";
4107
+ function metadataResponse(body, extraHeaders) {
4108
+ const headers = new Headers(extraHeaders);
4109
+ if (!headers.has("Cache-Control")) headers.set("Cache-Control", METADATA_CACHE_CONTROL);
4110
+ headers.set("Content-Type", "application/json");
4111
+ return new Response(JSON.stringify(body), {
4112
+ status: 200,
4113
+ headers
4114
+ });
4115
+ }
4026
4116
  /**
4027
4117
  * Provides an exportable `/.well-known/oauth-authorization-server`.
4028
4118
  *
@@ -4032,16 +4122,11 @@ function oidcServerMetadata(ctx, opts) {
4032
4122
  * @external
4033
4123
  */
4034
4124
  const oauthProviderAuthServerMetadata = (auth, opts) => {
4035
- return async (_request) => {
4036
- const res = await auth.api.getOAuthServerConfig();
4037
- return new Response(JSON.stringify(res), {
4038
- status: 200,
4039
- headers: {
4040
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
4041
- ...opts?.headers,
4042
- "Content-Type": "application/json"
4043
- }
4044
- });
4125
+ return async (request) => {
4126
+ return metadataResponse(await auth.api.getOAuthServerConfig({
4127
+ request,
4128
+ asResponse: false
4129
+ }), opts?.headers);
4045
4130
  };
4046
4131
  };
4047
4132
  /**
@@ -4053,17 +4138,12 @@ const oauthProviderAuthServerMetadata = (auth, opts) => {
4053
4138
  * @external
4054
4139
  */
4055
4140
  const oauthProviderOpenIdConfigMetadata = (auth, opts) => {
4056
- return async (_request) => {
4057
- const res = await auth.api.getOpenIdConfig();
4058
- return new Response(JSON.stringify(res), {
4059
- status: 200,
4060
- headers: {
4061
- "Cache-Control": "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400",
4062
- ...opts?.headers,
4063
- "Content-Type": "application/json"
4064
- }
4065
- });
4141
+ return async (request) => {
4142
+ return metadataResponse(await auth.api.getOpenIdConfig({
4143
+ request,
4144
+ asResponse: false
4145
+ }), opts?.headers);
4066
4146
  };
4067
4147
  };
4068
4148
  //#endregion
4069
- export { authServerMetadata, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata };
4149
+ export { authServerMetadata, checkOAuthClient, getOAuthProviderState, mcpHandler, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthToSchema, oidcServerMetadata };
@@ -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-C8aTlaAC.mjs";
1
+ import { _ as Scope, d as OAuthConsent, h as Prompt, i as OAuthClient, p as OAuthOptions, r as GrantType, s as TokenEndpointAuthMethod, t as AuthMethod } from "./oauth-CU79t-eG.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";
@@ -54,7 +54,64 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
54
54
  metadata: {
55
55
  SERVER_ONLY: true;
56
56
  };
57
- }, AuthServerMetadata>;
57
+ }, {
58
+ jwks_uri?: string | undefined;
59
+ userinfo_endpoint: string;
60
+ acr_values_supported: string[];
61
+ subject_types_supported: ("public" | "pairwise")[];
62
+ claims_supported: string[];
63
+ end_session_endpoint: string;
64
+ prompt_values_supported: Prompt[];
65
+ issuer: string;
66
+ authorization_endpoint: string;
67
+ token_endpoint: string;
68
+ registration_endpoint: string;
69
+ scopes_supported?: string[] | undefined;
70
+ response_types_supported: "code"[];
71
+ response_modes_supported: "query"[];
72
+ grant_types_supported: GrantType[];
73
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
74
+ token_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
75
+ service_documentation?: string | undefined;
76
+ ui_locales_supported?: string[] | undefined;
77
+ op_policy_uri?: string | undefined;
78
+ op_tos_uri?: string | undefined;
79
+ revocation_endpoint?: string | undefined;
80
+ revocation_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
81
+ revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
82
+ introspection_endpoint?: string | undefined;
83
+ introspection_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
84
+ introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
85
+ code_challenge_methods_supported: "S256"[];
86
+ authorization_response_iss_parameter_supported?: boolean | undefined;
87
+ client_id_metadata_document_supported?: boolean | undefined;
88
+ id_token_signing_alg_values_supported: better_auth_plugins0.JWSAlgorithms[] | ["HS256"];
89
+ } | {
90
+ issuer: string;
91
+ authorization_endpoint: string;
92
+ token_endpoint: string;
93
+ jwks_uri?: string;
94
+ registration_endpoint: string;
95
+ scopes_supported?: string[];
96
+ response_types_supported: "code"[];
97
+ response_modes_supported: "query"[];
98
+ grant_types_supported: GrantType[];
99
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[];
100
+ token_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[];
101
+ service_documentation?: string;
102
+ ui_locales_supported?: string[];
103
+ op_policy_uri?: string;
104
+ op_tos_uri?: string;
105
+ revocation_endpoint?: string;
106
+ revocation_endpoint_auth_methods_supported?: AuthMethod[];
107
+ revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[];
108
+ introspection_endpoint?: string;
109
+ introspection_endpoint_auth_methods_supported?: AuthMethod[];
110
+ introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[];
111
+ code_challenge_methods_supported: "S256"[];
112
+ authorization_response_iss_parameter_supported?: boolean;
113
+ client_id_metadata_document_supported?: boolean;
114
+ }>;
58
115
  /**
59
116
  * A server-only endpoint that helps provide the
60
117
  * OpenId configuration at the well-known endpoint.
@@ -67,7 +124,37 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
67
124
  metadata: {
68
125
  SERVER_ONLY: true;
69
126
  };
70
- }, Omit<OIDCMetadata, "id_token_signing_alg_values_supported"> & {
127
+ }, {
128
+ jwks_uri?: string | undefined;
129
+ userinfo_endpoint: string;
130
+ acr_values_supported: string[];
131
+ subject_types_supported: ("public" | "pairwise")[];
132
+ claims_supported: string[];
133
+ end_session_endpoint: string;
134
+ prompt_values_supported: Prompt[];
135
+ issuer: string;
136
+ authorization_endpoint: string;
137
+ token_endpoint: string;
138
+ registration_endpoint: string;
139
+ scopes_supported?: string[] | undefined;
140
+ response_types_supported: "code"[];
141
+ response_modes_supported: "query"[];
142
+ grant_types_supported: GrantType[];
143
+ token_endpoint_auth_methods_supported?: TokenEndpointAuthMethod[] | undefined;
144
+ token_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
145
+ service_documentation?: string | undefined;
146
+ ui_locales_supported?: string[] | undefined;
147
+ op_policy_uri?: string | undefined;
148
+ op_tos_uri?: string | undefined;
149
+ revocation_endpoint?: string | undefined;
150
+ revocation_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
151
+ revocation_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
152
+ introspection_endpoint?: string | undefined;
153
+ introspection_endpoint_auth_methods_supported?: AuthMethod[] | undefined;
154
+ introspection_endpoint_auth_signing_alg_values_supported?: better_auth0.AssertionSigningAlgorithm[] | undefined;
155
+ code_challenge_methods_supported: "S256"[];
156
+ authorization_response_iss_parameter_supported?: boolean | undefined;
157
+ client_id_metadata_document_supported?: boolean | undefined;
71
158
  id_token_signing_alg_values_supported: better_auth_plugins0.JWSAlgorithms[] | ["HS256"];
72
159
  }>;
73
160
  oauth2Authorize: better_call0.StrictEndpoint<"/oauth2/authorize", {
@@ -430,8 +517,10 @@ declare const oauthProvider: <O extends OAuthOptions<Scope[]>>(options: O) => {
430
517
  access_token: string;
431
518
  expires_in: number;
432
519
  expires_at: number;
433
- token_type: string;
520
+ token_type: "Bearer";
521
+ refresh_token: string | undefined;
434
522
  scope: string;
523
+ id_token: string | undefined;
435
524
  }>;
436
525
  oauth2Introspect: better_call0.StrictEndpoint<"/oauth2/introspect", {
437
526
  method: "POST";
@@ -315,6 +315,46 @@ type InternallySupportedScopes = "openid" | "profile" | "email" | "offline_acces
315
315
  type Scope = LiteralString | InternallySupportedScopes;
316
316
  type Prompt = "none" | "consent" | "login" | "create" | "select_account";
317
317
  type AuthorizePrompt = Prompt | "login consent" | "select_account consent";
318
+ /**
319
+ * Describes how to resolve a `client_id` from an external source (a URL-based
320
+ * metadata document, a federated registry, an attestation header, etc.) and
321
+ * what fields that source contributes to discovery metadata.
322
+ *
323
+ * Plugins install one of these onto {@link OAuthOptions.clientDiscovery}.
324
+ * The host walks the configured entries in order and returns the first
325
+ * non-null `resolve()` result.
326
+ */
327
+ interface ClientDiscovery<Scopes extends readonly Scope[] = InternallySupportedScopes[]> {
328
+ /**
329
+ * Stable identifier used in error messages and diagnostics. Convention
330
+ * is to match the plugin id (for example `"cimd"`).
331
+ */
332
+ readonly id: string;
333
+ /**
334
+ * Return `true` if this discovery handles the given `client_id`. Called
335
+ * on every `getClient()` lookup for every configured discovery, so keep
336
+ * it cheap and synchronous.
337
+ */
338
+ matches: (clientId: string) => boolean;
339
+ /**
340
+ * Resolve a client when this discovery matches. Receives the existing DB
341
+ * record (or `null`) so an implementation can decide between creating,
342
+ * refreshing, or passing through to the database result.
343
+ *
344
+ * Return:
345
+ * - a client record: `getClient()` returns it (creation / refresh / takeover).
346
+ * - `null`: `getClient()` falls through to the next matching discovery
347
+ * or to the database record (if any).
348
+ */
349
+ resolve: (ctx: GenericEndpointContext, clientId: string, existing: SchemaClient<Scopes> | null) => Awaitable<SchemaClient<Scopes> | null>;
350
+ /**
351
+ * Fields merged into `/.well-known/oauth-authorization-server` and
352
+ * `/.well-known/openid-configuration` responses. Useful for advertising
353
+ * RFC-registered discovery flags like
354
+ * `client_id_metadata_document_supported`.
355
+ */
356
+ discoveryMetadata?: Record<string, unknown>;
357
+ }
318
358
  interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScopes[]> {
319
359
  /**
320
360
  * Custom schema definitions
@@ -412,10 +452,13 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
412
452
  /**
413
453
  * Allow unauthenticated dynamic client registration.
414
454
  *
415
- * Support for `allowUnauthenticatedClientRegistration` **will be deprecated**
416
- * when the MCP protocol standardizes unauthenticated dynamic client registration.
417
- * As of writing, both [Client ID Metadata Documents](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/991)
418
- * and [`software_statement` and `jwks_uri`](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1032) are under debate.
455
+ * When enabled, the `/oauth2/register` endpoint accepts requests
456
+ * without a session, but only for public clients
457
+ * (`token_endpoint_auth_method: "none"`).
458
+ *
459
+ * For verified client discovery (MCP), consider installing the
460
+ * `@better-auth/cimd` plugin, which verifies client identity through
461
+ * domain ownership via Client ID Metadata Documents.
419
462
  *
420
463
  * @default false
421
464
  */
@@ -426,6 +469,21 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
426
469
  * @default false
427
470
  */
428
471
  allowDynamicClientRegistration?: boolean;
472
+ /**
473
+ * Discovery implementations consulted by `getClient()` when resolving
474
+ * a `client_id`. Each entry decides whether it handles the `client_id`
475
+ * via {@link ClientDiscovery.matches}, then creates, refreshes, or
476
+ * passes on a client record. Entries run in order; the first one to
477
+ * return a client wins.
478
+ *
479
+ * Each entry also contributes {@link ClientDiscovery.discoveryMetadata}
480
+ * into the `/.well-known/oauth-authorization-server` and
481
+ * `/.well-known/openid-configuration` responses.
482
+ *
483
+ * Plugins such as `@better-auth/cimd` install an entry here at init
484
+ * time; users can also pass discovery implementations directly.
485
+ */
486
+ clientDiscovery?: ClientDiscovery<Scopes> | ClientDiscovery<Scopes>[];
429
487
  /**
430
488
  * List of scopes for newly registered clients
431
489
  * if not requested.
@@ -768,6 +826,34 @@ interface OAuthOptions<Scopes extends readonly Scope[] = InternallySupportedScop
768
826
  resource?: string; /** oAuthClient metadata */
769
827
  metadata?: Record<string, any>;
770
828
  }) => Awaitable<Record<string, any>>;
829
+ /**
830
+ * Custom fields to include in the token response body.
831
+ *
832
+ * Unlike `customAccessTokenClaims` (which adds claims inside the JWT payload),
833
+ * this adds fields to the JSON response envelope alongside `access_token`,
834
+ * `token_type`, etc. Standard OAuth fields (`access_token`, `token_type`,
835
+ * `expires_in`, `expires_at`, `refresh_token`, `scope`, `id_token`) cannot
836
+ * be overridden.
837
+ *
838
+ * @param info - context that may be useful when creating custom fields
839
+ */
840
+ customTokenResponseFields?: (info: {
841
+ /** The grant type being processed */grantType: GrantType;
842
+ /**
843
+ * The user, if applicable.
844
+ * Undefined for `client_credentials` (M2M, no user).
845
+ * Always present for `authorization_code` and `refresh_token`.
846
+ */
847
+ user?: (User & Record<string, unknown>) | null; /** Scopes granted for this token */
848
+ scopes: Scopes; /** oAuthClient metadata */
849
+ metadata?: Record<string, any>;
850
+ /**
851
+ * The authorization code verification value.
852
+ * Only present for `authorization_code` grant. Contains the original
853
+ * authorization request parameters (`query`), `referenceId`, `sessionId`, etc.
854
+ */
855
+ verificationValue?: VerificationValue;
856
+ }) => Awaitable<Record<string, unknown>>;
771
857
  /**
772
858
  * Overwrite specific /.well-known/openid-configuration
773
859
  * values so they are not available publically.
@@ -1476,6 +1562,17 @@ interface AuthServerMetadata {
1476
1562
  * @default true
1477
1563
  */
1478
1564
  authorization_response_iss_parameter_supported?: boolean;
1565
+ /**
1566
+ * Whether the authorization server supports discovering clients via
1567
+ * [Client ID Metadata Documents](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)
1568
+ * (an HTTPS URL as `client_id`).
1569
+ *
1570
+ * Set at runtime by the `@better-auth/cimd` plugin (or any other
1571
+ * `ClientDiscovery` that contributes this field via
1572
+ * {@link ClientDiscovery.discoveryMetadata}). oauth-provider never sets
1573
+ * it on its own.
1574
+ */
1575
+ client_id_metadata_document_supported?: boolean;
1479
1576
  }
1480
1577
  /**
1481
1578
  * Metadata returned by the openid-configuration endpoint:
@@ -1642,4 +1739,4 @@ interface ResourceServerMetadata {
1642
1739
  dpop_bound_access_tokens_required?: boolean;
1643
1740
  }
1644
1741
  //#endregion
1645
- export { Awaitable as _, ResourceServerMetadata as a, OAuthConsent as c, OAuthRefreshToken as d, Prompt as f, VerificationValue as g, StoreTokenType as h, OIDCMetadata as i, OAuthOpaqueAccessToken as l, Scope as m, GrantType as n, AuthorizePrompt as o, SchemaClient as p, OAuthClient as r, OAuthAuthorizationQuery as s, AuthServerMetadata as t, OAuthOptions as u };
1742
+ export { Scope as _, OIDCMetadata as a, Awaitable as b, AuthorizePrompt as c, OAuthConsent as d, OAuthOpaqueAccessToken as f, SchemaClient as g, Prompt as h, OAuthClient as i, ClientDiscovery as l, OAuthRefreshToken as m, AuthServerMetadata as n, ResourceServerMetadata as o, OAuthOptions as p, GrantType as r, TokenEndpointAuthMethod as s, AuthMethod as t, OAuthAuthorizationQuery as u, StoreTokenType as v, VerificationValue as y };
@@ -89,17 +89,49 @@ async function verifyOAuthQueryParams(oauth_query, secret) {
89
89
  async function getClient(ctx, options, clientId) {
90
90
  const trustedClient = cachedTrustedClients.get(clientId);
91
91
  if (trustedClient) return Object.assign({}, trustedClient);
92
- const dbClient = await ctx.context.adapter.findOne({
92
+ let dbClient = await ctx.context.adapter.findOne({
93
93
  model: options.schema?.oauthClient?.modelName ?? "oauthClient",
94
94
  where: [{
95
95
  field: "clientId",
96
96
  value: clientId
97
97
  }]
98
98
  });
99
+ const discoveries = toClientDiscoveryArray(options.clientDiscovery);
100
+ for (const discovery of discoveries) {
101
+ if (!discovery.matches(clientId)) continue;
102
+ const resolved = await discovery.resolve(ctx, clientId, dbClient);
103
+ if (resolved) {
104
+ dbClient = resolved;
105
+ break;
106
+ }
107
+ }
99
108
  if (dbClient && options.cachedTrustedClients?.has(clientId)) cachedTrustedClients.set(clientId, Object.assign({}, dbClient));
100
109
  return dbClient;
101
110
  }
102
111
  /**
112
+ * Normalize the `clientDiscovery` option into an array. Accepts a single
113
+ * {@link ClientDiscovery}, an array of them, or `undefined`.
114
+ *
115
+ * @internal
116
+ */
117
+ function toClientDiscoveryArray(discovery) {
118
+ if (!discovery) return [];
119
+ return Array.isArray(discovery) ? discovery : [discovery];
120
+ }
121
+ /**
122
+ * Merge `discoveryMetadata` from every configured {@link ClientDiscovery}
123
+ * into a single object. Entries are spread in order; later entries override
124
+ * earlier ones on key collisions.
125
+ *
126
+ * @internal
127
+ */
128
+ function mergeDiscoveryMetadata(discovery) {
129
+ return toClientDiscoveryArray(discovery).reduce((acc, d) => ({
130
+ ...acc,
131
+ ...d.discoveryMetadata ?? {}
132
+ }), {});
133
+ }
134
+ /**
103
135
  * Default client secret hasher using SHA-256
104
136
  *
105
137
  * @internal
@@ -292,7 +324,7 @@ async function extractClientCredentials(ctx, opts, expectedAudience) {
292
324
  error_description: "client_assertion cannot be combined with client_secret or Basic auth",
293
325
  error: "invalid_client"
294
326
  });
295
- const { verifyClientAssertion: verify } = await import("./client-assertion-DZqo-L5j.mjs").then((n) => n.t);
327
+ const { verifyClientAssertion: verify } = await import("./client-assertion-CderPEmR.mjs").then((n) => n.t);
296
328
  const result = await verify(ctx, opts, body.client_assertion, body.client_assertion_type, body.client_id, expectedAudience);
297
329
  return {
298
330
  method: "private_key_jwt",
@@ -414,4 +446,4 @@ function isPKCERequired(client, requestedScopes) {
414
446
  return false;
415
447
  }
416
448
  //#endregion
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 };
449
+ export { storeClientSecret as _, getClient as a, validateClientCredentials as b, getStoredToken as c, normalizeTimestampValue as d, parseClientMetadata as f, searchParamsToQuery as g, resolveSubjectIdentifier as h, extractClientCredentials as i, isPKCERequired as l, resolveSessionAuthTime as m, deleteFromPrompt as n, getJwtPlugin as o, parsePrompt as p, destructureCredentials as r, getOAuthProviderPlugin as s, decryptStoredClientSecret as t, mergeDiscoveryMetadata as u, storeToken as v, verifyOAuthQueryParams as x, toClientDiscoveryArray as y };
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-beta.0";
3
+ const PACKAGE_VERSION = "1.7.0-beta.1";
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-beta.0",
3
+ "version": "1.7.0-beta.1",
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/core": "1.7.0-beta.0",
68
- "better-auth": "1.7.0-beta.0"
67
+ "@better-auth/core": "1.7.0-beta.1",
68
+ "better-auth": "1.7.0-beta.1"
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.7.0-beta.0",
75
- "better-auth": "^1.7.0-beta.0"
74
+ "@better-auth/core": "^1.7.0-beta.1",
75
+ "better-auth": "^1.7.0-beta.1"
76
76
  },
77
77
  "scripts": {
78
78
  "build": "tsdown",