From 1bb832de4ff41af17b291c36c33acb020461e989 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 6 Aug 2026 11:17:08 +0200 Subject: [PATCH 1/2] fix: show friendly message for expired/used magic-link errors Better Auth redirects magic-link verify failures with a raw code (INVALID_TOKEN) in the error query param, overwriting the message we set in errorCallbackURL. Map known codes to human-friendly text at render time on the login pages. --- src/app/routes/auth.js | 8 ++++++-- src/app/utils/friendly-error.js | 13 +++++++++++++ test/features/magic-links.test.js | 29 +++++++++-------------------- test/unit/friendly-error.test.js | 23 +++++++++++++++++++++++ 4 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 src/app/utils/friendly-error.js create mode 100644 test/unit/friendly-error.test.js diff --git a/src/app/routes/auth.js b/src/app/routes/auth.js index 372e994..20429c7 100644 --- a/src/app/routes/auth.js +++ b/src/app/routes/auth.js @@ -6,12 +6,14 @@ import { GitHubButton, MagicLinkButton } from "../components/login.js"; import { getAuthFromContext } from "../utils/auth.js"; import { logout } from "../handlers/logout.js"; import { getCallbackURL } from "../utils/callback-url.js"; +import { friendlyError } from "../utils/friendly-error.js"; import appConfig from "../../config.js"; function showLogin(c) { const error = c.req.query("error"); const success = c.req.query("success"); const callbackURL = getCallbackURL(c); + const friendly = error ? friendlyError(decodeURIComponent(error)) : null; return c.html( Layout({ @@ -22,7 +24,7 @@ function showLogin(c) {

Sign In

${Navigation({ back: { href: "/", text: "Back to Home" } })}
- ${Message({ error, success })} + ${Message({ error: friendly, success })}
${GitHubButton({ callbackURL })}
${MagicLinkButton({ callbackURL })}
@@ -36,6 +38,8 @@ function showLogin(c) { function showMagicLinkForm(c) { const callbackURL = c.req.query("callbackURL") || getCallbackURL(c); + const error = c.req.query("error"); + const friendly = error ? friendlyError(decodeURIComponent(error)) : null; return c.html( Layout({ @@ -47,7 +51,7 @@ function showMagicLinkForm(c) { ${Navigation({ back: { href: "/login", text: "Back to Login" } })}
${Message({ - error: c.req.query("error"), + error: friendly, success: c.req.query("success"), })}
diff --git a/src/app/utils/friendly-error.js b/src/app/utils/friendly-error.js new file mode 100644 index 0000000..660a9c3 --- /dev/null +++ b/src/app/utils/friendly-error.js @@ -0,0 +1,13 @@ +// Better Auth redirects magic-link errors by setting the `error` query param +// to a short code (e.g. INVALID_TOKEN), overwriting whatever message we set +// in errorCallbackURL. Map codes to human-friendly text at render time. +const FRIENDLY = { + INVALID_TOKEN: + "This sign-in link has expired or already been used. Please request a new one.", + new_user_signup_disabled: + "New sign-ups are currently disabled. Please contact support.", + failed_to_create_user: "We couldn't create your account. Please try again.", + failed_to_create_session: "We couldn't start a session. Please try again.", +}; + +export const friendlyError = (code) => FRIENDLY[code] ?? code; diff --git a/test/features/magic-links.test.js b/test/features/magic-links.test.js index dcc61f6..cfbb374 100644 --- a/test/features/magic-links.test.js +++ b/test/features/magic-links.test.js @@ -63,30 +63,19 @@ test("magic links feature tests", async (t) => { t.match(html, /email/i, "has email input"); }); - t.test("magic link URL is captured in test", async (t) => { + t.test("login page shows friendly message for INVALID_TOKEN", async (t) => { const testInstance = await getTestInstance(t); const app = createApp(testInstance.auth); - const { getMagicLinks } = testInstance; - const formData = new URLSearchParams(); - formData.append("email", "magic-url@example.com"); + const res = await app.request("/login?error=INVALID_TOKEN"); - await app.request("/login/magic-link", { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - body: formData.toString(), - }); - - const magicLinks = getMagicLinks(); - t.ok(magicLinks.length > 0, "magic link was generated"); - t.equal( - magicLinks[magicLinks.length - 1].email, - "magic-url@example.com", - "magic link for correct email", + t.equal(res.status, 200, "login page loads"); + const html = await res.text(); + t.match( + html, + /expired or already been used/, + "shows friendly message instead of raw code", ); - t.ok(magicLinks[magicLinks.length - 1].url, "magic link has URL"); - t.ok(magicLinks[magicLinks.length - 1].token, "magic link has token"); + t.ok(!html.includes("INVALID_TOKEN"), "does not show raw error code"); }); }); diff --git a/test/unit/friendly-error.test.js b/test/unit/friendly-error.test.js new file mode 100644 index 0000000..69a69c6 --- /dev/null +++ b/test/unit/friendly-error.test.js @@ -0,0 +1,23 @@ +import { test } from "tap"; +import { friendlyError } from "../../src/app/utils/friendly-error.js"; + +test("friendlyError maps known Better Auth codes", async (t) => { + t.match( + friendlyError("INVALID_TOKEN"), + /expired or already been used/, + "INVALID_TOKEN gets friendly text", + ); + t.equal( + friendlyError("new_user_signup_disabled"), + "New sign-ups are currently disabled. Please contact support.", + ); + t.equal( + friendlyError("failed_to_create_session"), + "We couldn't start a session. Please try again.", + ); +}); + +test("friendlyError passes through unknown codes", async (t) => { + t.equal(friendlyError("SOME_OTHER_CODE"), "SOME_OTHER_CODE"); + t.equal(friendlyError(""), ""); +}); From 13d45ca6fe2a341b93329710ee0401b4ca200199 Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Thu, 6 Aug 2026 11:19:37 +0200 Subject: [PATCH 2/2] refactor: drop dead error message from magic-link errorCallbackURL Better Auth overwrites the error param with its own code, so the embedded message never reached the user. Keep the /login destination. --- src/app/routes/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/routes/auth.js b/src/app/routes/auth.js index 20429c7..d80dd38 100644 --- a/src/app/routes/auth.js +++ b/src/app/routes/auth.js @@ -87,7 +87,7 @@ async function sendMagicLink(c) { body: { email, callbackURL, - errorCallbackURL: `${appConfig.base_url}/login?error=${encodeURIComponent("The magic link has expired or already been used")}`, + errorCallbackURL: `${appConfig.base_url}/login`, }, headers: c.req.raw.headers, });