mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-24 08:01:56 -05:00
fix: add backward compatibility for magic-link tokens without prefix
Ensure previously issued magic link tokens stored without the 'magic-link:' prefix can still be verified after upgrade. The verification logic now checks both the prefixed identifier (new format) and non-prefixed identifier (old format) for backward compatibility. - Check prefixed identifier first, then fallback to non-prefixed - Add test to verify backward compatibility works correctly
This commit is contained in:
@@ -340,10 +340,18 @@ export const magicLink = (options: MagicLinkOptions) => {
|
||||
ctx.context.baseURL,
|
||||
).toString();
|
||||
const storedToken = await storeToken(ctx, token);
|
||||
const tokenValue =
|
||||
// Try prefixed identifier first (new format), then fallback to non-prefixed (backward compatibility)
|
||||
let tokenValue =
|
||||
await ctx.context.internalAdapter.findVerificationValue(
|
||||
`magic-link:${storedToken}`,
|
||||
);
|
||||
if (!tokenValue) {
|
||||
// Fallback to non-prefixed identifier for backward compatibility
|
||||
tokenValue =
|
||||
await ctx.context.internalAdapter.findVerificationValue(
|
||||
storedToken,
|
||||
);
|
||||
}
|
||||
if (!tokenValue) {
|
||||
redirectWithError("INVALID_TOKEN");
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ describe("magic link storeToken", async () => {
|
||||
plugins: [
|
||||
magicLink({
|
||||
storeToken: "hashed",
|
||||
sendMagicLink(data, request) {
|
||||
sendMagicLink(data, _request) {
|
||||
verificationEmail = data;
|
||||
},
|
||||
}),
|
||||
@@ -402,8 +402,9 @@ describe("magic link storeToken", async () => {
|
||||
headers,
|
||||
});
|
||||
const hashedToken = await defaultKeyHasher(verificationEmail.token);
|
||||
const storedToken =
|
||||
await internalAdapter.findVerificationValue(`magic-link:${hashedToken}`);
|
||||
const storedToken = await internalAdapter.findVerificationValue(
|
||||
`magic-link:${hashedToken}`,
|
||||
);
|
||||
expect(storedToken).toBeDefined();
|
||||
const response2 = await auth.api.signInMagicLink({
|
||||
body: {
|
||||
@@ -426,10 +427,10 @@ describe("magic link storeToken", async () => {
|
||||
storeToken: {
|
||||
type: "custom-hasher",
|
||||
async hash(token) {
|
||||
return token + "hashed";
|
||||
return `${token}hashed`;
|
||||
},
|
||||
},
|
||||
sendMagicLink(data, request) {
|
||||
sendMagicLink(data, _request) {
|
||||
verificationEmail = data;
|
||||
},
|
||||
}),
|
||||
@@ -445,8 +446,9 @@ describe("magic link storeToken", async () => {
|
||||
headers,
|
||||
});
|
||||
const hashedToken = `${verificationEmail.token}hashed`;
|
||||
const storedToken =
|
||||
await internalAdapter.findVerificationValue(`magic-link:${hashedToken}`);
|
||||
const storedToken = await internalAdapter.findVerificationValue(
|
||||
`magic-link:${hashedToken}`,
|
||||
);
|
||||
expect(storedToken).toBeDefined();
|
||||
const response2 = await auth.api.signInMagicLink({
|
||||
body: {
|
||||
@@ -456,4 +458,62 @@ describe("magic link storeToken", async () => {
|
||||
});
|
||||
expect(response2.status).toBe(true);
|
||||
});
|
||||
|
||||
it("should verify tokens stored without prefix for backward compatibility", async () => {
|
||||
let _verificationEmail: VerificationEmail = {
|
||||
email: "",
|
||||
token: "",
|
||||
url: "",
|
||||
};
|
||||
const {
|
||||
auth,
|
||||
signInWithTestUser,
|
||||
testUser,
|
||||
customFetchImpl,
|
||||
sessionSetter,
|
||||
} = await getTestInstance({
|
||||
plugins: [
|
||||
magicLink({
|
||||
sendMagicLink(data) {
|
||||
_verificationEmail = data;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const internalAdapter = (await auth.$context).internalAdapter;
|
||||
await signInWithTestUser();
|
||||
|
||||
// Manually create a verification value without the prefix (simulating old format)
|
||||
const token = "backward-compat-token";
|
||||
await internalAdapter.createVerificationValue({
|
||||
identifier: token, // No prefix - old format
|
||||
value: JSON.stringify({ email: testUser.email }),
|
||||
expiresAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
});
|
||||
|
||||
// Verify that the token can still be verified (backward compatibility)
|
||||
const client = createAuthClient({
|
||||
plugins: [magicLinkClient()],
|
||||
fetchOptions: {
|
||||
customFetchImpl,
|
||||
},
|
||||
baseURL: "http://localhost:3000",
|
||||
basePath: "/api/auth",
|
||||
});
|
||||
|
||||
const verifyHeaders = new Headers();
|
||||
const response = await client.magicLink.verify({
|
||||
query: {
|
||||
token,
|
||||
},
|
||||
fetchOptions: {
|
||||
onSuccess: sessionSetter(verifyHeaders),
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.data?.token).toBeDefined();
|
||||
const betterAuthCookie = verifyHeaders.get("set-cookie");
|
||||
expect(betterAuthCookie).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user