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.
- package/dist/api/routes/account.mjs +7 -4
- package/dist/api/routes/callback.mjs +11 -3
- package/dist/api/routes/sign-in.mjs +5 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/errors.mjs +1 -0
- package/dist/oauth2/index.d.mts +2 -2
- package/dist/oauth2/index.mjs +2 -2
- package/dist/oauth2/state.d.mts +14 -1
- package/dist/oauth2/state.mjs +13 -2
- package/dist/package.mjs +1 -1
- package/dist/plugins/generic-oauth/index.mjs +11 -6
- package/dist/plugins/generic-oauth/types.d.mts +35 -1
- package/dist/plugins/oauth-popup/index.mjs +4 -0
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +1 -0
- package/dist/utils/index.d.mts +1 -1
- package/package.json +8 -8
|
@@ -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 };
|
package/dist/oauth2/errors.mjs
CHANGED
|
@@ -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",
|
package/dist/oauth2/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/oauth2/index.mjs
CHANGED
|
@@ -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 };
|
package/dist/oauth2/state.d.mts
CHANGED
|
@@ -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 };
|
package/dist/oauth2/state.mjs
CHANGED
|
@@ -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
|
@@ -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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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(
|
|
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
|
});
|
package/dist/utils/index.d.mts
CHANGED
|
@@ -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.
|
|
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.
|
|
469
|
-
"@better-auth/drizzle-adapter": "1.7.0-beta.
|
|
470
|
-
"@better-auth/kysely-adapter": "1.7.0-beta.
|
|
471
|
-
"@better-auth/memory-adapter": "1.7.0-beta.
|
|
472
|
-
"@better-auth/mongo-adapter": "1.7.0-beta.
|
|
473
|
-
"@better-auth/prisma-adapter": "1.7.0-beta.
|
|
474
|
-
"@better-auth/telemetry": "1.7.0-beta.
|
|
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",
|