← All case files

Case 009 · B2B web app · Login

Corporate customers’ login links arrive already used.

Email security scanners open one-time login links first, so business users get “link expired”.

A dark laptop showing a sign-in page: “This link has already been used” in red and a “Send a new link” button.
Before
Business users’ login links arrive already used.
After
Token spent only on click; scanner test in CI.

The bug

A B2B web app logs users in with one-time links sent by email. Company mailboxes often sit behind an email security scanner that opens the links in an incoming message to check them before a person does. The scan is a real visit. It uses up the one-time token, so by the time the user clicks, the link is spent.

How it shows up

Every tester on Gmail logs in without trouble. Then the first companies sign up, and their users click the link and get “Email link is invalid or has expired”. They ask for a new link, and that one arrives used too. Only business customers behind a mail scanner are affected, which usually means the paying ones, and they blame the product.

How a test finds it

Do what the scanner does. Request the login link once with a plain HTTP client, the way a security scanner fetches it, then open the same link in a browser as the user. If the second visit fails, every customer behind such a scanner is locked out. The test takes minutes to write and needs no Microsoft account.

The fix

Send a one-time code the user types in, instead of a link. Or make the link open a page with a “Confirm sign-in” button, so the token is spent only when a person clicks it. A scanner loads the page and leaves the token alone.

The test that catches it

A test fetches the login link once with a plain HTTP client, as a scanner would, then opens it in a browser and asserts that the user ends up signed in. It runs in CI on every change to the login flow.

login-link.spec.tsIllustrative
import { test, expect, request } from "@playwright/test";
import { requestLoginLink, signInFrom } from "./auth";

test("a mail scanner doesn't use up the login link", async ({ page }) => {
  const link = await requestLoginLink("buyer@company.example");

  // What an email security scanner does: fetch the link before anyone clicks.
  const scanner = await request.newContext();
  await scanner.get(link);

  await signInFrom(page, link);
  const expired = page.getByText("Email link is invalid or has expired");
  await expect(expired).toHaveCount(0);
  await expect(page.getByRole("button", { name: "Sign out" })).toBeVisible();
});

What it puts at risk

The company accounts the product was built to win, at the moment they first try it. A team that can’t log in on day one may not try again.

Tell us what you’re shipping.

An app, a rewrite, an AI agent, an automation, or a release that needs testing. A free 30-minute call, then a price in writing. If you don’t need us yet, we’ll say so.

Book a free call