better-auth 1.7.0-beta.6 → 1.7.0-beta.7

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.
@@ -7,7 +7,7 @@ import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
7
7
  import { decryptOAuthToken } from "../../oauth2/token-encryption.mjs";
8
8
  import { persistOAuthAccount } from "../../oauth2/persist-account.mjs";
9
9
  import { applyUpdateUserInfoOnLink } from "../../oauth2/resolve-account.mjs";
10
- import { generateState } from "../../oauth2/state.mjs";
10
+ import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
11
11
  import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
12
12
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
13
13
  import { additionalAuthorizationParamsSchema, readGrantedScopes, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
@@ -184,9 +184,11 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
184
184
  }
185
185
  const stateNonce = generateRandomString(32);
186
186
  const codeVerifier = generateRandomString(128);
187
+ const idTokenNonce = generateIdTokenNonce(provider);
187
188
  const { url, requestedScopes } = await provider.createAuthorizationURL({
188
189
  state: stateNonce,
189
190
  codeVerifier,
191
+ idTokenNonce,
190
192
  redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
191
193
  scopes: c.body.scopes,
192
194
  loginHint: c.body.loginHint,
@@ -200,7 +202,8 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
200
202
  additionalData: c.body.additionalData,
201
203
  requestedScopes,
202
204
  state: stateNonce,
203
- codeVerifier
205
+ codeVerifier,
206
+ idTokenNonce
204
207
  });
205
208
  if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
206
209
  return c.json({
@@ -291,7 +294,7 @@ async function getValidAccessToken(ctx, { resolvedUserId, providerId, accountId,
291
294
  const accessTokenExpired = account.accessTokenExpiresAt && new Date(account.accessTokenExpiresAt).getTime() - Date.now() < 5e3;
292
295
  if (account.refreshToken && accessTokenExpired && provider.refreshAccessToken) {
293
296
  const refreshToken = await decryptOAuthToken(account.refreshToken, ctx.context);
294
- newTokens = await provider.refreshAccessToken(refreshToken);
297
+ newTokens = await provider.refreshAccessToken(refreshToken, ctx);
295
298
  await persistOAuthAccount(ctx, {
296
299
  userId: account.userId,
297
300
  providerId: account.providerId,
@@ -420,7 +423,7 @@ const refreshToken = createAuthEndpoint("/refresh-token", {
420
423
  if (!refreshToken) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.REFRESH_TOKEN_NOT_FOUND);
421
424
  try {
422
425
  const decryptedRefreshToken = await decryptOAuthToken(refreshToken, ctx.context);
423
- const tokens = await provider.refreshAccessToken(decryptedRefreshToken);
426
+ const tokens = await provider.refreshAccessToken(decryptedRefreshToken, ctx);
424
427
  await persistOAuthAccount(ctx, {
425
428
  userId: account.userId,
426
429
  providerId: account.providerId,
@@ -6,7 +6,7 @@ import { getAwaitableValue } from "../../context/helpers.mjs";
6
6
  import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
7
7
  import { persistOAuthAccount } from "../../oauth2/persist-account.mjs";
8
8
  import { applyUpdateUserInfoOnLink } from "../../oauth2/resolve-account.mjs";
9
- import { generateState, parseState } from "../../oauth2/state.mjs";
9
+ import { generateIdTokenNonce, generateState, parseState } from "../../oauth2/state.mjs";
10
10
  import { signInWithOAuthIdentity } from "../../oauth2/sign-in-with-oauth-identity.mjs";
11
11
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
12
12
  import { safeJSONParse } from "@better-auth/core/utils/json";
@@ -60,15 +60,18 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
60
60
  if (provider?.allowIdpInitiated) {
61
61
  const state = generateRandomString(32);
62
62
  const codeVerifier = generateRandomString(128);
63
+ const idTokenNonce = generateIdTokenNonce(provider);
63
64
  const { url: authUrl, requestedScopes } = await provider.createAuthorizationURL({
64
65
  state,
65
66
  codeVerifier,
67
+ idTokenNonce,
66
68
  redirectURI: `${c.context.baseURL}${provider.callbackPath}`
67
69
  });
68
70
  await generateState(c, {
69
71
  requestedScopes,
70
72
  state,
71
- codeVerifier
73
+ codeVerifier,
74
+ idTokenNonce
72
75
  });
73
76
  throw c.redirect(authUrl.toString());
74
77
  }
@@ -78,7 +81,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
78
81
  const url = `${defaultErrorURL}${defaultErrorURL.includes("?") ? "&" : "?"}error=state_not_found`;
79
82
  throw c.redirect(url);
80
83
  }
81
- const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, requestedScopes } = await parseState(c);
84
+ const { codeVerifier, callbackURL, link, errorURL, newUserURL, requestSignUp, requestedScopes, idTokenNonce } = await parseState(c);
82
85
  function redirectOnError(error, description) {
83
86
  const baseURL = errorURL ?? defaultErrorURL;
84
87
  const params = new URLSearchParams({ error });
@@ -103,6 +106,10 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
103
106
  });
104
107
  throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.ISSUER_MISMATCH);
105
108
  }
109
+ if (provider.requiresIdTokenNonce && !idTokenNonce) {
110
+ c.context.logger.error("OAuth id_token nonce binding required but no expected nonce was found in state", { providerId: provider.id });
111
+ throw redirectOnError(OAUTH_CALLBACK_ERROR_CODES.NONCE_BINDING_MISSING);
112
+ }
106
113
  let tokens;
107
114
  try {
108
115
  tokens = await provider.validateAuthorizationCode({
@@ -119,6 +126,7 @@ const callbackOAuth = createAuthEndpoint("/callback/:id", {
119
126
  const parsedUserData = userData ? safeJSONParse(userData) : null;
120
127
  const providerResult = await provider.getUserInfo({
121
128
  ...tokens,
129
+ ...idTokenNonce ? { expectedIdTokenNonce: idTokenNonce } : {},
122
130
  user: parsedUserData ?? void 0
123
131
  });
124
132
  if (!providerResult?.user || providerResult.user.id === void 0 || providerResult.user.id === null || providerResult.user.id === "") {
@@ -4,7 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
4
4
  import { setSessionCookie } from "../../cookies/index.mjs";
5
5
  import { getAwaitableValue } from "../../context/helpers.mjs";
6
6
  import { OAUTH_CALLBACK_ERROR_CODES, missingEmailLogMessage } from "../../oauth2/errors.mjs";
7
- import { generateState } from "../../oauth2/state.mjs";
7
+ import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
8
8
  import { signInWithOAuthIdentity } from "../../oauth2/sign-in-with-oauth-identity.mjs";
9
9
  import { createEmailVerificationToken } from "./email-verification.mjs";
10
10
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
@@ -144,9 +144,11 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
144
144
  }
145
145
  const state = generateRandomString(32);
146
146
  const codeVerifier = generateRandomString(128);
147
+ const idTokenNonce = generateIdTokenNonce(provider);
147
148
  const { url, requestedScopes } = await provider.createAuthorizationURL({
148
149
  state,
149
150
  codeVerifier,
151
+ idTokenNonce,
150
152
  redirectURI: `${c.context.baseURL}${provider.callbackPath}`,
151
153
  scopes: c.body.scopes,
152
154
  loginHint: c.body.loginHint,
@@ -156,7 +158,8 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
156
158
  additionalData: c.body.additionalData,
157
159
  requestedScopes,
158
160
  state,
159
- codeVerifier
161
+ codeVerifier,
162
+ idTokenNonce
160
163
  });
161
164
  if (!c.body.disableRedirect) c.setHeader("Location", url.toString());
162
165
  return c.json({
package/dist/index.d.mts CHANGED
@@ -7,7 +7,7 @@ import { InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPlug
7
7
  import { Auth } from "./types/auth.mjs";
8
8
  import { BetterAuthAdvancedOptions, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, StoreIdentifierOption, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource } from "./types/index.mjs";
9
9
  import { betterAuth } from "./auth/full.mjs";
10
- import { GenerateStateOptions, generateState, parseState } from "./oauth2/state.mjs";
10
+ import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
11
11
  import { StateData, generateGenericState, parseGenericState } from "./state.mjs";
12
12
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
13
13
  import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
@@ -27,4 +27,4 @@ export * from "@better-auth/core/utils/json";
27
27
  export * from "@better-auth/core/social-providers";
28
28
  export * from "better-call";
29
29
  export * from "zod";
30
- export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
30
+ export { APIError, Account, AdditionalSessionFieldsInput, AdditionalUserFieldsInput, Auth, BetterAuthAdvancedOptions, BetterAuthClientOptions, BetterAuthClientPlugin, BetterAuthCookies, BetterAuthOptions, BetterAuthPlugin, BetterAuthRateLimitOptions, ClientAtomListener, ClientStore, DBAdapter, DBAdapterInstance, DBAdapterSchemaCreation, DBTransactionAdapter, ExtractPluginField, FilteredAPI, GenerateStateOptions, HIDE_METADATA, HasRequiredKeys, InferAPI, InferActions, InferAdditionalFromClient, InferClientAPI, InferErrorCodes, InferOptionSchema, InferPluginContext, InferPluginErrorCodes, InferPluginFieldFromTuple, InferPluginIDs, InferPluginTypes, InferSessionAPI, InferSessionFromClient, InferUserFromClient, IsAny, IsSignal, type JSONWebKeySet, type JWTPayload, JoinConfig, JoinOption, OverrideMerge, Prettify, PrettifyDeep, RateLimit, RequiredKeysOf, Session, SessionQueryParams, type StandardSchemaV1, StateData, StoreIdentifierOption, StripEmptyObjects, type TelemetryEvent, UnionToIntersection, User, UserProvisioningSource, ValidateUserInfoAction, ValidateUserInfoMethod, ValidateUserInfoOAuthInfo, ValidateUserInfoResult, ValidateUserInfoSSOInfo, ValidateUserInfoSource, Verification, Where, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./utils/url.mjs";
2
2
  import { generateGenericState, parseGenericState } from "./state.mjs";
3
- import { generateState, parseState } from "./oauth2/state.mjs";
3
+ import { generateIdTokenNonce, generateState, parseState } from "./oauth2/state.mjs";
4
4
  import { HIDE_METADATA } from "./utils/hide-metadata.mjs";
5
5
  import { APIError } from "./api/index.mjs";
6
6
  import { betterAuth } from "./auth/full.mjs";
@@ -14,4 +14,4 @@ export * from "@better-auth/core/oauth2";
14
14
  export * from "@better-auth/core/utils/error-codes";
15
15
  export * from "@better-auth/core/utils/id";
16
16
  export * from "@better-auth/core/utils/json";
17
- export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
17
+ export { APIError, HIDE_METADATA, betterAuth, createTelemetry, generateGenericState, generateIdTokenNonce, generateState, getBaseURL, getCurrentAdapter, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, getTelemetryAuthConfig, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, parseGenericState, parseState, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes };
@@ -11,6 +11,7 @@ const OAUTH_CALLBACK_ERROR_CODES = {
11
11
  ISSUER_MISSING: "issuer_missing",
12
12
  ISSUER_MISMATCH: "issuer_mismatch",
13
13
  INVALID_CODE: "invalid_code",
14
+ NONCE_BINDING_MISSING: "nonce_binding_missing",
14
15
  UNABLE_TO_GET_USER_INFO: "unable_to_get_user_info",
15
16
  NO_CALLBACK_URL: "no_callback_url",
16
17
  UNABLE_TO_LINK_ACCOUNT: "unable_to_link_account",
@@ -1,7 +1,7 @@
1
- import { GenerateStateOptions, generateState, parseState } from "./state.mjs";
1
+ import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "./state.mjs";
2
2
  import { PersistOAuthAccountMode, PersistOAuthAccountParams, persistOAuthAccount } from "./persist-account.mjs";
3
3
  import { OAuthLinkPolicy, ResolveOAuthUserError, ResolveOAuthUserParams, ResolvedOAuthUser, applyUpdateUserInfoOnLink, canLinkImplicitly, resolveOAuthUser } from "./resolve-account.mjs";
4
4
  import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
5
5
  import { decryptOAuthToken, setTokenUtil } from "./token-encryption.mjs";
6
6
  export * from "@better-auth/core/oauth2";
7
- export { GenerateStateOptions, OAuthLinkPolicy, PersistOAuthAccountMode, PersistOAuthAccountParams, ResolveOAuthUserError, ResolveOAuthUserParams, ResolvedOAuthUser, applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
7
+ export { GenerateStateOptions, OAuthLinkPolicy, PersistOAuthAccountMode, PersistOAuthAccountParams, ResolveOAuthUserError, ResolveOAuthUserParams, ResolvedOAuthUser, applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateIdTokenNonce, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
@@ -1,7 +1,7 @@
1
1
  import { decryptOAuthToken, setTokenUtil } from "./token-encryption.mjs";
2
2
  import { persistOAuthAccount } from "./persist-account.mjs";
3
3
  import { applyUpdateUserInfoOnLink, canLinkImplicitly, resolveOAuthUser } from "./resolve-account.mjs";
4
- import { generateState, parseState } from "./state.mjs";
4
+ import { generateIdTokenNonce, generateState, parseState } from "./state.mjs";
5
5
  import { signInWithOAuthIdentity } from "./sign-in-with-oauth-identity.mjs";
6
6
  export * from "@better-auth/core/oauth2";
7
- export { applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
7
+ export { applyUpdateUserInfoOnLink, canLinkImplicitly, decryptOAuthToken, generateIdTokenNonce, generateState, parseState, persistOAuthAccount, resolveOAuthUser, setTokenUtil, signInWithOAuthIdentity };
@@ -1,6 +1,16 @@
1
1
  import { GenericEndpointContext } from "@better-auth/core";
2
2
 
3
3
  //#region src/oauth2/state.d.ts
4
+ /**
5
+ * Mint the OIDC `nonce` for the redirect flow, or `undefined` when the provider
6
+ * does not require ID-token nonce binding. Every redirect entrypoint (social
7
+ * sign-in, account linking, IDP-initiated bounce, and the OAuth popup) mints
8
+ * through this helper, so the value sent on the authorization URL and the value
9
+ * persisted in state are produced one way and cannot drift apart.
10
+ */
11
+ declare function generateIdTokenNonce(provider: {
12
+ requiresIdTokenNonce?: boolean | undefined;
13
+ }): string | undefined;
4
14
  /**
5
15
  * Inputs for {@link generateState}. Grouped into one object so call sites read
6
16
  * by name instead of by position.
@@ -27,6 +37,8 @@ interface GenerateStateOptions {
27
37
  state?: string | undefined;
28
38
  /** The PKCE `codeVerifier` already used to build the authorization URL. Minted when omitted. */
29
39
  codeVerifier?: string | undefined;
40
+ /** The OIDC nonce already sent as the authorization URL `nonce` parameter. */
41
+ idTokenNonce?: string | undefined;
30
42
  }
31
43
  declare function generateState(c: GenericEndpointContext, options?: GenerateStateOptions): Promise<{
32
44
  state: string;
@@ -45,8 +57,9 @@ declare function parseState(c: GenericEndpointContext): Promise<{
45
57
  userId: string;
46
58
  } | undefined;
47
59
  requestSignUp?: boolean | undefined;
60
+ idTokenNonce?: string | undefined;
48
61
  requestedScopes?: string[] | undefined;
49
62
  serverContext?: Record<string, unknown> | undefined;
50
63
  }>;
51
64
  //#endregion
52
- export { GenerateStateOptions, generateState, parseState };
65
+ export { GenerateStateOptions, generateIdTokenNonce, generateState, parseState };
@@ -4,6 +4,16 @@ import { getOAuthServerContext, setOAuthState } from "../api/state/oauth.mjs";
4
4
  import { StateError, generateGenericState, parseGenericState } from "../state.mjs";
5
5
  import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
6
6
  //#region src/oauth2/state.ts
7
+ /**
8
+ * Mint the OIDC `nonce` for the redirect flow, or `undefined` when the provider
9
+ * does not require ID-token nonce binding. Every redirect entrypoint (social
10
+ * sign-in, account linking, IDP-initiated bounce, and the OAuth popup) mints
11
+ * through this helper, so the value sent on the authorization URL and the value
12
+ * persisted in state are produced one way and cannot drift apart.
13
+ */
14
+ function generateIdTokenNonce(provider) {
15
+ return provider.requiresIdTokenNonce ? generateRandomString(32) : void 0;
16
+ }
7
17
  async function generateState(c, options) {
8
18
  const callbackURL = c.body?.callbackURL || c.context.options.baseURL;
9
19
  if (!callbackURL) throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.CALLBACK_URL_REQUIRED);
@@ -20,7 +30,8 @@ async function generateState(c, options) {
20
30
  serverContext,
21
31
  expiresAt: Date.now() + 600 * 1e3,
22
32
  requestSignUp: c.body?.requestSignUp,
23
- requestedScopes: options?.requestedScopes
33
+ requestedScopes: options?.requestedScopes,
34
+ idTokenNonce: options?.idTokenNonce
24
35
  };
25
36
  await setOAuthState(stateData);
26
37
  try {
@@ -54,4 +65,4 @@ async function parseState(c) {
54
65
  return parsedData;
55
66
  }
56
67
  //#endregion
57
- export { generateState, parseState };
68
+ export { generateIdTokenNonce, generateState, parseState };
package/dist/package.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "1.7.0-beta.6";
2
+ var version = "1.7.0-beta.7";
3
3
  //#endregion
4
4
  export { version };
@@ -125,6 +125,7 @@ const genericOAuth = (options) => {
125
125
  callbackPath: `/callback/${c.providerId}`,
126
126
  issuer,
127
127
  idToken: idTokenConfig,
128
+ requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
128
129
  allowIdpInitiated: c.allowIdpInitiated,
129
130
  createAuthorizationURL(data) {
130
131
  if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
@@ -148,6 +149,7 @@ const genericOAuth = (options) => {
148
149
  accessType: c.accessType,
149
150
  responseType: c.responseType,
150
151
  responseMode: c.responseMode,
152
+ nonce: data.idTokenNonce,
151
153
  additionalParams: {
152
154
  ...c.authorizationUrlParams ?? {},
153
155
  ...data.additionalParams ?? {}
@@ -175,13 +177,14 @@ const genericOAuth = (options) => {
175
177
  }), c.accessTokenExpiresIn);
176
178
  },
177
179
  async getUserInfo(tokens) {
178
- if (tokens.idToken && provider.idToken) {
179
- if (!await verifyProviderIdToken(provider, tokens.idToken)) {
180
- ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS`);
180
+ const { expectedIdTokenNonce, ...oauthTokens } = tokens;
181
+ if (oauthTokens.idToken && provider.idToken) {
182
+ if (!await verifyProviderIdToken(provider, oauthTokens.idToken, expectedIdTokenNonce)) {
183
+ ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS or expected nonce`);
181
184
  return null;
182
185
  }
183
186
  }
184
- const raw = c.getUserInfo ? await c.getUserInfo(tokens) : await fetchUserInfo(tokens, userInfoUrl);
187
+ const raw = c.getUserInfo ? await c.getUserInfo(oauthTokens) : await fetchUserInfo(oauthTokens, userInfoUrl);
185
188
  if (!raw) return null;
186
189
  const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
187
190
  const rawId = isNonEmptyOAuthId(mapped.id) ? mapped.id : isNonEmptyOAuthId(raw.id) ? raw.id : isNonEmptyOAuthId(raw.sub) ? raw.sub : void 0;
@@ -202,8 +205,9 @@ const genericOAuth = (options) => {
202
205
  data: raw
203
206
  };
204
207
  },
205
- async refreshAccessToken(refreshToken) {
208
+ async refreshAccessToken(refreshToken, refreshCtx) {
206
209
  if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
210
+ const resolvedRefreshParams = typeof c.refreshTokenParams === "function" ? await c.refreshTokenParams(refreshCtx) : c.refreshTokenParams;
207
211
  return applyDefaultAccessTokenExpiry(await refreshAccessToken({
208
212
  refreshToken,
209
213
  options: {
@@ -212,7 +216,8 @@ const genericOAuth = (options) => {
212
216
  },
213
217
  authentication: c.authentication,
214
218
  tokenEndpointAuth,
215
- tokenEndpoint: tokenUrl
219
+ tokenEndpoint: tokenUrl,
220
+ extraParams: resolvedRefreshParams
216
221
  }), c.accessTokenExpiresIn);
217
222
  },
218
223
  disableImplicitSignUp: c.disableImplicitSignUp,
@@ -1,5 +1,6 @@
1
+ import { Awaitable } from "@better-auth/core";
1
2
  import { User } from "@better-auth/core/db";
2
- import { OAuth2Tokens, OAuth2UserInfo, TokenEndpointAuth } from "@better-auth/core/oauth2";
3
+ import { OAuth2Tokens, OAuth2UserInfo, OAuthRefreshContext, TokenEndpointAuth } from "@better-auth/core/oauth2";
3
4
 
4
5
  //#region src/plugins/generic-oauth/types.d.ts
5
6
  type GenericOAuthUserInfo = Omit<OAuth2UserInfo, "id"> & {
@@ -136,6 +137,26 @@ interface GenericOAuthConfig<ID extends string = string> {
136
137
  * tokenEndpointAuth.
137
138
  */
138
139
  tokenUrlParams?: Record<string, string> | undefined;
140
+ /**
141
+ * Additional body params merged into the token endpoint request when
142
+ * refreshing an access token. Useful for multi-tenant OIDC providers that
143
+ * need to change `scope`, `audience`, `resource`, or a tenant identifier on
144
+ * the refresh call — e.g. Zitadel's `urn:zitadel:iam:org:id:{orgId}` scope
145
+ * on workspace switch or Auth0 `audience` rotation — without forcing a new
146
+ * authorization redirect.
147
+ *
148
+ * The function form is invoked at refresh time and receives request
149
+ * metadata for the triggering request, so dynamic
150
+ * per-request values like an active organization id read from cookies or
151
+ * headers can be injected directly. Headers and cookies are
152
+ * attacker-controlled: callers MUST validate any value derived from them
153
+ * against the authenticated user's entitlements before forwarding it as a
154
+ * `scope`, `audience`, or tenant claim. Resolved values are merged into the
155
+ * form body; `grant_type` and `refresh_token` are protected from override,
156
+ * and `client_id` is set by the configured token-endpoint authentication
157
+ * after the merge so it cannot be overridden here.
158
+ */
159
+ refreshTokenParams?: Record<string, string> | ((ctx?: OAuthRefreshContext) => Awaitable<Record<string, string> | undefined>) | undefined;
139
160
  /**
140
161
  * Disable implicit sign up for new users. When set to true for the provider,
141
162
  * sign-in need to be called with with requestSignUp as true to create new users.
@@ -191,6 +212,19 @@ interface GenericOAuthConfig<ID extends string = string> {
191
212
  * @default false
192
213
  */
193
214
  allowIdpInitiated?: boolean | undefined;
215
+ /**
216
+ * Disable OIDC `nonce` binding for this provider's `id_token`.
217
+ *
218
+ * Providers configured with `discoveryUrl` that publish a JWKS bind the
219
+ * `id_token` to the authorization request by default: Better Auth sends a
220
+ * server-generated `nonce` and rejects a callback whose `id_token` does not
221
+ * echo it (OIDC Core 1.0 §3.1.3.7). Set this to `true` only for OIDC
222
+ * providers that do not return the `nonce` claim in the authorization-code
223
+ * flow; doing so removes `id_token` replay protection for this provider.
224
+ *
225
+ * @default false
226
+ */
227
+ disableIdTokenNonceBinding?: boolean | undefined;
194
228
  }
195
229
  //#endregion
196
230
  export { GenericOAuthConfig, GenericOAuthOptions, GenericOAuthUserInfo };
@@ -4,6 +4,7 @@ import { expireCookie } from "../../cookies/index.mjs";
4
4
  import { getAwaitableValue } from "../../context/helpers.mjs";
5
5
  import { setOAuthState } from "../../api/state/oauth.mjs";
6
6
  import { INTERNAL_STATE_KEYS, generateGenericState } from "../../state.mjs";
7
+ import { generateIdTokenNonce } from "../../oauth2/state.mjs";
7
8
  import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
8
9
  import { PACKAGE_VERSION } from "../../version.mjs";
9
10
  import { OAUTH_POPUP_DATA_ELEMENT_ID, OAUTH_POPUP_MESSAGE_TYPE, POPUP_MARKER_COOKIE } from "./constants.mjs";
@@ -132,11 +133,13 @@ const oauthPopupStart = createAuthEndpoint("/oauth-popup/start", {
132
133
  let url;
133
134
  try {
134
135
  const codeVerifier = generateRandomString(128);
136
+ const idTokenNonce = generateIdTokenNonce(provider);
135
137
  const parsedAdditionalData = c.query.additionalData ? safeJSONParse(c.query.additionalData) ?? {} : {};
136
138
  const stateData = {
137
139
  ...Object.fromEntries(Object.entries(parsedAdditionalData).filter(([key]) => !INTERNAL_STATE_KEYS.has(key))),
138
140
  callbackURL,
139
141
  codeVerifier,
142
+ idTokenNonce,
140
143
  errorURL: c.query.errorCallbackURL,
141
144
  newUserURL: c.query.newUserCallbackURL,
142
145
  requestSignUp: c.query.requestSignUp === "true" ? true : void 0,
@@ -152,6 +155,7 @@ const oauthPopupStart = createAuthEndpoint("/oauth-popup/start", {
152
155
  ({url} = await provider.createAuthorizationURL({
153
156
  state,
154
157
  codeVerifier,
158
+ idTokenNonce,
155
159
  redirectURI: `${c.context.baseURL}/callback/${provider.id}`,
156
160
  scopes: c.query.scopes ? c.query.scopes.split(",") : void 0
157
161
  }));
package/dist/state.d.mts CHANGED
@@ -19,6 +19,11 @@ declare const stateDataSchema: z.ZodObject<{
19
19
  userId: z.ZodCoercedString<unknown>;
20
20
  }, z.core.$strip>>;
21
21
  requestSignUp: z.ZodOptional<z.ZodBoolean>;
22
+ /**
23
+ * OIDC nonce sent as the authorization request `nonce` parameter when the
24
+ * provider requires an ID token to be bound to this redirect flow.
25
+ */
26
+ idTokenNonce: z.ZodOptional<z.ZodString>;
22
27
  /**
23
28
  * The effective set of scopes requested in the authorization URL, captured
24
29
  * from `createAuthorizationURL`. Persisted so the callback can fall back to
@@ -57,6 +62,7 @@ declare function parseGenericState(c: GenericEndpointContext, state: string | un
57
62
  userId: string;
58
63
  } | undefined;
59
64
  requestSignUp?: boolean | undefined;
65
+ idTokenNonce?: string | undefined;
60
66
  requestedScopes?: string[] | undefined;
61
67
  serverContext?: Record<string, unknown> | undefined;
62
68
  }>;
package/dist/state.mjs CHANGED
@@ -16,6 +16,7 @@ const stateDataSchema = z.looseObject({
16
16
  userId: z.coerce.string()
17
17
  }).optional(),
18
18
  requestSignUp: z.boolean().optional(),
19
+ idTokenNonce: z.string().optional(),
19
20
  requestedScopes: z.array(z.string()).optional(),
20
21
  serverContext: z.record(z.string(), z.unknown()).optional()
21
22
  });
@@ -1,4 +1,4 @@
1
- import { GenerateStateOptions, generateState, parseState } from "../oauth2/state.mjs";
1
+ import { GenerateStateOptions, generateIdTokenNonce, generateState, parseState } from "../oauth2/state.mjs";
2
2
  import { StateData, generateGenericState, parseGenericState } from "../state.mjs";
3
3
  import { HIDE_METADATA } from "./hide-metadata.mjs";
4
4
  import { getBaseURL, getHost, getHostFromSource, getOrigin, getProtocol, getProtocolFromSource, isDynamicBaseURLConfig, isRequestLike, matchesHostPattern, resolveBaseURL, resolveDynamicBaseURL, trimTrailingSlashes } from "./url.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-auth",
3
- "version": "1.7.0-beta.6",
3
+ "version": "1.7.0-beta.7",
4
4
  "description": "The most comprehensive authentication framework for TypeScript.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -465,13 +465,13 @@
465
465
  "kysely": "^0.28.17 || ^0.29.0",
466
466
  "nanostores": "^1.1.1",
467
467
  "zod": "^4.3.6",
468
- "@better-auth/core": "1.7.0-beta.6",
469
- "@better-auth/drizzle-adapter": "1.7.0-beta.6",
470
- "@better-auth/kysely-adapter": "1.7.0-beta.6",
471
- "@better-auth/memory-adapter": "1.7.0-beta.6",
472
- "@better-auth/mongo-adapter": "1.7.0-beta.6",
473
- "@better-auth/prisma-adapter": "1.7.0-beta.6",
474
- "@better-auth/telemetry": "1.7.0-beta.6"
468
+ "@better-auth/core": "1.7.0-beta.7",
469
+ "@better-auth/drizzle-adapter": "1.7.0-beta.7",
470
+ "@better-auth/kysely-adapter": "1.7.0-beta.7",
471
+ "@better-auth/memory-adapter": "1.7.0-beta.7",
472
+ "@better-auth/mongo-adapter": "1.7.0-beta.7",
473
+ "@better-auth/prisma-adapter": "1.7.0-beta.7",
474
+ "@better-auth/telemetry": "1.7.0-beta.7"
475
475
  },
476
476
  "devDependencies": {
477
477
  "@lynx-js/react": "^0.116.3",