Agent Wranglers

Sign-up Required

Prove the email address before trusting it

Mark an email address as verified only after someone opens a link or types a code sent to it. Until then, send no account data there and never use it to link accounts.

How to do it

Store email_verified_at as a time, empty at sign-up. Send a link with a single-use token of at least 128 random bits, valid for 24 hours. A 6-digit code instead lasts at most 15 minutes and dies after 5 wrong tries, because a million guesses are quick. Build the token as the sign-in links rule says, hashed, single use and behind a button page. The link only confirms the address and never signs anyone in, so it may last longer than a sign-in link. People may use the product while unverified, but block invitations, exports, billing email and linking a provider login. Allow a resend after 60 seconds, at most 5 an hour. A verified address proves access to an inbox only, so it never replaces a sign-in factor.

Example

const NEEDS_VERIFIED_EMAIL = new Set(['invite', 'export', 'link-identity', 'billing-email']);

export function assertEmailVerified(user, action) {
  if (NEEDS_VERIFIED_EMAIL.has(action) && !user.emailVerifiedAt) {
    throw new ForbiddenError('Confirm your email address first. We sent you a link.');
  }
}
NEEDS_VERIFIED_EMAIL = {"invite", "export", "link-identity", "billing-email"}

def assert_email_verified(user, action: str) -> None:
    if action in NEEDS_VERIFIED_EMAIL and user.email_verified_at is None:
        raise Forbidden("Confirm your email address first. We sent you a link.")

How to check

Sign up with a test address and skip the link. Invite a colleague, export data and link a provider login with the same address. Each is refused. Open the link, and all three work.

For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/sign-up/email-verification.