Back to guides

Updated 17 August 2026

Magic-link authentication: secure passwordless sign-in


auth.requestMagicLink creates a short-lived, single-use token and sends the sign-in URL through the configured email provider. auth.verifyMagicLink consumes the token in the backend and returns a session or an MFA challenge.

Request the same response for every email

request-magic-link.ts
await auth.requestMagicLink({
  email,
});

return { message: "If that address can sign in, a link has been sent." };

With allowMagicLinkSignup: false, an unknown email returns successfully without sending. Discard the method result and return the fixed response above. Own Auth limits each email to five requests in ten minutes.

Verify in the backend and create the session once

verify-magic-link.ts
export async function verifyMagicLink(token: string) {
  const result = await auth.verifyMagicLink({ token });

  if (result.status === "mfa_required") {
    return {
      status: result.status,
      challengeToken: result.challengeToken,
      methods: result.methods,
      expiresAt: result.expiresAt,
    };
  }

  return {
    status: result.status,
    userId: result.user.id,
    sessionToken: result.sessionToken,
    expiresAt: result.session.expiresAt,
  };
}

For a complete result, put sessionToken in the application's session transport. Replace the browser URL after verification. A refresh or second click cannot reuse the consumed link.

Use HTTPS links on mobile

For iOS and Android, use an HTTPS Universal Link or App Link and pass the token to the same backend verification endpoint. See the native iOS guide or the React Native, Flutter, Ionic, and Capacitor examples.

Test the magic-link email

Use MemoryEmailProvider in automated tests to inspect the message created by auth.requestMagicLink without sending email or logging the credential URL. Keep the provider instance inside the test process and assert only the fields the flow needs.

magic-link.test.ts
import assert from "node:assert/strict";
import test from "node:test";
import { createOwnAuth, MemoryEmailProvider } from "own-auth";

test("creates a magic-link email", async () => {
  const emailProvider = new MemoryEmailProvider();
  const auth = createOwnAuth({
    tokenPepper: "test-token-pepper",
    baseUrl: "http://localhost:3000",
    emailProvider,
  });

  await auth.requestMagicLink({ email: "alice@example.com" });

  assert.equal(emailProvider.messages.length, 1);

  const message = emailProvider.messages[0];
  assert.ok(message);
  assert.equal(message.to, "alice@example.com");
  assert.equal(message.type, "magic_link");

  const link = new URL(message.url);
  assert.equal(link.origin, "http://localhost:3000");
  assert.equal(link.pathname, "/auth/magic-link/verify");
  assert.ok(link.searchParams.get("token"));
});