better-auth 1.8.0-beta.3 → 1.8.0-beta.5
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/dispatch.d.mts +1 -0
- package/dist/api/dispatch.mjs +31 -8
- package/dist/api/index.mjs +1 -1
- package/dist/api/routes/account.mjs +14 -6
- package/dist/api/routes/email-verification.mjs +29 -37
- package/dist/api/routes/sign-in.mjs +2 -2
- package/dist/api/routes/sign-up.mjs +1 -1
- package/dist/api/routes/update-user.mjs +10 -4
- package/dist/auth/trusted-origins.mjs +31 -8
- package/dist/client/index.d.mts +2 -1
- package/dist/client/parser.mjs +1 -11
- package/dist/client/vue/index.mjs +13 -7
- package/dist/context/secret-utils.mjs +3 -3
- package/dist/db/internal-adapter.mjs +24 -6
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/oauth2/link-account.d.mts +3 -5
- package/dist/oauth2/link-account.mjs +33 -3
- package/dist/oauth2/state.d.mts +3 -4
- package/dist/package.mjs +1 -1
- package/dist/plugins/admin/access/statement.d.mts +10 -10
- package/dist/plugins/admin/access/statement.mjs +2 -0
- package/dist/plugins/admin/admin.d.mts +6 -4
- package/dist/plugins/admin/admin.mjs +4 -4
- package/dist/plugins/admin/client.d.mts +6 -4
- package/dist/plugins/admin/error-codes.d.mts +2 -0
- package/dist/plugins/admin/error-codes.mjs +3 -1
- package/dist/plugins/admin/routes.mjs +9 -0
- package/dist/plugins/anonymous/index.mjs +1 -1
- package/dist/plugins/device-authorization/routes.mjs +3 -3
- package/dist/plugins/email-otp/routes.mjs +33 -50
- package/dist/plugins/generic-oauth/index.mjs +54 -30
- package/dist/plugins/generic-oauth/providers/hubspot.mjs +5 -2
- package/dist/plugins/index.d.mts +2 -1
- package/dist/plugins/index.mjs +2 -1
- package/dist/plugins/oauth-proxy/index.mjs +1 -1
- package/dist/plugins/one-tap/index.mjs +1 -1
- package/dist/plugins/organization/adapter.mjs +1 -0
- package/dist/plugins/organization/client.d.mts +1 -0
- package/dist/plugins/organization/error-codes.d.mts +1 -0
- package/dist/plugins/organization/error-codes.mjs +1 -0
- package/dist/plugins/organization/has-permission.d.mts +2 -3
- package/dist/plugins/organization/has-permission.mjs +62 -23
- package/dist/plugins/organization/index.d.mts +2 -1
- package/dist/plugins/organization/index.mjs +2 -1
- package/dist/plugins/organization/organization.d.mts +2 -2
- package/dist/plugins/organization/organization.mjs +1 -3
- package/dist/plugins/organization/permission.mjs +3 -3
- package/dist/plugins/organization/roles.d.mts +8 -0
- package/dist/plugins/organization/roles.mjs +26 -0
- package/dist/plugins/organization/routes/crud-access-control.mjs +2 -1
- package/dist/plugins/organization/routes/crud-invites.d.mts +22 -0
- package/dist/plugins/organization/routes/crud-invites.mjs +28 -13
- package/dist/plugins/organization/routes/crud-members.mjs +10 -14
- package/dist/plugins/organization/routes/crud-org.mjs +12 -3
- package/dist/plugins/phone-number/client.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.d.mts +2 -0
- package/dist/plugins/phone-number/error-codes.mjs +2 -0
- package/dist/plugins/phone-number/index.d.mts +25 -1
- package/dist/plugins/phone-number/index.mjs +22 -6
- package/dist/plugins/phone-number/routes.mjs +128 -62
- package/dist/plugins/phone-number/types.d.mts +11 -5
- package/dist/plugins/two-factor/index.mjs +7 -4
- package/dist/plugins/two-factor/verify-two-factor.mjs +2 -2
- package/dist/plugins/username/index.mjs +6 -19
- package/dist/state.d.mts +6 -0
- package/dist/state.mjs +8 -2
- package/dist/utils/index.d.mts +2 -2
- package/package.json +9 -9
package/dist/api/dispatch.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthContext } from "@better-auth/core";
|
|
2
2
|
import { Endpoint, EndpointContext, InputContext } from "better-call";
|
|
3
|
+
import "@better-auth/core/api";
|
|
3
4
|
//#region src/api/dispatch.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Input accepted by {@link dispatchAuthEndpoint}. `context` must already be a
|
package/dist/api/dispatch.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { APIError } from "@better-auth/core/error";
|
|
|
6
6
|
import { createDefu } from "defu";
|
|
7
7
|
import { ATTR_CONTEXT, ATTR_HOOK_TYPE, ATTR_HTTP_ROUTE, ATTR_OPERATION_ID, createWithSpan } from "@better-auth/core/instrumentation";
|
|
8
8
|
import { kAPIErrorHeaderSymbol, toResponse } from "better-call";
|
|
9
|
+
import { getInternalAuthCompletion } from "@better-auth/core/utils/auth-completion";
|
|
9
10
|
//#region src/api/dispatch.ts
|
|
10
11
|
const defuReplaceArrays = createDefu((obj, key, value) => {
|
|
11
12
|
if (Array.isArray(obj[key]) && Array.isArray(value)) {
|
|
@@ -14,6 +15,18 @@ const defuReplaceArrays = createDefu((obj, key, value) => {
|
|
|
14
15
|
}
|
|
15
16
|
});
|
|
16
17
|
const hooksSourceWeakMap = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
const signInGates = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
/**
|
|
20
|
+
* Runs a plugin after hook ahead of every other plugin after hook, so a sign-in
|
|
21
|
+
* it withholds (such as a pending two-factor challenge) is never observed as
|
|
22
|
+
* complete, regardless of plugin registration order.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
function asSignInGate(hook) {
|
|
27
|
+
signInGates.add(hook.handler);
|
|
28
|
+
return hook;
|
|
29
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* Resolves the operation id used for spans, preferring an explicit
|
|
19
32
|
* `operationId`, then the OpenAPI one, then the caller's `fallback` (the
|
|
@@ -163,12 +176,19 @@ function getHooks(authContext) {
|
|
|
163
176
|
const pluginAfterHooks = plugins.flatMap((plugin) => (plugin.hooks?.after ?? []).map((h) => {
|
|
164
177
|
hooksSourceWeakMap.set(h.handler, `plugin:${plugin.id}`);
|
|
165
178
|
return h;
|
|
166
|
-
}));
|
|
179
|
+
})).sort((a, b) => Number(signInGates.has(b.handler)) - Number(signInGates.has(a.handler)));
|
|
180
|
+
const completionHooks = plugins.flatMap((plugin) => {
|
|
181
|
+
const completion = getInternalAuthCompletion(plugin);
|
|
182
|
+
if (!completion) return [];
|
|
183
|
+
hooksSourceWeakMap.set(completion.handler, `plugin:${plugin.id}`);
|
|
184
|
+
return [completion];
|
|
185
|
+
});
|
|
167
186
|
if (pluginBeforeHooks.length) beforeHooks.push(...pluginBeforeHooks);
|
|
168
187
|
if (pluginAfterHooks.length) afterHooks.push(...pluginAfterHooks);
|
|
169
188
|
return {
|
|
170
189
|
beforeHooks,
|
|
171
|
-
afterHooks
|
|
190
|
+
afterHooks,
|
|
191
|
+
completionHooks
|
|
172
192
|
};
|
|
173
193
|
}
|
|
174
194
|
/**
|
|
@@ -208,7 +228,7 @@ async function dispatchAuthEndpoint(endpoint, input) {
|
|
|
208
228
|
[ATTR_HTTP_ROUTE]: route,
|
|
209
229
|
[ATTR_OPERATION_ID]: operationId
|
|
210
230
|
}, async () => runWithEndpointContext(internalContext, async () => {
|
|
211
|
-
const { beforeHooks, afterHooks } = getHooks(internalContext.context);
|
|
231
|
+
const { beforeHooks, afterHooks, completionHooks } = getHooks(internalContext.context);
|
|
212
232
|
const before = await runBeforeHooks(internalContext, beforeHooks, endpoint, operationId);
|
|
213
233
|
if ("context" in before && before.context && typeof before.context === "object") {
|
|
214
234
|
const { headers, ...rest } = before.context;
|
|
@@ -243,10 +263,13 @@ async function dispatchAuthEndpoint(endpoint, input) {
|
|
|
243
263
|
});
|
|
244
264
|
if (result instanceof Response) return result;
|
|
245
265
|
internalContext.context.returned = result.response;
|
|
246
|
-
internalContext.context
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
mergeResponseHeaders(internalContext.context, result.headers);
|
|
267
|
+
await runAfterHooks(internalContext, afterHooks, endpoint, operationId);
|
|
268
|
+
const matchingCompletions = completionHooks.filter((hook) => hook.matcher(internalContext));
|
|
269
|
+
if (matchingCompletions.length > 1) throw new APIError("INTERNAL_SERVER_ERROR", { message: "Multiple authentication completion handlers matched." });
|
|
270
|
+
const completed = await runAfterHooks(internalContext, matchingCompletions, endpoint, operationId);
|
|
271
|
+
if (completed.response !== void 0) result.response = completed.response;
|
|
272
|
+
result.headers = completed.headers ?? result.headers;
|
|
250
273
|
if (isAPIError(result.response) && shouldPublishLog(internalContext.context.logger.level, "debug")) result.response.stack = result.response.errorStack;
|
|
251
274
|
if (isAPIError(result.response) && !shouldReturnResponse) {
|
|
252
275
|
if (result.headers) Object.defineProperty(result.response, kAPIErrorHeaderSymbol, {
|
|
@@ -274,4 +297,4 @@ async function dispatchAuthEndpoint(endpoint, input) {
|
|
|
274
297
|
}));
|
|
275
298
|
}
|
|
276
299
|
//#endregion
|
|
277
|
-
export { dispatchAuthEndpoint, getOperationId };
|
|
300
|
+
export { asSignInGate, dispatchAuthEndpoint, getOperationId };
|
package/dist/api/index.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import { isAPIError } from "../utils/is-api-error.mjs";
|
|
|
2
2
|
import { requireOrgRole, requireResourceOwnership } from "./middlewares/authorization.mjs";
|
|
3
3
|
import { formCsrfMiddleware, originCheck, originCheckMiddleware } from "./middlewares/origin-check.mjs";
|
|
4
4
|
import { onRequestRateLimit } from "./rate-limiter/index.mjs";
|
|
5
|
-
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
6
5
|
import { getShouldSkipSessionRefresh, setShouldSkipSessionRefresh } from "./state/should-session-refresh.mjs";
|
|
7
6
|
import { freshSessionMiddleware, getAuthoritativeSessionFromCtx, getSession, getSessionFromCtx, isStateful, listSessions, requestOnlySessionMiddleware, revokeOtherSessions, revokeSession, revokeSessions, sensitiveSessionMiddleware, sessionMiddleware } from "./routes/session.mjs";
|
|
7
|
+
import { addOAuthServerContext, getOAuthState } from "./state/oauth.mjs";
|
|
8
8
|
import { accountInfo, getAccessToken, linkSocialAccount, listUserAccounts, refreshToken, unlinkAccount } from "./routes/account.mjs";
|
|
9
9
|
import { callbackOAuth } from "./routes/callback.mjs";
|
|
10
10
|
import { createEmailVerificationToken, sendVerificationEmail, sendVerificationEmailFn, verifyEmail } from "./routes/email-verification.mjs";
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { shouldBindAccountCookieToSessionUser } from "../../context/store-capabilities.mjs";
|
|
2
2
|
import { parseAccountOutput } from "../../db/schema.mjs";
|
|
3
|
+
import { symmetricEncrypt } from "../../crypto/index.mjs";
|
|
3
4
|
import { getAccountCookie, setAccountCookie } from "../../cookies/session-store.mjs";
|
|
4
5
|
import { getAwaitableValue } from "../../context/helpers.mjs";
|
|
6
|
+
import { derivePurposeKey } from "../../crypto/purpose.mjs";
|
|
5
7
|
import { resolveOAuthAccountKeyForAPI } from "../../oauth2/account-key.mjs";
|
|
6
8
|
import { missingEmailLogMessage } from "../../oauth2/errors.mjs";
|
|
9
|
+
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sensitiveSessionMiddleware, sessionMiddleware } from "./session.mjs";
|
|
10
|
+
import { toBoolean } from "../../utils/boolean.mjs";
|
|
7
11
|
import { decryptOAuthToken, getOAuthCallbackPath, setTokenUtil } from "../../oauth2/utils.mjs";
|
|
8
12
|
import { applyUpdateUserInfoOnLink } from "../../oauth2/link-account.mjs";
|
|
9
13
|
import { generateIdTokenNonce, generateState } from "../../oauth2/state.mjs";
|
|
10
|
-
import { freshSessionMiddleware, getSessionFromCtx, isStateful, sessionMiddleware } from "./session.mjs";
|
|
11
14
|
import { APIError, BASE_ERROR_CODES } from "@better-auth/core/error";
|
|
12
15
|
import { additionalAuthorizationParamsSchema, supportsIdTokenSignIn, verifyProviderIdToken } from "@better-auth/core/oauth2";
|
|
13
16
|
import { SocialProviderListEnum } from "@better-auth/core/social-providers";
|
|
@@ -129,7 +132,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
129
132
|
*/
|
|
130
133
|
additionalData: z.record(z.string(), z.any()).optional()
|
|
131
134
|
}),
|
|
132
|
-
use: [
|
|
135
|
+
use: [sensitiveSessionMiddleware],
|
|
133
136
|
metadata: { openapi: {
|
|
134
137
|
description: "Link a social account to the user",
|
|
135
138
|
operationId: "linkSocialAccount",
|
|
@@ -169,11 +172,12 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
169
172
|
c.context.logger.warn("Invalid id token", { provider: c.body.provider });
|
|
170
173
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.INVALID_TOKEN);
|
|
171
174
|
}
|
|
172
|
-
const
|
|
175
|
+
const oauthTokens = {
|
|
173
176
|
idToken: token,
|
|
174
177
|
accessToken: c.body.idToken.accessToken,
|
|
175
178
|
refreshToken: c.body.idToken.refreshToken
|
|
176
|
-
}
|
|
179
|
+
};
|
|
180
|
+
const linkingUserInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
|
|
177
181
|
if (!linkingUserInfo || !linkingUserInfo?.user) {
|
|
178
182
|
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
179
183
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
|
|
@@ -204,7 +208,7 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
204
208
|
});
|
|
205
209
|
}
|
|
206
210
|
if (linkedAccount) throw APIError.from("CONFLICT", BASE_ERROR_CODES.SOCIAL_ACCOUNT_ALREADY_LINKED);
|
|
207
|
-
if (!c.context.trustedProviders.includes(provider.id) && !linkingUserInfo.user.emailVerified || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
|
|
211
|
+
if (!c.context.trustedProviders.includes(provider.id) && !toBoolean(linkingUserInfo.user.emailVerified) || c.context.options.account?.accountLinking?.enabled === false) throw APIError.from("UNAUTHORIZED", {
|
|
208
212
|
message: "Account not linked - linking not allowed",
|
|
209
213
|
code: "LINKING_NOT_ALLOWED"
|
|
210
214
|
});
|
|
@@ -237,7 +241,11 @@ const linkSocialAccount = createAuthEndpoint("/link-social", {
|
|
|
237
241
|
const state = await generateState(c, {
|
|
238
242
|
link: {
|
|
239
243
|
userId: session.user.id,
|
|
240
|
-
email: session.user.email
|
|
244
|
+
email: session.user.email,
|
|
245
|
+
encryptedSessionToken: isStateful(c) ? await symmetricEncrypt({
|
|
246
|
+
key: derivePurposeKey(c.context.secretConfig, "oauth-link-session-token"),
|
|
247
|
+
data: session.session.token
|
|
248
|
+
}) : void 0
|
|
241
249
|
},
|
|
242
250
|
additionalData: c.body.additionalData,
|
|
243
251
|
idTokenNonce
|
|
@@ -26,7 +26,7 @@ async function sendVerificationEmailFn(ctx, user) {
|
|
|
26
26
|
ctx.context.logger.error("Verification email isn't enabled.");
|
|
27
27
|
throw APIError.from("BAD_REQUEST", BASE_ERROR_CODES.VERIFICATION_EMAIL_NOT_ENABLED);
|
|
28
28
|
}
|
|
29
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
29
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
30
30
|
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
31
31
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
32
32
|
await ctx.context.options.emailVerification.sendVerificationEmail({
|
|
@@ -183,24 +183,30 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
183
183
|
const parsed = z.object({
|
|
184
184
|
email: z.email(),
|
|
185
185
|
updateTo: z.string().optional(),
|
|
186
|
-
requestType: z.string().optional()
|
|
186
|
+
requestType: z.string().optional(),
|
|
187
|
+
userId: z.string().optional()
|
|
187
188
|
}).parse(jwt.payload);
|
|
188
|
-
const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
|
|
189
|
-
if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
190
189
|
if (parsed.updateTo) {
|
|
190
|
+
if (!parsed.userId) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
191
|
+
const boundUser = await ctx.context.internalAdapter.findUserById(parsed.userId);
|
|
192
|
+
if (!boundUser) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
193
|
+
if (boundUser.email.toLowerCase() !== parsed.email) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
191
194
|
const session = await getSessionFromCtx(ctx);
|
|
192
|
-
if (session && session.user.
|
|
195
|
+
if (session && session.user.id !== boundUser.id) return redirectOnError(BASE_ERROR_CODES.INVALID_USER);
|
|
193
196
|
switch (parsed.requestType) {
|
|
194
197
|
/**
|
|
195
198
|
* User clicks confirmation -> sends verification to new email
|
|
196
199
|
*/
|
|
197
200
|
case "change-email-confirmation": {
|
|
198
|
-
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, {
|
|
201
|
+
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.email, parsed.updateTo, ctx.context.options.emailVerification?.expiresIn, {
|
|
202
|
+
requestType: "change-email-verification",
|
|
203
|
+
userId: boundUser.id
|
|
204
|
+
});
|
|
199
205
|
const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
|
|
200
206
|
const url = `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`;
|
|
201
207
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
202
208
|
user: {
|
|
203
|
-
...
|
|
209
|
+
...boundUser,
|
|
204
210
|
email: parsed.updateTo
|
|
205
211
|
},
|
|
206
212
|
url,
|
|
@@ -213,26 +219,18 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
213
219
|
* User clicks verification -> updates email
|
|
214
220
|
*/
|
|
215
221
|
case "change-email-verification": {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
219
|
-
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
220
|
-
activeSession = {
|
|
221
|
-
session: newSession,
|
|
222
|
-
user: user.user
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
222
|
+
const emailVerified = boundUser.emailVerified;
|
|
223
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
|
|
226
224
|
email: parsed.updateTo,
|
|
227
|
-
emailVerified
|
|
225
|
+
emailVerified
|
|
228
226
|
});
|
|
229
|
-
if (ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
|
|
230
|
-
await setSessionCookie(ctx, {
|
|
231
|
-
session:
|
|
227
|
+
if (emailVerified && ctx.context.options.emailVerification?.afterEmailVerification) await ctx.context.options.emailVerification.afterEmailVerification(updatedUser, ctx.request);
|
|
228
|
+
if (session) await setSessionCookie(ctx, {
|
|
229
|
+
session: session.session,
|
|
232
230
|
user: {
|
|
233
|
-
...
|
|
231
|
+
...session.user,
|
|
234
232
|
email: parsed.updateTo,
|
|
235
|
-
emailVerified
|
|
233
|
+
emailVerified
|
|
236
234
|
}
|
|
237
235
|
});
|
|
238
236
|
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
@@ -248,30 +246,21 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
248
246
|
* - updates email immediately
|
|
249
247
|
*/
|
|
250
248
|
default: {
|
|
251
|
-
|
|
252
|
-
if (!activeSession) {
|
|
253
|
-
const newSession = await ctx.context.internalAdapter.createSession(user.user.id);
|
|
254
|
-
if (!newSession) throw APIError.from("INTERNAL_SERVER_ERROR", BASE_ERROR_CODES.FAILED_TO_CREATE_SESSION);
|
|
255
|
-
activeSession = {
|
|
256
|
-
session: newSession,
|
|
257
|
-
user: user.user
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
const updatedUser = await ctx.context.internalAdapter.updateUserByEmail(parsed.email, {
|
|
249
|
+
const updatedUser = await ctx.context.internalAdapter.updateUser(boundUser.id, {
|
|
261
250
|
email: parsed.updateTo,
|
|
262
251
|
emailVerified: false
|
|
263
252
|
});
|
|
264
|
-
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo);
|
|
253
|
+
const newToken = await createEmailVerificationToken(ctx.context.secret, parsed.updateTo, void 0, void 0, { userId: boundUser.id });
|
|
265
254
|
const updateCallbackURL = ctx.query.callbackURL ? encodeURIComponent(ctx.query.callbackURL) : encodeURIComponent("/");
|
|
266
255
|
if (ctx.context.options.emailVerification?.sendVerificationEmail) await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
267
256
|
user: updatedUser,
|
|
268
257
|
url: `${ctx.context.baseURL}/verify-email?token=${newToken}&callbackURL=${updateCallbackURL}`,
|
|
269
258
|
token: newToken
|
|
270
259
|
}, safeCloneRequest(ctx.request)));
|
|
271
|
-
await setSessionCookie(ctx, {
|
|
272
|
-
session:
|
|
260
|
+
if (session) await setSessionCookie(ctx, {
|
|
261
|
+
session: session.session,
|
|
273
262
|
user: {
|
|
274
|
-
...
|
|
263
|
+
...session.user,
|
|
275
264
|
email: parsed.updateTo,
|
|
276
265
|
emailVerified: false
|
|
277
266
|
}
|
|
@@ -284,6 +273,9 @@ const verifyEmail = createAuthEndpoint("/verify-email", {
|
|
|
284
273
|
}
|
|
285
274
|
}
|
|
286
275
|
}
|
|
276
|
+
const user = await ctx.context.internalAdapter.findUserByEmail(parsed.email);
|
|
277
|
+
if (!user) return redirectOnError(BASE_ERROR_CODES.USER_NOT_FOUND);
|
|
278
|
+
if (parsed.userId && parsed.userId !== user.user.id) return redirectOnError(BASE_ERROR_CODES.INVALID_TOKEN);
|
|
287
279
|
if (user.user.emailVerified) {
|
|
288
280
|
if (ctx.query.callbackURL) throw ctx.redirect(ctx.query.callbackURL);
|
|
289
281
|
return ctx.json({
|
|
@@ -167,7 +167,7 @@ const signInSocial = () => createAuthEndpoint("/sign-in/social", {
|
|
|
167
167
|
refreshToken: c.body.idToken.refreshToken,
|
|
168
168
|
user: c.body.idToken.user
|
|
169
169
|
};
|
|
170
|
-
const userInfo = await provider.getUserInfo(oauthTokens);
|
|
170
|
+
const userInfo = provider.getUserInfoFromIdToken ? await provider.getUserInfoFromIdToken(token) : await provider.getUserInfo(oauthTokens);
|
|
171
171
|
if (!userInfo || !userInfo?.user) {
|
|
172
172
|
c.context.logger.error("Failed to get user info", { provider: c.body.provider });
|
|
173
173
|
throw APIError.from("UNAUTHORIZED", BASE_ERROR_CODES.FAILED_TO_GET_USER_INFO);
|
|
@@ -338,7 +338,7 @@ const signInEmail = () => createAuthEndpoint("/sign-in/email", {
|
|
|
338
338
|
if (ctx.context.options?.emailAndPassword?.requireEmailVerification && !user.emailVerified) {
|
|
339
339
|
if (!ctx.context.options?.emailVerification?.sendVerificationEmail) throw APIError.from("FORBIDDEN", BASE_ERROR_CODES.EMAIL_NOT_VERIFIED);
|
|
340
340
|
if (ctx.context.options?.emailVerification?.sendOnSignIn) {
|
|
341
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
341
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
342
342
|
const callbackURL = ctx.body.callbackURL ? encodeURIComponent(ctx.body.callbackURL) : encodeURIComponent("/");
|
|
343
343
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
344
344
|
await ctx.context.runInBackgroundOrAwait(ctx.context.options.emailVerification.sendVerificationEmail({
|
|
@@ -251,7 +251,7 @@ const signUpEmail = () => createAuthEndpoint("/sign-up/email", {
|
|
|
251
251
|
const callbackURL = body.callbackURL ? encodeURIComponent(body.callbackURL) : encodeURIComponent("/");
|
|
252
252
|
const request = safeCloneRequest(ctx.request);
|
|
253
253
|
await queueAfterTransactionHook(async () => {
|
|
254
|
-
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
254
|
+
const token = await createEmailVerificationToken(ctx.context.secret, user.email, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: user.id });
|
|
255
255
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${callbackURL}`;
|
|
256
256
|
await ctx.context.runInBackgroundOrAwait(sendVerificationEmail({
|
|
257
257
|
user,
|
|
@@ -450,7 +450,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
450
450
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
451
451
|
}
|
|
452
452
|
if (await ctx.context.internalAdapter.findUserByEmail(newEmail)) {
|
|
453
|
-
await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn);
|
|
453
|
+
await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
|
|
454
454
|
ctx.context.logger.info("Change email attempt for existing email");
|
|
455
455
|
return ctx.json({ status: true });
|
|
456
456
|
}
|
|
@@ -467,7 +467,7 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
467
467
|
}
|
|
468
468
|
});
|
|
469
469
|
if (canSendVerification) {
|
|
470
|
-
const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn);
|
|
470
|
+
const token = await createEmailVerificationToken(ctx.context.secret, newEmail, void 0, ctx.context.options.emailVerification?.expiresIn, { userId: ctx.context.session.user.id });
|
|
471
471
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
472
472
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
473
473
|
user: {
|
|
@@ -484,7 +484,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
484
484
|
* If the email is verified, we need to send a verification email
|
|
485
485
|
*/
|
|
486
486
|
if (canSendConfirmation) {
|
|
487
|
-
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
487
|
+
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
488
|
+
requestType: "change-email-confirmation",
|
|
489
|
+
userId: ctx.context.session.user.id
|
|
490
|
+
});
|
|
488
491
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
489
492
|
await ctx.context.runInBackgroundOrAwait(canSendConfirmation({
|
|
490
493
|
user: ctx.context.session.user,
|
|
@@ -498,7 +501,10 @@ const changeEmail = createAuthEndpoint("/change-email", {
|
|
|
498
501
|
ctx.context.logger.error("Verification email isn't enabled.");
|
|
499
502
|
throw APIError.fromStatus("BAD_REQUEST", { message: "Verification email isn't enabled" });
|
|
500
503
|
}
|
|
501
|
-
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
504
|
+
const token = await createEmailVerificationToken(ctx.context.secret, ctx.context.session.user.email, newEmail, ctx.context.options.emailVerification?.expiresIn, {
|
|
505
|
+
requestType: "change-email-verification",
|
|
506
|
+
userId: ctx.context.session.user.id
|
|
507
|
+
});
|
|
502
508
|
const url = `${ctx.context.baseURL}/verify-email?token=${token}&callbackURL=${encodeURIComponent(ctx.body.callbackURL || "/")}`;
|
|
503
509
|
await ctx.context.runInBackgroundOrAwait(canSendVerification({
|
|
504
510
|
user: {
|
|
@@ -4,6 +4,19 @@ import { getHost, getOrigin, getProtocol } from "../utils/url.mjs";
|
|
|
4
4
|
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f-\u009f]/;
|
|
5
5
|
const RELATIVE_URL_PARSER_ORIGIN = "https://better-auth.invalid";
|
|
6
6
|
const ENCODED_PATH_SEPARATOR_PATTERN = /%2[fF]|%5[cC]/;
|
|
7
|
+
const IP_WILDCARD_HOST_PATTERN = /^(?=.*\d)[\d*?.]+$/;
|
|
8
|
+
const IPV4_HOST_PATTERN = /^\d{1,3}(?:\.\d{1,3}){3}$/;
|
|
9
|
+
/**
|
|
10
|
+
* A wildcard host made only of digits, dots and wildcards (`192.168.*.*`,
|
|
11
|
+
* `10.*`) names an address range, so it admits only an IPv4 address. A plain
|
|
12
|
+
* `*` would otherwise cross `.` and let `192.168.attacker.com` match. Other
|
|
13
|
+
* hosts keep the documented subdomain matching.
|
|
14
|
+
*/
|
|
15
|
+
const isAllowedIPv4Host = (patternAuthority, authority) => {
|
|
16
|
+
const patternHost = patternAuthority.split(":")[0];
|
|
17
|
+
if (!IP_WILDCARD_HOST_PATTERN.test(patternHost)) return true;
|
|
18
|
+
return IPV4_HOST_PATTERN.test(authority.split(":")[0]);
|
|
19
|
+
};
|
|
7
20
|
/**
|
|
8
21
|
* Resolves `.` and `..` segments in a path after percent-decoding so a
|
|
9
22
|
* path-pinned pattern cannot be bypassed with traversal: e.g.
|
|
@@ -33,7 +46,7 @@ const normalizePath = (path) => {
|
|
|
33
46
|
* host); the path is percent-decoded and resolved so traversal cannot bypass
|
|
34
47
|
* a path-pinned pattern.
|
|
35
48
|
*/
|
|
36
|
-
const parseCustomSchemeOrigin = (value) => {
|
|
49
|
+
const parseCustomSchemeOrigin = (value, wildcardPattern = false) => {
|
|
37
50
|
if (CONTROL_CHARACTER_PATTERN.test(value)) return null;
|
|
38
51
|
const schemeEnd = value.indexOf(":");
|
|
39
52
|
if (schemeEnd <= 0) return null;
|
|
@@ -42,7 +55,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
42
55
|
let authority = "";
|
|
43
56
|
if (rest.startsWith("//")) {
|
|
44
57
|
rest = rest.slice(2);
|
|
45
|
-
const authorityEnd = rest.search(/[/?#]/);
|
|
58
|
+
const authorityEnd = rest.search(wildcardPattern ? /[/#]/ : /[/?#]/);
|
|
46
59
|
if (authorityEnd === -1) {
|
|
47
60
|
authority = rest;
|
|
48
61
|
rest = "";
|
|
@@ -51,7 +64,7 @@ const parseCustomSchemeOrigin = (value) => {
|
|
|
51
64
|
rest = rest.slice(authorityEnd);
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
|
-
const pathEnd = rest.search(/[?#]/);
|
|
67
|
+
const pathEnd = rest.search(wildcardPattern ? /#/ : /[?#]/);
|
|
55
68
|
const path = normalizePath(pathEnd === -1 ? rest : rest.slice(0, pathEnd));
|
|
56
69
|
return {
|
|
57
70
|
scheme,
|
|
@@ -87,17 +100,27 @@ const isSafeRelativeURL = (value) => {
|
|
|
87
100
|
*/
|
|
88
101
|
const matchesOriginPattern = (url, pattern, settings) => {
|
|
89
102
|
if (url.startsWith("/")) return settings?.allowRelativePaths === true && isSafeRelativeURL(url);
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
const hasWildcard = pattern.includes("*") || pattern.includes("?");
|
|
104
|
+
if (hasWildcard && !pattern.includes("://")) {
|
|
92
105
|
const host = getHost(url);
|
|
93
106
|
if (!host) return false;
|
|
94
|
-
return wildcardMatch(pattern)(host);
|
|
107
|
+
return wildcardMatch(pattern)(host) && isAllowedIPv4Host(pattern, host);
|
|
95
108
|
}
|
|
96
109
|
const protocol = getProtocol(url);
|
|
97
|
-
if (protocol === "http:" || protocol === "https:" || !protocol)
|
|
110
|
+
if (protocol === "http:" || protocol === "https:" || !protocol) {
|
|
111
|
+
if (!hasWildcard) return pattern === getOrigin(url);
|
|
112
|
+
const origin = getOrigin(url);
|
|
113
|
+
if (origin === null || !wildcardMatch(pattern)(origin)) return false;
|
|
114
|
+
return isAllowedIPv4Host(pattern.slice(pattern.indexOf("://") + 3), origin.slice(origin.indexOf("://") + 3));
|
|
115
|
+
}
|
|
98
116
|
const parsed = parseCustomSchemeOrigin(url);
|
|
99
|
-
const parsedPattern = parseCustomSchemeOrigin(pattern);
|
|
117
|
+
const parsedPattern = parseCustomSchemeOrigin(pattern, hasWildcard);
|
|
100
118
|
if (!parsed || !parsedPattern || parsed.scheme !== parsedPattern.scheme) return false;
|
|
119
|
+
if (hasWildcard) {
|
|
120
|
+
if (parsed.authority.includes("@")) return false;
|
|
121
|
+
if (parsedPattern.authority && !(wildcardMatch(parsedPattern.authority)(parsed.authority) && isAllowedIPv4Host(parsedPattern.authority, parsed.authority))) return false;
|
|
122
|
+
return !parsedPattern.path || wildcardMatch(parsedPattern.path)(parsed.path || "/");
|
|
123
|
+
}
|
|
101
124
|
if (parsedPattern.authority && parsed.authority !== parsedPattern.authority) return false;
|
|
102
125
|
if (!parsedPattern.path) return true;
|
|
103
126
|
return parsed.path === parsedPattern.path || parsed.path.startsWith(`${parsedPattern.path}/`);
|
package/dist/client/index.d.mts
CHANGED
|
@@ -16,7 +16,8 @@ import { OrganizationOptions } from "../plugins/organization/types.mjs";
|
|
|
16
16
|
import { InferInvitation, InferMember, InferOrganization, InferOrganizationRolesFromOption, InferOrganizationZodRolesFromOption, InferTeam, Invitation, InvitationInput, InvitationStatus, Member, MemberInput, Organization, OrganizationInput, OrganizationRole, OrganizationSchema, Team, TeamInput, TeamMember, TeamMemberInput, defaultRolesSchema, invitationSchema, invitationStatus, memberSchema, organizationRoleSchema, organizationSchema, roleSchema, teamMemberSchema, teamSchema } from "../plugins/organization/schema.mjs";
|
|
17
17
|
import { getOrgAdapter } from "../plugins/organization/adapter.mjs";
|
|
18
18
|
import { hasPermission } from "../plugins/organization/has-permission.mjs";
|
|
19
|
-
import {
|
|
19
|
+
import { parseRoles } from "../plugins/organization/roles.mjs";
|
|
20
|
+
import { DefaultOrganizationPlugin, DynamicAccessControlEndpoints, OrganizationCreator, OrganizationEndpoints, OrganizationPlugin, TeamEndpoints, organization } from "../plugins/organization/organization.mjs";
|
|
20
21
|
import "../plugins/organization/index.mjs";
|
|
21
22
|
import { BetterAuthOptions, BetterAuthPlugin } from "@better-auth/core";
|
|
22
23
|
import { DBPrimitive } from "@better-auth/core/db";
|
package/dist/client/parser.mjs
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/client/parser.ts
|
|
2
|
-
const PROTO_POLLUTION_PATTERNS = {
|
|
3
|
-
proto: /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/,
|
|
4
|
-
constructor: /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/,
|
|
5
|
-
protoShort: /"__proto__"\s*:/,
|
|
6
|
-
constructorShort: /"constructor"\s*:/
|
|
7
|
-
};
|
|
8
2
|
const JSON_SIGNATURE = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
|
|
9
3
|
const SPECIAL_VALUES = {
|
|
10
4
|
true: true,
|
|
@@ -40,14 +34,10 @@ function betterJSONParse(value, options = {}) {
|
|
|
40
34
|
if (strict) throw new SyntaxError("[better-json] Invalid JSON");
|
|
41
35
|
return value;
|
|
42
36
|
}
|
|
43
|
-
if (Object.entries(PROTO_POLLUTION_PATTERNS).some(([key, pattern]) => {
|
|
44
|
-
const matches = pattern.test(trimmed);
|
|
45
|
-
if (matches && warnings) console.warn(`[better-json] Detected potential prototype pollution attempt using ${key} pattern`);
|
|
46
|
-
return matches;
|
|
47
|
-
}) && strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
|
|
48
37
|
try {
|
|
49
38
|
const secureReviver = (key, value) => {
|
|
50
39
|
if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
|
|
40
|
+
if (strict) throw new Error("[better-json] Potential prototype pollution attempt detected");
|
|
51
41
|
if (warnings) console.warn(`[better-json] Dropping "${key}" key to prevent prototype pollution`);
|
|
52
42
|
return;
|
|
53
43
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimTrailingSlashes } from "../../utils/url.mjs";
|
|
1
2
|
import { getClientConfig } from "../config.mjs";
|
|
2
3
|
import { createDynamicPathProxy } from "../proxy.mjs";
|
|
3
4
|
import { useStore } from "./vue-store.mjs";
|
|
@@ -16,18 +17,23 @@ function toHeadersInit(headers) {
|
|
|
16
17
|
}
|
|
17
18
|
function createAuthClient(options) {
|
|
18
19
|
const { baseURL, pluginPathMethods, pluginsActions, pluginsAtoms, hydrateSession, $sessionSignal, $fetch, $store, atomListeners } = getClientConfig(options, false);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const baseURLKey = options?.baseURL || "inferred";
|
|
21
|
+
const basePath = options?.basePath ?? "/api/auth";
|
|
22
|
+
const sessionCacheKey = `better-auth:session:${baseURLKey}:${basePath}`;
|
|
23
|
+
const sessionPath = trimTrailingSlashes(basePath.startsWith("/") ? basePath : `/${basePath}`);
|
|
24
|
+
/**
|
|
25
|
+
* Stays relative without an explicit `baseURL` so the SSR session fetch
|
|
26
|
+
* forwards the incoming cookies.
|
|
27
|
+
*
|
|
28
|
+
* @see https://nuxt.com/docs/4.x/getting-started/data-fetching
|
|
29
|
+
*/
|
|
30
|
+
const sessionURL = `${options?.baseURL ? baseURL : sessionPath}/get-session`;
|
|
25
31
|
const resolvedHooks = {};
|
|
26
32
|
for (const [key, value] of Object.entries(pluginsAtoms)) resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
|
27
33
|
function useSession(useFetch) {
|
|
28
34
|
if (useFetch) {
|
|
29
35
|
const sessionSignal = useStore($sessionSignal);
|
|
30
|
-
return useFetch(
|
|
36
|
+
return useFetch(sessionURL, {
|
|
31
37
|
headers: toHeadersInit(options?.fetchOptions?.headers),
|
|
32
38
|
key: sessionCacheKey,
|
|
33
39
|
watch: [sessionSignal]
|
|
@@ -12,12 +12,12 @@ function estimateEntropy(str) {
|
|
|
12
12
|
}
|
|
13
13
|
function parseSecretsEnv(envValue) {
|
|
14
14
|
if (!envValue) return null;
|
|
15
|
-
return envValue.split(",").map((entry) => {
|
|
15
|
+
return envValue.split(",").map((entry, index) => {
|
|
16
16
|
entry = entry.trim();
|
|
17
17
|
const colonIdx = entry.indexOf(":");
|
|
18
|
-
if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry
|
|
18
|
+
if (colonIdx === -1) throw new BetterAuthError(`Invalid BETTER_AUTH_SECRETS entry at position ${index + 1}. Expected format: "<version>:<secret>"`);
|
|
19
19
|
const version = parseInt(entry.slice(0, colonIdx), 10);
|
|
20
|
-
if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS
|
|
20
|
+
if (!Number.isInteger(version) || version < 0) throw new BetterAuthError(`Invalid version in BETTER_AUTH_SECRETS entry at position ${index + 1}. Version must be a non-negative integer.`);
|
|
21
21
|
const value = entry.slice(colonIdx + 1).trim();
|
|
22
22
|
if (!value) throw new BetterAuthError(`Empty secret value for version ${version} in BETTER_AUTH_SECRETS.`);
|
|
23
23
|
return {
|
|
@@ -78,8 +78,18 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
78
78
|
const activeSessions = await secondaryStorage.get(`active-sessions-${userId}`);
|
|
79
79
|
return activeSessions ? safeJSONParse(activeSessions) || [] : [];
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
const revokedBeforeKey = (userId) => `revoked-sessions-before-${userId}`;
|
|
82
|
+
const revokedBeforeTTL = Math.max(sessionExpiration, 86400, Number(options.plugins?.find((plugin) => plugin.id === "admin")?.options?.impersonationSessionDuration) || 0);
|
|
83
|
+
async function isRevokedSession(session, revokedBeforeByUser) {
|
|
84
|
+
if (!secondaryStorage) return false;
|
|
85
|
+
const revokedBefore = revokedBeforeByUser?.get(session.userId) ?? Number(await secondaryStorage.get(revokedBeforeKey(session.userId)));
|
|
86
|
+
revokedBeforeByUser?.set(session.userId, revokedBefore);
|
|
87
|
+
return new Date(session.createdAt).getTime() < revokedBefore;
|
|
88
|
+
}
|
|
89
|
+
async function deleteCachedUserSessions(userId, sessionReferences, revokedAt) {
|
|
82
90
|
if (!secondaryStorage) return;
|
|
91
|
+
const previousRevokedBefore = Number(await secondaryStorage.get(revokedBeforeKey(userId)));
|
|
92
|
+
await secondaryStorage.set(revokedBeforeKey(userId), String(Math.max(revokedAt, previousRevokedBefore || 0)), revokedBeforeTTL);
|
|
83
93
|
const deletedTokens = new Set(sessionReferences.map((session) => session.token));
|
|
84
94
|
for (const { token } of sessionReferences) await secondaryStorage.delete(token);
|
|
85
95
|
const activeSessionsKey = `active-sessions-${userId}`;
|
|
@@ -97,7 +107,8 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
97
107
|
async function queueCachedUserSessionDeletion(userId, sessionReferences) {
|
|
98
108
|
if (!secondaryStorage) return;
|
|
99
109
|
const references = sessionReferences ?? await getActiveSessionReferences(userId);
|
|
100
|
-
|
|
110
|
+
const revokedAt = Date.now();
|
|
111
|
+
await queueAfterTransactionHook(() => deleteCachedUserSessions(userId, references, revokedAt), { onError(error) {
|
|
101
112
|
logger.error("Failed to delete committed user sessions from secondary storage", error);
|
|
102
113
|
} });
|
|
103
114
|
}
|
|
@@ -194,6 +205,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
194
205
|
const now = Date.now();
|
|
195
206
|
const seenTokens = /* @__PURE__ */ new Set();
|
|
196
207
|
const sessions = [];
|
|
208
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
197
209
|
for (const { token, expiresAt } of list) {
|
|
198
210
|
if (expiresAt <= now || seenTokens.has(token)) continue;
|
|
199
211
|
seenTokens.add(token);
|
|
@@ -201,7 +213,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
201
213
|
if (!data) continue;
|
|
202
214
|
try {
|
|
203
215
|
const parsed = typeof data === "string" ? JSON.parse(data) : data;
|
|
204
|
-
if (!parsed?.session) continue;
|
|
216
|
+
if (!parsed?.session || await isRevokedSession(parsed.session, revokedBeforeByUser)) continue;
|
|
205
217
|
sessions.push(parseSessionOutput(ctx.options, {
|
|
206
218
|
...parsed.session,
|
|
207
219
|
expiresAt: new Date(parsed.session.expiresAt)
|
|
@@ -312,7 +324,10 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
312
324
|
});
|
|
313
325
|
const sessionTTL = getTTLSeconds(data.expiresAt, now);
|
|
314
326
|
if (sessionTTL > 0) await secondaryStorage.set(data.token, JSON.stringify({
|
|
315
|
-
session:
|
|
327
|
+
session: {
|
|
328
|
+
...sessionData,
|
|
329
|
+
createdAt: data.createdAt
|
|
330
|
+
},
|
|
316
331
|
user
|
|
317
332
|
}), sessionTTL);
|
|
318
333
|
return sessionData;
|
|
@@ -336,7 +351,7 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
336
351
|
if (!sessionStringified && (!options.session?.storeSessionInDatabase || ctx.options.session?.preserveSessionInDatabase)) return null;
|
|
337
352
|
if (sessionStringified) {
|
|
338
353
|
const s = safeJSONParse(sessionStringified);
|
|
339
|
-
if (!s) return null;
|
|
354
|
+
if (!s || await isRevokedSession(s.session)) return null;
|
|
340
355
|
return {
|
|
341
356
|
session: parseSessionOutput(ctx.options, {
|
|
342
357
|
...s.session,
|
|
@@ -372,11 +387,12 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
372
387
|
const uniqueTokens = [...new Set(sessionTokens)];
|
|
373
388
|
const sessionsByToken = /* @__PURE__ */ new Map();
|
|
374
389
|
const missingTokens = [];
|
|
390
|
+
const revokedBeforeByUser = /* @__PURE__ */ new Map();
|
|
375
391
|
if (secondaryStorage) for (const sessionToken of uniqueTokens) {
|
|
376
392
|
const sessionStringified = await secondaryStorage.get(sessionToken);
|
|
377
393
|
if (sessionStringified) try {
|
|
378
394
|
const s = typeof sessionStringified === "string" ? JSON.parse(sessionStringified) : sessionStringified;
|
|
379
|
-
if (!s) continue;
|
|
395
|
+
if (!s || await isRevokedSession(s.session, revokedBeforeByUser)) continue;
|
|
380
396
|
const expiresAt = new Date(s.session.expiresAt);
|
|
381
397
|
if (options?.onlyActiveSessions && expiresAt <= /* @__PURE__ */ new Date()) continue;
|
|
382
398
|
const session = {
|
|
@@ -590,6 +606,8 @@ const createInternalAdapter = (adapter, ctx) => {
|
|
|
590
606
|
join: { ...options?.includeAccounts ? { account: true } : {} }
|
|
591
607
|
});
|
|
592
608
|
if (!result) return null;
|
|
609
|
+
const normalize = (value) => value.normalize("NFC").toLowerCase();
|
|
610
|
+
if (normalize(result.email) !== normalize(email)) return null;
|
|
593
611
|
const { account: accounts, ...user } = result;
|
|
594
612
|
return {
|
|
595
613
|
user,
|