Skip to content

Commit 8a0475f

Browse files
committed
fix(db): support verification operations with secondary storage (#8247)
1 parent 01ae4a7 commit 8a0475f

File tree

23 files changed

+513
-124
lines changed

23 files changed

+513
-124
lines changed

packages/better-auth/src/api/routes/password.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export const resetPassword = createAuthEndpoint(
308308
} else {
309309
await ctx.context.internalAdapter.updatePassword(userId, hashedPassword);
310310
}
311-
await ctx.context.internalAdapter.deleteVerificationValue(verification.id);
311+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(id);
312312

313313
if (ctx.context.options.emailAndPassword?.onPasswordReset) {
314314
const user = await ctx.context.internalAdapter.findUserById(userId);

packages/better-auth/src/api/routes/update-user.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,9 @@ export const deleteUserCallback = createAuthEndpoint(
643643
await ctx.context.internalAdapter.deleteUser(session.user.id);
644644
await ctx.context.internalAdapter.deleteSessions(session.user.id);
645645
await ctx.context.internalAdapter.deleteAccounts(session.user.id);
646-
await ctx.context.internalAdapter.deleteVerificationValue(token.id);
646+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
647+
`delete-account-${ctx.query.token}`,
648+
);
647649

648650
deleteSessionCookie(ctx);
649651

packages/better-auth/src/db/internal-adapter.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,13 +233,13 @@ describe("internal adapter test", async () => {
233233
});
234234

235235
it("should delete verification by value with hooks", async () => {
236-
const verification = await internalAdapter.createVerificationValue({
236+
await internalAdapter.createVerificationValue({
237237
identifier: `test-id-1`,
238238
value: "test-id-1",
239239
expiresAt: new Date(Date.now() + 1000),
240240
});
241241

242-
await internalAdapter.deleteVerificationValue(verification.id);
242+
await internalAdapter.deleteVerificationByIdentifier("test-id-1");
243243
expect(hookVerificationDeleteBefore).toHaveBeenCalledOnce();
244244
expect(hookVerificationDeleteAfter).toHaveBeenCalledOnce();
245245
});

packages/better-auth/src/db/internal-adapter.ts

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,19 +1139,6 @@ export const createInternalAdapter = (
11391139

11401140
return (verification[0] as Verification) || null;
11411141
},
1142-
/**
1143-
* Note: In secondary-only mode, this is a no-op since secondary storage
1144-
* is keyed by identifier, not id. Use deleteVerificationByIdentifier instead.
1145-
*/
1146-
deleteVerificationValue: async (id: string) => {
1147-
if (!secondaryStorage || options.verification?.storeInDatabase) {
1148-
await deleteWithHooks(
1149-
[{ field: "id", value: id }],
1150-
"verification",
1151-
undefined,
1152-
);
1153-
}
1154-
},
11551142
deleteVerificationByIdentifier: async (identifier: string) => {
11561143
const storageOption = getStorageOption(
11571144
identifier,
@@ -1174,17 +1161,55 @@ export const createInternalAdapter = (
11741161
);
11751162
}
11761163
},
1177-
updateVerificationValue: async (
1178-
id: string,
1164+
updateVerificationByIdentifier: async (
1165+
identifier: string,
11791166
data: Partial<Verification>,
11801167
) => {
1181-
const verification = await updateWithHooks<Verification>(
1182-
data,
1183-
[{ field: "id", value: id }],
1184-
"verification",
1185-
undefined,
1168+
const storageOption = getStorageOption(
1169+
identifier,
1170+
options.verification?.storeIdentifier,
1171+
);
1172+
const storedIdentifier = await processIdentifier(
1173+
identifier,
1174+
storageOption,
11861175
);
1187-
return verification;
1176+
1177+
if (secondaryStorage) {
1178+
const cached = await secondaryStorage.get(
1179+
`verification:${storedIdentifier}`,
1180+
);
1181+
if (cached) {
1182+
const parsed = safeJSONParse<Verification>(cached);
1183+
if (parsed) {
1184+
const updated = { ...parsed, ...data };
1185+
const expiresAt = updated.expiresAt ?? parsed.expiresAt;
1186+
const ttl = getTTLSeconds(
1187+
expiresAt instanceof Date ? expiresAt : new Date(expiresAt),
1188+
);
1189+
if (ttl > 0) {
1190+
await secondaryStorage.set(
1191+
`verification:${storedIdentifier}`,
1192+
JSON.stringify(updated),
1193+
ttl,
1194+
);
1195+
}
1196+
if (!options.verification?.storeInDatabase) {
1197+
return updated;
1198+
}
1199+
}
1200+
}
1201+
}
1202+
1203+
if (!secondaryStorage || options.verification?.storeInDatabase) {
1204+
const verification = await updateWithHooks<Verification>(
1205+
data,
1206+
[{ field: "identifier", value: storedIdentifier }],
1207+
"verification",
1208+
undefined,
1209+
);
1210+
return verification;
1211+
}
1212+
return data as Verification;
11881213
},
11891214
};
11901215
};

packages/better-auth/src/plugins/email-otp/routes.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -378,25 +378,26 @@ export const checkVerificationOTP = (opts: RequiredEmailOTPOptions) =>
378378
if (!verificationValue) {
379379
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
380380
}
381+
const otpIdentifier = `${ctx.body.type}-otp-${email}`;
381382
if (verificationValue.expiresAt < new Date()) {
382-
await ctx.context.internalAdapter.deleteVerificationValue(
383-
verificationValue.id,
383+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
384+
otpIdentifier,
384385
);
385386
throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
386387
}
387388

388389
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
389390
const allowedAttempts = opts?.allowedAttempts || 3;
390391
if (attempts && parseInt(attempts) >= allowedAttempts) {
391-
await ctx.context.internalAdapter.deleteVerificationValue(
392-
verificationValue.id,
392+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
393+
otpIdentifier,
393394
);
394395
throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
395396
}
396397
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
397398
if (!verified) {
398-
await ctx.context.internalAdapter.updateVerificationValue(
399-
verificationValue.id,
399+
await ctx.context.internalAdapter.updateVerificationByIdentifier(
400+
otpIdentifier,
400401
{
401402
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
402403
},
@@ -1097,9 +1098,10 @@ export const requestEmailChangeEmailOTP = (opts: RequiredEmailOTPOptions) =>
10971098
if (!currentEmailVerificationValue) {
10981099
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
10991100
}
1101+
const currentEmailIdentifier = `email-verification-otp-${email}`;
11001102
if (currentEmailVerificationValue.expiresAt < new Date()) {
1101-
await ctx.context.internalAdapter.deleteVerificationValue(
1102-
currentEmailVerificationValue.id,
1103+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1104+
currentEmailIdentifier,
11031105
);
11041106
throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
11051107
}
@@ -1109,8 +1111,8 @@ export const requestEmailChangeEmailOTP = (opts: RequiredEmailOTPOptions) =>
11091111
);
11101112
const allowedAttempts = opts?.allowedAttempts || 3;
11111113
if (attempts && parseInt(attempts) >= allowedAttempts) {
1112-
await ctx.context.internalAdapter.deleteVerificationValue(
1113-
currentEmailVerificationValue.id,
1114+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1115+
currentEmailIdentifier,
11141116
);
11151117
throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
11161118
}
@@ -1122,16 +1124,16 @@ export const requestEmailChangeEmailOTP = (opts: RequiredEmailOTPOptions) =>
11221124
ctx.body.otp,
11231125
);
11241126
if (!verified) {
1125-
await ctx.context.internalAdapter.updateVerificationValue(
1126-
currentEmailVerificationValue.id,
1127+
await ctx.context.internalAdapter.updateVerificationByIdentifier(
1128+
currentEmailIdentifier,
11271129
{
11281130
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
11291131
},
11301132
);
11311133
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
11321134
}
1133-
await ctx.context.internalAdapter.deleteVerificationValue(
1134-
currentEmailVerificationValue.id,
1135+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1136+
currentEmailIdentifier,
11351137
);
11361138
} else {
11371139
if (ctx.body.otp) {
@@ -1265,34 +1267,35 @@ export const changeEmailEmailOTP = (opts: RequiredEmailOTPOptions) =>
12651267
if (!verificationValue) {
12661268
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
12671269
}
1270+
const changeEmailIdentifier = `change-email-otp-${email}-${newEmail}`;
12681271
if (verificationValue.expiresAt < new Date()) {
1269-
await ctx.context.internalAdapter.deleteVerificationValue(
1270-
verificationValue.id,
1272+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1273+
changeEmailIdentifier,
12711274
);
12721275
throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
12731276
}
12741277

12751278
const [otpValue, attempts] = splitAtLastColon(verificationValue.value);
12761279
const allowedAttempts = opts?.allowedAttempts || 3;
12771280
if (attempts && parseInt(attempts) >= allowedAttempts) {
1278-
await ctx.context.internalAdapter.deleteVerificationValue(
1279-
verificationValue.id,
1281+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1282+
changeEmailIdentifier,
12801283
);
12811284
throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
12821285
}
12831286

12841287
const verified = await verifyStoredOTP(ctx, opts, otpValue, ctx.body.otp);
12851288
if (!verified) {
1286-
await ctx.context.internalAdapter.updateVerificationValue(
1287-
verificationValue.id,
1289+
await ctx.context.internalAdapter.updateVerificationByIdentifier(
1290+
changeEmailIdentifier,
12881291
{
12891292
value: `${otpValue}:${parseInt(attempts || "0") + 1}`,
12901293
},
12911294
);
12921295
throw APIError.from("BAD_REQUEST", ERROR_CODES.INVALID_OTP);
12931296
}
1294-
await ctx.context.internalAdapter.deleteVerificationValue(
1295-
verificationValue.id,
1297+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1298+
changeEmailIdentifier,
12961299
);
12971300

12981301
const currentUser =
@@ -1371,8 +1374,8 @@ async function atomicVerifyOTP(
13711374
}
13721375

13731376
if (verificationValue.expiresAt < new Date()) {
1374-
await ctx.context.internalAdapter.deleteVerificationValue(
1375-
verificationValue.id,
1377+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1378+
identifier,
13761379
);
13771380
throw APIError.from("BAD_REQUEST", ERROR_CODES.OTP_EXPIRED);
13781381
}
@@ -1381,16 +1384,14 @@ async function atomicVerifyOTP(
13811384
const allowedAttempts = opts?.allowedAttempts || 3;
13821385

13831386
if (attempts && parseInt(attempts) >= allowedAttempts) {
1384-
await ctx.context.internalAdapter.deleteVerificationValue(
1385-
verificationValue.id,
1387+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
1388+
identifier,
13861389
);
13871390
throw APIError.from("FORBIDDEN", ERROR_CODES.TOO_MANY_ATTEMPTS);
13881391
}
13891392

13901393
// Atomically delete token before verification to prevent race condition
1391-
await ctx.context.internalAdapter.deleteVerificationValue(
1392-
verificationValue.id,
1393-
);
1394+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(identifier);
13941395

13951396
const verified = await verifyStoredOTP(ctx, opts, otpValue, providedOTP);
13961397

packages/better-auth/src/plugins/magic-link/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,8 +354,8 @@ export const magicLink = (options: MagicLinkOptions) => {
354354
redirectWithError("INVALID_TOKEN");
355355
}
356356
if (tokenValue.expiresAt < new Date()) {
357-
await ctx.context.internalAdapter.deleteVerificationValue(
358-
tokenValue.id,
357+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
358+
storedToken,
359359
);
360360
redirectWithError("EXPIRED_TOKEN");
361361
}
@@ -369,13 +369,13 @@ export const magicLink = (options: MagicLinkOptions) => {
369369
attempt?: number | undefined;
370370
};
371371
if (attempt >= opts.allowedAttempts) {
372-
await ctx.context.internalAdapter.deleteVerificationValue(
373-
tokenValue.id,
372+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
373+
storedToken,
374374
);
375375
redirectWithError("ATTEMPTS_EXCEEDED");
376376
}
377-
await ctx.context.internalAdapter.updateVerificationValue(
378-
tokenValue.id,
377+
await ctx.context.internalAdapter.updateVerificationByIdentifier(
378+
storedToken,
379379
{
380380
value: JSON.stringify({
381381
email,

packages/better-auth/src/plugins/mcp/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,8 +489,8 @@ export const mcp = (options: MCPOptions) => {
489489
});
490490
}
491491

492-
await ctx.context.internalAdapter.deleteVerificationValue(
493-
verificationValue.id,
492+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
493+
code.toString(),
494494
);
495495

496496
if (!client_id) {
@@ -612,8 +612,8 @@ export const mcp = (options: MCPOptions) => {
612612
}
613613

614614
const requestedScopes = value.scope;
615-
await ctx.context.internalAdapter.deleteVerificationValue(
616-
verificationValue.id,
615+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
616+
code.toString(),
617617
);
618618
const accessToken = generateRandomString(32, "a-z", "A-Z");
619619
const refreshToken = generateRandomString(32, "A-Z", "a-z");

packages/better-auth/src/plugins/oidc-provider/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,8 @@ export const oidcProvider = (options: OIDCOptions) => {
594594
}
595595

596596
if (!ctx.body.accept) {
597-
await ctx.context.internalAdapter.deleteVerificationValue(
598-
verification.id,
597+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
598+
consentCode,
599599
);
600600
return ctx.json({
601601
redirectURI: `${value.redirectURI}?error=access_denied&error_description=User denied access`,
@@ -605,8 +605,8 @@ export const oidcProvider = (options: OIDCOptions) => {
605605
const codeExpiresInMs =
606606
(opts?.codeExpiresIn ?? DEFAULT_CODE_EXPIRES_IN) * 1000;
607607
const expiresAt = new Date(Date.now() + codeExpiresInMs);
608-
await ctx.context.internalAdapter.updateVerificationValue(
609-
verification.id,
608+
await ctx.context.internalAdapter.updateVerificationByIdentifier(
609+
consentCode,
610610
{
611611
value: JSON.stringify({
612612
...value,
@@ -812,8 +812,8 @@ export const oidcProvider = (options: OIDCOptions) => {
812812
});
813813
}
814814

815-
await ctx.context.internalAdapter.deleteVerificationValue(
816-
verificationValue.id,
815+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
816+
code.toString(),
817817
);
818818
if (!client_id) {
819819
throw new APIError("UNAUTHORIZED", {
@@ -921,8 +921,8 @@ export const oidcProvider = (options: OIDCOptions) => {
921921
}
922922

923923
const requestedScopes = value.scope;
924-
await ctx.context.internalAdapter.deleteVerificationValue(
925-
verificationValue.id,
924+
await ctx.context.internalAdapter.deleteVerificationByIdentifier(
925+
code.toString(),
926926
);
927927
const accessToken = generateRandomString(32, "a-z", "A-Z");
928928
const refreshToken = generateRandomString(32, "A-Z", "a-z");

packages/better-auth/src/plugins/one-time-token/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ export const oneTimeToken = (options?: OneTimeTokenOptions | undefined) => {
183183
message: "Invalid token",
184184
});
185185
}
186-
await c.context.internalAdapter.deleteVerificationValue(
187-
verificationValue.id,
186+
await c.context.internalAdapter.deleteVerificationByIdentifier(
187+
`one-time-token:${storedToken}`,
188188
);
189189
if (verificationValue.expiresAt < new Date()) {
190190
throw c.error("BAD_REQUEST", {

0 commit comments

Comments
 (0)