better-auth 1.7.7 → 1.7.8
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/password.mjs +5 -11
- package/dist/api/routes/sign-in.mjs +0 -1
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -5
- package/dist/cookies/index.d.mts +5 -5
- package/dist/db/get-migration.mjs +13 -11
- package/dist/db/get-schema.mjs +27 -11
- package/dist/db/internal-adapter.mjs +73 -56
- package/dist/db/to-zod.d.mts +8 -6
- package/dist/db/to-zod.mjs +12 -2
- package/dist/index.mjs +0 -1
- package/dist/package.mjs +1 -1
- package/dist/plugins/custom-session/index.mjs +6 -13
- package/dist/plugins/email-otp/routes.mjs +19 -10
- package/dist/plugins/generic-oauth/index.d.mts +13 -0
- package/dist/plugins/generic-oauth/index.mjs +232 -189
- package/dist/plugins/last-login-method/index.mjs +3 -3
- package/dist/plugins/magic-link/index.mjs +6 -12
- package/dist/plugins/open-api/index.mjs +0 -1
- package/dist/plugins/organization/routes/crud-members.d.mts +1 -42
- package/dist/plugins/organization/routes/crud-members.mjs +1 -1
- package/dist/plugins/phone-number/routes.mjs +33 -16
- package/dist/utils/password.mjs +33 -1
- package/package.json +13 -13
- package/dist/utils/index.mjs +0 -5
|
@@ -12,6 +12,7 @@ import { slack } from "./providers/slack.mjs";
|
|
|
12
12
|
import { yandex } from "./providers/yandex.mjs";
|
|
13
13
|
import { APIError } from "@better-auth/core/error";
|
|
14
14
|
import { applyDefaultAccessTokenExpiry, createAuthorizationURL, refreshAccessToken, validateAuthorizationCode, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
15
|
+
import { createAuthMiddleware } from "@better-auth/core/api";
|
|
15
16
|
import { createRemoteJWKSet, decodeJwt } from "jose";
|
|
16
17
|
import { betterFetch } from "@better-fetch/fetch";
|
|
17
18
|
//#region src/plugins/generic-oauth/index.ts
|
|
@@ -21,10 +22,20 @@ function isSecretlessTokenEndpointAuth(tokenEndpointAuth) {
|
|
|
21
22
|
function isClientSecretTokenEndpointAuth(tokenEndpointAuth) {
|
|
22
23
|
return tokenEndpointAuth?.method === "client_secret_basic" || tokenEndpointAuth?.method === "client_secret_post";
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
+
/** A request waits on a discovery retry, so bound it (the SSO plugin uses 10s too). */
|
|
26
|
+
const DISCOVERY_RETRY_TIMEOUT_MS = 1e4;
|
|
27
|
+
/**
|
|
28
|
+
* Providers skipped because startup discovery failed. Init stores them on each auth
|
|
29
|
+
* instance's context, so one plugin object can serve several instances. It is a context
|
|
30
|
+
* field rather than a WeakMap because requests see copies of the context, and a later
|
|
31
|
+
* plugin may replace `socialProviders`.
|
|
32
|
+
*/
|
|
33
|
+
const getPendingDiscovery = (ctx) => ctx.genericOAuthPendingDiscovery;
|
|
34
|
+
async function fetchDiscovery(url, headers, timeout) {
|
|
25
35
|
const result = await betterFetch(url, {
|
|
26
36
|
method: "GET",
|
|
27
|
-
headers
|
|
37
|
+
headers,
|
|
38
|
+
signal: timeout ? AbortSignal.timeout(timeout) : void 0
|
|
28
39
|
});
|
|
29
40
|
if (result.error || !result.data) return null;
|
|
30
41
|
if (result.data.issuer) try {
|
|
@@ -74,203 +85,235 @@ const genericOAuth = (options) => {
|
|
|
74
85
|
seenIds.add(id);
|
|
75
86
|
}
|
|
76
87
|
if (nonUniqueIds.size > 0) console.warn(`Duplicate provider IDs found: ${Array.from(nonUniqueIds).join(", ")}`);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
} catch {
|
|
108
|
-
ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
idTokenConfig = {
|
|
112
|
-
jwks: createRemoteJWKSet(jwksUrl),
|
|
113
|
-
issuer: discovered.issuer,
|
|
114
|
-
audience: c.clientId,
|
|
115
|
-
algorithms: isOidc ? signingAlgs : void 0
|
|
116
|
-
};
|
|
88
|
+
const buildProviders = async (configs, ctx, discoveryTimeout) => {
|
|
89
|
+
const genericProviders = [];
|
|
90
|
+
for (const c of configs) {
|
|
91
|
+
let authorizationUrl = c.authorizationUrl;
|
|
92
|
+
let tokenUrl = c.tokenUrl;
|
|
93
|
+
let userInfoUrl = c.userInfoUrl;
|
|
94
|
+
let endSessionEndpoint = c.endSessionEndpoint;
|
|
95
|
+
let issuer;
|
|
96
|
+
let isOidc = false;
|
|
97
|
+
let idTokenConfig;
|
|
98
|
+
if (c.discoveryUrl) {
|
|
99
|
+
const discovered = await fetchDiscovery(c.discoveryUrl, c.discoveryHeaders, discoveryTimeout).catch((err) => {
|
|
100
|
+
ctx.logger.error(`Discovery fetch failed for "${c.providerId}": ${err}`);
|
|
101
|
+
return null;
|
|
102
|
+
});
|
|
103
|
+
if (discovered) {
|
|
104
|
+
authorizationUrl ??= discovered.authorization_endpoint;
|
|
105
|
+
tokenUrl ??= discovered.token_endpoint;
|
|
106
|
+
userInfoUrl ??= discovered.userinfo_endpoint;
|
|
107
|
+
endSessionEndpoint ??= discovered.end_session_endpoint;
|
|
108
|
+
issuer = discovered.issuer;
|
|
109
|
+
const signingAlgs = discovered.id_token_signing_alg_values_supported;
|
|
110
|
+
isOidc = Array.isArray(signingAlgs) && signingAlgs.length > 0;
|
|
111
|
+
if (discovered.jwks_uri && discovered.issuer) {
|
|
112
|
+
let jwksUrl;
|
|
113
|
+
try {
|
|
114
|
+
jwksUrl = new URL(discovered.jwks_uri, c.discoveryUrl);
|
|
115
|
+
} catch {
|
|
116
|
+
ctx.logger.error(`Provider "${c.providerId}": invalid jwks_uri "${discovered.jwks_uri}" in discovery document. Provider skipped.`);
|
|
117
|
+
continue;
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
idTokenConfig = {
|
|
120
|
+
jwks: createRemoteJWKSet(jwksUrl),
|
|
121
|
+
issuer: discovered.issuer,
|
|
122
|
+
audience: c.clientId,
|
|
123
|
+
algorithms: isOidc ? signingAlgs : void 0
|
|
124
|
+
};
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
|
|
127
|
+
if (!authorizationUrl || !tokenUrl && !c.getToken) {
|
|
128
|
+
ctx.logger.error(`Provider "${c.providerId}": discovery left no usable authorization endpoint or token exchange. Provider skipped.`);
|
|
129
|
+
continue;
|
|
130
130
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
131
|
+
}
|
|
132
|
+
if (c.requireIdTokenVerification && !idTokenConfig) {
|
|
133
|
+
if (c.discoveryUrl) {
|
|
134
|
+
ctx.logger.error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri. Provider skipped.`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
throw new Error(`Provider "${c.providerId}": requires verified ID tokens, but discovery did not provide a usable issuer and jwks_uri.`);
|
|
138
|
+
}
|
|
139
|
+
const tokenEndpointAuth = c.tokenEndpointAuth;
|
|
140
|
+
if (c.clientSecret && isSecretlessTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" cannot be combined with clientSecret`);
|
|
141
|
+
if (!c.clientSecret && isClientSecretTokenEndpointAuth(tokenEndpointAuth)) throw new Error(`Provider "${c.providerId}": tokenEndpointAuth.method "${tokenEndpointAuth?.method}" requires clientSecret`);
|
|
142
|
+
if (!c.clientSecret && !tokenEndpointAuth && c.authentication === "basic") throw new Error(`Provider "${c.providerId}": authentication "basic" requires clientSecret`);
|
|
143
|
+
const accountSubject = c.accountSubject;
|
|
144
|
+
const provider = {
|
|
145
|
+
id: c.providerId,
|
|
146
|
+
name: c.name ?? c.providerId,
|
|
147
|
+
issuer,
|
|
148
|
+
accountSubject: ({ tokens, profile }) => {
|
|
149
|
+
const genericProfile = profile;
|
|
150
|
+
if (accountSubject) return accountSubject({
|
|
151
|
+
tokens,
|
|
152
|
+
profile: genericProfile
|
|
153
|
+
});
|
|
154
|
+
return isOidc ? genericProfile.sub ?? "" : genericProfile.id ?? "";
|
|
155
|
+
},
|
|
156
|
+
idToken: idTokenConfig,
|
|
157
|
+
requiresIdTokenNonce: idTokenConfig !== void 0 && c.disableIdTokenNonceBinding !== true,
|
|
158
|
+
allowIdpInitiated: c.allowIdpInitiated,
|
|
159
|
+
async createEndSessionURL(data) {
|
|
160
|
+
if (c.disableProviderLogout) return null;
|
|
161
|
+
if (!endSessionEndpoint) return null;
|
|
162
|
+
let url;
|
|
163
|
+
try {
|
|
164
|
+
url = new URL(endSessionEndpoint);
|
|
165
|
+
} catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
|
|
169
|
+
const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
|
|
170
|
+
const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
|
|
171
|
+
if (postLogoutRedirectURI) {
|
|
172
|
+
url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
|
|
173
|
+
url.searchParams.set("client_id", c.clientId);
|
|
174
|
+
if (data.state) url.searchParams.set("state", data.state);
|
|
175
|
+
} else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
|
|
176
|
+
return url;
|
|
177
|
+
},
|
|
178
|
+
createAuthorizationURL(data) {
|
|
179
|
+
if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
|
|
180
|
+
return createAuthorizationURL({
|
|
181
|
+
id: c.providerId,
|
|
182
|
+
options: {
|
|
183
|
+
clientId: c.clientId,
|
|
184
|
+
clientSecret: c.clientSecret,
|
|
185
|
+
redirectURI: c.redirectURI
|
|
186
|
+
},
|
|
187
|
+
authorizationEndpoint: authorizationUrl,
|
|
188
|
+
state: data.state,
|
|
189
|
+
codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
|
|
190
|
+
scopes: (() => {
|
|
191
|
+
const merged = [...data.scopes ?? [], ...c.scopes ?? []];
|
|
192
|
+
if (isOidc && !merged.includes("openid")) merged.unshift("openid");
|
|
193
|
+
return merged;
|
|
194
|
+
})(),
|
|
195
|
+
redirectURI: data.redirectURI,
|
|
196
|
+
prompt: c.prompt,
|
|
197
|
+
accessType: c.accessType,
|
|
198
|
+
responseType: c.responseType,
|
|
199
|
+
responseMode: c.responseMode,
|
|
200
|
+
nonce: data.idTokenNonce,
|
|
201
|
+
additionalParams: {
|
|
202
|
+
...c.authorizationUrlParams ?? {},
|
|
203
|
+
...data.additionalParams ?? {}
|
|
204
|
+
},
|
|
205
|
+
loginHint: data.loginHint
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
async validateAuthorizationCode(data) {
|
|
209
|
+
if (c.getToken) return applyDefaultAccessTokenExpiry(await c.getToken(data), c.accessTokenExpiresIn);
|
|
210
|
+
if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
211
|
+
return applyDefaultAccessTokenExpiry(await validateAuthorizationCode({
|
|
212
|
+
headers: c.authorizationHeaders,
|
|
213
|
+
code: data.code,
|
|
214
|
+
codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
|
|
215
|
+
redirectURI: data.redirectURI,
|
|
216
|
+
options: {
|
|
217
|
+
clientId: c.clientId,
|
|
218
|
+
clientSecret: c.clientSecret,
|
|
219
|
+
redirectURI: c.redirectURI
|
|
220
|
+
},
|
|
221
|
+
tokenEndpoint: tokenUrl,
|
|
222
|
+
authentication: c.authentication,
|
|
223
|
+
tokenEndpointAuth,
|
|
224
|
+
additionalParams: c.tokenUrlParams
|
|
225
|
+
}), c.accessTokenExpiresIn);
|
|
226
|
+
},
|
|
227
|
+
async getUserInfo(tokens) {
|
|
228
|
+
const { expectedIdTokenNonce, ...oauthTokens } = tokens;
|
|
229
|
+
if (oauthTokens.idToken && provider.idToken) {
|
|
230
|
+
if (!await verifyProviderIdToken(provider, oauthTokens.idToken, expectedIdTokenNonce)) {
|
|
231
|
+
ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS or expected nonce`);
|
|
158
232
|
return null;
|
|
159
233
|
}
|
|
160
|
-
if (data.idToken) url.searchParams.set("id_token_hint", data.idToken);
|
|
161
|
-
const configuredRedirectURI = data.postLogoutRedirectURI ?? c.postLogoutRedirectURI;
|
|
162
|
-
const postLogoutRedirectURI = configuredRedirectURI ? new URL(configuredRedirectURI, ctx.baseURL).toString() : void 0;
|
|
163
|
-
if (postLogoutRedirectURI) {
|
|
164
|
-
url.searchParams.set("post_logout_redirect_uri", postLogoutRedirectURI);
|
|
165
|
-
url.searchParams.set("client_id", c.clientId);
|
|
166
|
-
if (data.state) url.searchParams.set("state", data.state);
|
|
167
|
-
} else if (!data.idToken) url.searchParams.set("client_id", c.clientId);
|
|
168
|
-
return url;
|
|
169
|
-
},
|
|
170
|
-
createAuthorizationURL(data) {
|
|
171
|
-
if (!authorizationUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.INVALID_OAUTH_CONFIGURATION);
|
|
172
|
-
return createAuthorizationURL({
|
|
173
|
-
id: c.providerId,
|
|
174
|
-
options: {
|
|
175
|
-
clientId: c.clientId,
|
|
176
|
-
clientSecret: c.clientSecret,
|
|
177
|
-
redirectURI: c.redirectURI
|
|
178
|
-
},
|
|
179
|
-
authorizationEndpoint: authorizationUrl,
|
|
180
|
-
state: data.state,
|
|
181
|
-
codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
|
|
182
|
-
scopes: (() => {
|
|
183
|
-
const merged = [...data.scopes ?? [], ...c.scopes ?? []];
|
|
184
|
-
if (isOidc && !merged.includes("openid")) merged.unshift("openid");
|
|
185
|
-
return merged;
|
|
186
|
-
})(),
|
|
187
|
-
redirectURI: data.redirectURI,
|
|
188
|
-
prompt: c.prompt,
|
|
189
|
-
accessType: c.accessType,
|
|
190
|
-
responseType: c.responseType,
|
|
191
|
-
responseMode: c.responseMode,
|
|
192
|
-
nonce: data.idTokenNonce,
|
|
193
|
-
additionalParams: {
|
|
194
|
-
...c.authorizationUrlParams ?? {},
|
|
195
|
-
...data.additionalParams ?? {}
|
|
196
|
-
},
|
|
197
|
-
loginHint: data.loginHint
|
|
198
|
-
});
|
|
199
|
-
},
|
|
200
|
-
async validateAuthorizationCode(data) {
|
|
201
|
-
if (c.getToken) return applyDefaultAccessTokenExpiry(await c.getToken(data), c.accessTokenExpiresIn);
|
|
202
|
-
if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
203
|
-
return applyDefaultAccessTokenExpiry(await validateAuthorizationCode({
|
|
204
|
-
headers: c.authorizationHeaders,
|
|
205
|
-
code: data.code,
|
|
206
|
-
codeVerifier: c.pkce ?? true ? data.codeVerifier : void 0,
|
|
207
|
-
redirectURI: data.redirectURI,
|
|
208
|
-
options: {
|
|
209
|
-
clientId: c.clientId,
|
|
210
|
-
clientSecret: c.clientSecret,
|
|
211
|
-
redirectURI: c.redirectURI
|
|
212
|
-
},
|
|
213
|
-
tokenEndpoint: tokenUrl,
|
|
214
|
-
authentication: c.authentication,
|
|
215
|
-
tokenEndpointAuth,
|
|
216
|
-
additionalParams: c.tokenUrlParams
|
|
217
|
-
}), c.accessTokenExpiresIn);
|
|
218
|
-
},
|
|
219
|
-
async getUserInfo(tokens) {
|
|
220
|
-
const { expectedIdTokenNonce, ...oauthTokens } = tokens;
|
|
221
|
-
if (oauthTokens.idToken && provider.idToken) {
|
|
222
|
-
if (!await verifyProviderIdToken(provider, oauthTokens.idToken, expectedIdTokenNonce)) {
|
|
223
|
-
ctx.logger.error(`Provider "${c.providerId}": id_token failed verification against the discovery JWKS or expected nonce`);
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
const raw = c.getUserInfo ? await c.getUserInfo(oauthTokens) : await fetchUserInfo(oauthTokens, userInfoUrl);
|
|
228
|
-
if (!raw) return null;
|
|
229
|
-
const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
|
|
230
|
-
const user = {
|
|
231
|
-
email: raw.email,
|
|
232
|
-
emailVerified: raw.emailVerified,
|
|
233
|
-
image: raw.image,
|
|
234
|
-
name: raw.name,
|
|
235
|
-
...mapped
|
|
236
|
-
};
|
|
237
|
-
return {
|
|
238
|
-
user: {
|
|
239
|
-
...user,
|
|
240
|
-
image: user.image ?? void 0
|
|
241
|
-
},
|
|
242
|
-
data: raw
|
|
243
|
-
};
|
|
244
|
-
},
|
|
245
|
-
async refreshAccessToken(refreshToken, refreshCtx) {
|
|
246
|
-
if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
247
|
-
const resolvedRefreshParams = typeof c.refreshTokenParams === "function" ? await c.refreshTokenParams(refreshCtx) : c.refreshTokenParams;
|
|
248
|
-
return applyDefaultAccessTokenExpiry(await refreshAccessToken({
|
|
249
|
-
refreshToken,
|
|
250
|
-
options: {
|
|
251
|
-
clientId: c.clientId,
|
|
252
|
-
clientSecret: c.clientSecret
|
|
253
|
-
},
|
|
254
|
-
authentication: c.authentication,
|
|
255
|
-
tokenEndpointAuth,
|
|
256
|
-
tokenEndpoint: tokenUrl,
|
|
257
|
-
extraParams: resolvedRefreshParams
|
|
258
|
-
}), c.accessTokenExpiresIn);
|
|
259
|
-
},
|
|
260
|
-
disableImplicitSignUp: c.disableImplicitSignUp,
|
|
261
|
-
disableSignUp: c.disableSignUp,
|
|
262
|
-
options: {
|
|
263
|
-
disableSignUp: c.disableSignUp,
|
|
264
|
-
overrideUserInfoOnSignIn: c.overrideUserInfo,
|
|
265
|
-
requireEmailVerification: c.requireEmailVerification
|
|
266
234
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
235
|
+
const raw = c.getUserInfo ? await c.getUserInfo(oauthTokens) : await fetchUserInfo(oauthTokens, userInfoUrl);
|
|
236
|
+
if (!raw) return null;
|
|
237
|
+
const mapped = c.mapProfileToUser ? await c.mapProfileToUser(raw) : {};
|
|
238
|
+
const user = {
|
|
239
|
+
email: raw.email,
|
|
240
|
+
emailVerified: raw.emailVerified,
|
|
241
|
+
image: raw.image,
|
|
242
|
+
name: raw.name,
|
|
243
|
+
...mapped
|
|
244
|
+
};
|
|
245
|
+
return {
|
|
246
|
+
user: {
|
|
247
|
+
...user,
|
|
248
|
+
image: user.image ?? void 0
|
|
249
|
+
},
|
|
250
|
+
data: raw
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
async refreshAccessToken(refreshToken, refreshCtx) {
|
|
254
|
+
if (!tokenUrl) throw APIError.from("BAD_REQUEST", GENERIC_OAUTH_ERROR_CODES.TOKEN_URL_NOT_FOUND);
|
|
255
|
+
const resolvedRefreshParams = typeof c.refreshTokenParams === "function" ? await c.refreshTokenParams(refreshCtx) : c.refreshTokenParams;
|
|
256
|
+
return applyDefaultAccessTokenExpiry(await refreshAccessToken({
|
|
257
|
+
refreshToken,
|
|
258
|
+
options: {
|
|
259
|
+
clientId: c.clientId,
|
|
260
|
+
clientSecret: c.clientSecret
|
|
261
|
+
},
|
|
262
|
+
authentication: c.authentication,
|
|
263
|
+
tokenEndpointAuth,
|
|
264
|
+
tokenEndpoint: tokenUrl,
|
|
265
|
+
extraParams: resolvedRefreshParams
|
|
266
|
+
}), c.accessTokenExpiresIn);
|
|
267
|
+
},
|
|
268
|
+
disableImplicitSignUp: c.disableImplicitSignUp,
|
|
269
|
+
disableSignUp: c.disableSignUp,
|
|
270
|
+
options: {
|
|
271
|
+
disableSignUp: c.disableSignUp,
|
|
272
|
+
overrideUserInfoOnSignIn: c.overrideUserInfo,
|
|
273
|
+
requireEmailVerification: c.requireEmailVerification
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
genericProviders.push(provider);
|
|
277
|
+
}
|
|
278
|
+
return genericProviders;
|
|
279
|
+
};
|
|
280
|
+
const retryDiscovery = (ctx, providerId) => {
|
|
281
|
+
const pendingDiscovery = getPendingDiscovery(ctx);
|
|
282
|
+
const pending = pendingDiscovery?.get(providerId);
|
|
283
|
+
if (!pending) return;
|
|
284
|
+
pending.attempt ??= buildProviders([pending.config], ctx, DISCOVERY_RETRY_TIMEOUT_MS).then(([provider]) => {
|
|
285
|
+
if (!provider) return;
|
|
286
|
+
pendingDiscovery?.delete(providerId);
|
|
287
|
+
ctx.socialProviders.unshift(provider);
|
|
288
|
+
}).finally(() => {
|
|
289
|
+
pending.attempt = void 0;
|
|
290
|
+
});
|
|
291
|
+
return pending.attempt;
|
|
292
|
+
};
|
|
293
|
+
const requestedProviderId = (context) => {
|
|
294
|
+
const id = context.body?.provider ?? context.body?.providerId ?? context.query?.provider ?? context.params?.id;
|
|
295
|
+
return typeof id === "string" ? id : void 0;
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
id: "generic-oauth",
|
|
299
|
+
version: PACKAGE_VERSION,
|
|
300
|
+
init: async (ctx) => {
|
|
301
|
+
const genericProviders = await buildProviders(options.config, ctx);
|
|
302
|
+
const built = new Set(genericProviders.map((p) => p.id));
|
|
303
|
+
const genericOAuthPendingDiscovery = new Map(options.config.filter((c) => !built.has(c.providerId)).map((c) => [c.providerId, { config: c }]));
|
|
270
304
|
const existingIds = new Set(ctx.socialProviders.map((p) => p.id));
|
|
271
305
|
for (const gp of genericProviders) if (existingIds.has(gp.id)) ctx.logger.warn(`Generic OAuth provider "${gp.id}" shadows a built-in social provider with the same ID`);
|
|
272
|
-
return { context: {
|
|
306
|
+
return { context: {
|
|
307
|
+
socialProviders: genericProviders.concat(ctx.socialProviders),
|
|
308
|
+
genericOAuthPendingDiscovery
|
|
309
|
+
} };
|
|
273
310
|
},
|
|
311
|
+
hooks: { before: [{
|
|
312
|
+
matcher: (context) => getPendingDiscovery(context.context)?.has(requestedProviderId(context) ?? "") === true,
|
|
313
|
+
handler: createAuthMiddleware(async (ctx) => {
|
|
314
|
+
await retryDiscovery(ctx.context, requestedProviderId(ctx) ?? "");
|
|
315
|
+
})
|
|
316
|
+
}] },
|
|
274
317
|
options,
|
|
275
318
|
$ERROR_CODES: GENERIC_OAUTH_ERROR_CODES
|
|
276
319
|
};
|
|
@@ -27,9 +27,9 @@ const lastLoginMethod = (userConfig) => {
|
|
|
27
27
|
return config.customResolveMethod?.(resolveContext) ?? defaultResolveMethod(resolveContext);
|
|
28
28
|
};
|
|
29
29
|
const config = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
...userConfig,
|
|
31
|
+
cookieName: userConfig?.cookieName ?? "better-auth.last_used_login_method",
|
|
32
|
+
maxAge: userConfig?.maxAge ?? 3600 * 24 * 30
|
|
33
33
|
};
|
|
34
34
|
return {
|
|
35
35
|
id: "last-login-method",
|
|
@@ -132,15 +132,9 @@ const magicLink = (options) => {
|
|
|
132
132
|
method: "GET",
|
|
133
133
|
query: magicLinkVerifyQuerySchema,
|
|
134
134
|
use: [
|
|
135
|
-
originCheck((ctx) =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
originCheck((ctx) => {
|
|
139
|
-
return ctx.query.newUserCallbackURL ? decodeURIComponent(ctx.query.newUserCallbackURL) : "/";
|
|
140
|
-
}),
|
|
141
|
-
originCheck((ctx) => {
|
|
142
|
-
return ctx.query.errorCallbackURL ? decodeURIComponent(ctx.query.errorCallbackURL) : "/";
|
|
143
|
-
})
|
|
135
|
+
originCheck((ctx) => ctx.query.callbackURL || "/"),
|
|
136
|
+
originCheck((ctx) => ctx.query.newUserCallbackURL || "/"),
|
|
137
|
+
originCheck((ctx) => ctx.query.errorCallbackURL || "/")
|
|
144
138
|
],
|
|
145
139
|
requireHeaders: true,
|
|
146
140
|
metadata: { openapi: {
|
|
@@ -159,14 +153,14 @@ const magicLink = (options) => {
|
|
|
159
153
|
} }
|
|
160
154
|
}, async (ctx) => {
|
|
161
155
|
const token = ctx.query.token;
|
|
162
|
-
const callbackURL = new URL(ctx.query.callbackURL
|
|
163
|
-
const errorCallbackURL = new URL(ctx.query.errorCallbackURL
|
|
156
|
+
const callbackURL = new URL(ctx.query.callbackURL || "/", ctx.context.baseURL).toString();
|
|
157
|
+
const errorCallbackURL = new URL(ctx.query.errorCallbackURL || callbackURL, ctx.context.baseURL);
|
|
164
158
|
function redirectWithError(error, description) {
|
|
165
159
|
errorCallbackURL.searchParams.set("error", error);
|
|
166
160
|
if (description) errorCallbackURL.searchParams.set("error_description", description);
|
|
167
161
|
throw ctx.redirect(errorCallbackURL.toString());
|
|
168
162
|
}
|
|
169
|
-
const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL
|
|
163
|
+
const newUserCallbackURL = new URL(ctx.query.newUserCallbackURL || callbackURL, ctx.context.baseURL).toString();
|
|
170
164
|
const identifier = magicLinkIdentifier(await storeToken(ctx, token));
|
|
171
165
|
const pendingValue = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
172
166
|
if (!pendingValue || !parseMagicLinkRecord(pendingValue.value)?.success) redirectWithError("INVALID_TOKEN");
|
|
@@ -1009,48 +1009,7 @@ declare const getActiveMemberRole: <O extends OrganizationOptions>(options: O) =
|
|
|
1009
1009
|
};
|
|
1010
1010
|
}>>)[];
|
|
1011
1011
|
}, {
|
|
1012
|
-
role:
|
|
1013
|
-
enabled: true;
|
|
1014
|
-
} ? {
|
|
1015
|
-
id: string;
|
|
1016
|
-
organizationId: string;
|
|
1017
|
-
role: InferOrganizationRolesFromOption<O>;
|
|
1018
|
-
createdAt: Date;
|
|
1019
|
-
userId: string;
|
|
1020
|
-
teamId?: string | undefined;
|
|
1021
|
-
user: {
|
|
1022
|
-
id: string;
|
|
1023
|
-
email: string;
|
|
1024
|
-
name: string;
|
|
1025
|
-
image?: string | undefined;
|
|
1026
|
-
};
|
|
1027
|
-
} : {
|
|
1028
|
-
id: string;
|
|
1029
|
-
organizationId: string;
|
|
1030
|
-
role: InferOrganizationRolesFromOption<O>;
|
|
1031
|
-
createdAt: Date;
|
|
1032
|
-
userId: string;
|
|
1033
|
-
user: {
|
|
1034
|
-
id: string;
|
|
1035
|
-
email: string;
|
|
1036
|
-
name: string;
|
|
1037
|
-
image?: string | undefined;
|
|
1038
|
-
};
|
|
1039
|
-
}) & (O["schema"] extends {
|
|
1040
|
-
member?: {
|
|
1041
|
-
additionalFields: infer Field extends Record<string, _$_better_auth_core_db0.DBFieldAttribute>;
|
|
1042
|
-
} | undefined;
|
|
1043
|
-
} ? FieldAttributeToObject<Field> : {}) extends infer T ? { [K in keyof T]: T[K] } : never) & {
|
|
1044
|
-
user: {
|
|
1045
|
-
id: string;
|
|
1046
|
-
createdAt: Date;
|
|
1047
|
-
updatedAt: Date;
|
|
1048
|
-
email: string;
|
|
1049
|
-
emailVerified: boolean;
|
|
1050
|
-
name: string;
|
|
1051
|
-
image?: string | null | undefined;
|
|
1052
|
-
};
|
|
1053
|
-
})["role"];
|
|
1012
|
+
role: InferOrganizationRolesFromOption<O>;
|
|
1054
1013
|
}>;
|
|
1055
1014
|
//#endregion
|
|
1056
1015
|
export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };
|
|
@@ -512,7 +512,7 @@ const getActiveMemberRole = (options) => createAuthEndpoint("/organization/get-a
|
|
|
512
512
|
organizationId
|
|
513
513
|
});
|
|
514
514
|
if (!member) throw APIError.from("FORBIDDEN", ORGANIZATION_ERROR_CODES.YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATION);
|
|
515
|
-
return ctx.json({ role: member
|
|
515
|
+
return ctx.json({ role: member.role });
|
|
516
516
|
});
|
|
517
517
|
//#endregion
|
|
518
518
|
export { addMember, getActiveMember, getActiveMemberRole, leaveOrganization, listMembers, removeMember, updateMemberRole };
|
|
@@ -4,8 +4,7 @@ import { generateRandomString } from "../../crypto/random.mjs";
|
|
|
4
4
|
import { setSessionCookie } from "../../cookies/index.mjs";
|
|
5
5
|
import { getSessionFromCtx } from "../../api/routes/session.mjs";
|
|
6
6
|
import { HIDE_METADATA } from "../../utils/hide-metadata.mjs";
|
|
7
|
-
import { assertPasswordNotTooLong, assertPasswordNotTooShort } from "../../utils/password.mjs";
|
|
8
|
-
import "../../utils/index.mjs";
|
|
7
|
+
import { assertPasswordNotTooLong, assertPasswordNotTooShort, resetCredentialPassword } from "../../utils/password.mjs";
|
|
9
8
|
import { PHONE_NUMBER_ERROR_CODES } from "./error-codes.mjs";
|
|
10
9
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
11
10
|
import { createAuthEndpoint } from "@better-auth/core/api";
|
|
@@ -176,15 +175,19 @@ const sendPhoneNumberOTP = (opts) => createAuthEndpoint("/phone-number/send-otp"
|
|
|
176
175
|
async function verifyAndConsumePhoneNumberOTP(ctx, opts, body) {
|
|
177
176
|
const { phoneNumber, code } = body;
|
|
178
177
|
if (opts.verifyOTP) {
|
|
179
|
-
|
|
180
|
-
phoneNumber,
|
|
181
|
-
code
|
|
182
|
-
}, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
183
|
-
await ctx.context.internalAdapter.deleteVerificationByIdentifier(phoneNumber);
|
|
178
|
+
await verifyWithCustomVerifier(ctx, opts.verifyOTP, phoneNumber, code, phoneNumber);
|
|
184
179
|
return;
|
|
185
180
|
}
|
|
186
181
|
await verifyPhoneNumberOTP(ctx, opts, phoneNumber, code);
|
|
187
182
|
}
|
|
183
|
+
/** Checks the code with `verifyOTP`, then drops the code stored under `identifier`. */
|
|
184
|
+
async function verifyWithCustomVerifier(ctx, verifyOTP, phoneNumber, code, identifier) {
|
|
185
|
+
if (!await verifyOTP({
|
|
186
|
+
phoneNumber,
|
|
187
|
+
code
|
|
188
|
+
}, ctx)) throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
189
|
+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
|
|
190
|
+
}
|
|
188
191
|
/**
|
|
189
192
|
* ### API Methods
|
|
190
193
|
*
|
|
@@ -460,7 +463,17 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
460
463
|
} }
|
|
461
464
|
} }
|
|
462
465
|
}, async (ctx) => {
|
|
463
|
-
|
|
466
|
+
const phoneResetIdentifier = `${ctx.body.phoneNumber}-request-password-reset`;
|
|
467
|
+
let otpConsumed = false;
|
|
468
|
+
if (opts.verifyOTP) {
|
|
469
|
+
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
470
|
+
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
471
|
+
await verifyWithCustomVerifier(ctx, opts.verifyOTP, ctx.body.phoneNumber, ctx.body.otp, phoneResetIdentifier);
|
|
472
|
+
otpConsumed = true;
|
|
473
|
+
} else if (!await matchesUsableStoredPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp)) {
|
|
474
|
+
await verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp);
|
|
475
|
+
otpConsumed = true;
|
|
476
|
+
}
|
|
464
477
|
const user = await ctx.context.adapter.findOne({
|
|
465
478
|
model: "user",
|
|
466
479
|
where: [{
|
|
@@ -472,15 +485,8 @@ const resetPasswordPhoneNumber = (opts) => createAuthEndpoint("/phone-number/res
|
|
|
472
485
|
assertPasswordNotTooShort(ctx, ctx.body.newPassword);
|
|
473
486
|
assertPasswordNotTooLong(ctx, ctx.body.newPassword);
|
|
474
487
|
const hashedPassword = await ctx.context.password.hash(ctx.body.newPassword);
|
|
475
|
-
|
|
476
|
-
userId: user.id,
|
|
477
|
-
providerId: "credential",
|
|
478
|
-
accountId: user.id,
|
|
479
|
-
password: hashedPassword
|
|
480
|
-
});
|
|
481
|
-
else await ctx.context.internalAdapter.updatePassword(user.id, hashedPassword);
|
|
488
|
+
await resetCredentialPassword(ctx, user.id, hashedPassword, otpConsumed ? void 0 : () => verifyPhoneNumberOTP(ctx, opts, phoneResetIdentifier, ctx.body.otp));
|
|
482
489
|
if (ctx.context.options.emailAndPassword?.onPasswordReset) await ctx.context.options.emailAndPassword.onPasswordReset({ user }, ctx.request);
|
|
483
|
-
if (ctx.context.options.emailAndPassword?.revokeSessionsOnPasswordReset) await ctx.context.internalAdapter.deleteUserSessions(user.id);
|
|
484
490
|
return ctx.json({ status: true });
|
|
485
491
|
});
|
|
486
492
|
/**
|
|
@@ -519,6 +525,17 @@ async function verifyPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
|
519
525
|
throw APIError.from("BAD_REQUEST", PHONE_NUMBER_ERROR_CODES.INVALID_OTP);
|
|
520
526
|
}
|
|
521
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Reports whether the provided code matches a stored, unexpired record with
|
|
530
|
+
* attempts left, without consuming it or counting an attempt.
|
|
531
|
+
*/
|
|
532
|
+
async function matchesUsableStoredPhoneNumberOTP(ctx, opts, identifier, providedCode) {
|
|
533
|
+
const existing = await ctx.context.internalAdapter.findVerificationValue(identifier);
|
|
534
|
+
if (!existing || existing.expiresAt < /* @__PURE__ */ new Date()) return false;
|
|
535
|
+
const [otpValue, rawAttempts] = existing.value.split(":");
|
|
536
|
+
if (parseVerificationAttempts(rawAttempts) >= (opts?.allowedAttempts ?? 3)) return false;
|
|
537
|
+
return otpValue === providedCode;
|
|
538
|
+
}
|
|
522
539
|
function parseVerificationAttempts(value) {
|
|
523
540
|
const attempts = Number(value ?? 0);
|
|
524
541
|
return Number.isSafeInteger(attempts) && attempts > 0 ? attempts : 0;
|