Back to blog

Updated 29 July 2026

How we hash magic links


A magic-link token is a short-lived bearer credential. Anyone holding the value can use it before expiry, so Own Auth generates it from a cryptographically secure random source, stores only a peppered hash, and admits one successful verification.

Creation stores a verifier, not the credential

auth.requestMagicLink generates the raw token and creates an own_auth_tokens record containing its protected hash, token type, expiry, email, and user ID when the email already belongs to a user. Own Auth passes the raw token to the configured email provider and does not persist it.

request-magic-link.ts
await auth.requestMagicLink({
  email: "alice@example.com",
});

Magic-link signup is enabled by default. For an unknown address, user creation is deferred until a valid link is verified. With allowMagicLinkSignup: false, an unknown address receives the same public request response, but Own Auth sends no message and creates no user. The application route should return its own fixed success shape rather than exposing the internal result.

That behavior limits account discovery through the request endpoint. OWASP applies the same principle to password recovery: public responses should not distinguish registered and unregistered accounts.

The token pepper protects database values

Own Auth derives the stored token hash with OWN_AUTH_TOKEN_PEPPER. The token record contains the hash and its metadata; it contains neither the raw token nor the pepper.

The pepper is defense in depth for a database-only compromise. It does not replace random generation, database access controls, or careful secret storage. Every application instance needs the same stable pepper. Changing it invalidates outstanding magic links and every session, phone code, invitation, and API key protected by the same secret.

Magic-link hashing has a different objective from password hashing. Human passwords come from a guessable input space and require a deliberately expensive password function such as Argon2id. Magic-link tokens are machine-generated high-entropy values. Their stored verifier supports exact lookup without retaining the bearer credential.

Verification is an atomic state change

auth.verifyMagicLink hashes the incoming token with the same pepper, validates its type and expiry, and atomically consumes the record. It returns a database-backed session when the user does not require MFA, or an MFA challenge when another factor is required. Concurrent requests produce one successful consumer.

verify-magic-link.ts
const result = await auth.verifyMagicLink({
  token: tokenFromUrl,
});

if (result.status === "mfa_required") {
  const challenge = {
    challengeToken: result.challengeToken,
    methods: result.methods,
    expiresAt: result.expiresAt,
  };
} else {
  const { user, session, sessionToken } = result;
}

Typed errors distinguish expired, already-used, and invalid tokens. The default magic-link lifetime is 15 minutes. Verification rejects an expired record even if cleanup has not removed it, and a consumed record cannot be reactivated for another attempt.

The URL remains sensitive outside the database

Hashing protects the stored record, but the delivered email necessarily contains the raw URL. RFC 8959 defines the relevant bearer property: possession is sufficient for use. Inbox systems, link scanners, browsers, reverse proxies, analytics, and support tools can all observe URLs. Magic-link routes should use HTTPS, exclude query strings from logs, avoid third-party analytics, and apply a referrer policy that does not forward the token to another origin.

Own Auth builds links from configured application data rather than trusting a request Host header. When a flow accepts a caller-selected destination, absolute targets are checked against redirectAllowlist; relative application paths remain available. Unapproved absolute targets fail with redirect_not_allowed.

Request limits protect the sending path

Own Auth rate-limits magic-link requests by normalized email address. That protects a recipient from repeated requests while the application's edge can apply independent network controls. The useful telemetry is request count and rate-limit decisions, never the credential-bearing URL.