Magic Links
Passwordless sign-in via email. The user enters their email, receives a link, clicks it, and they are in. No password to remember, no password to steal.
Send a magic link
await auth.requestMagicLink({
email: "alice@example.com",
});This generates a single-use token, hashes it, stores the hash, and sends an email containing a link with the raw token.
By default, Own Auth also sends a link when the email does not belong to an existing user. The user is created only after the link is verified. Set allowMagicLinkSignup: false to allow existing users only.
When new-user signup is disabled, an unknown email returns the same public result shape without creating a token or sending an email.
Errors
| Code | When |
|---|---|
rate_limited | Too many magic-link requests for the email address. |
redirect_not_allowed | The requested redirect URL is not allowed. |
Email delivery
The email is sent through the configured emailProvider, which can use any email service, or through Own Auth Delivery. See Configuration for setup.
Link format
With baseUrl set to https://myapp.com, the email points to the application:
https://myapp.com/auth/magic-link/verify?token=abc123...Or if you use hosted links (for mobile apps):
https://go.own-auth.com/your-app/magic?token=abc123...The hosted page opens the configured mobile app destination. See Hosted Auth Links for setup.
For a complete request, test, and verification flow, follow Magic-link authentication. To send magic-link emails without configuring your own email provider, follow Authentication email delivery setup.
Verify a magic link
When the user opens the link, extract the token and verify it in the backend:
const result = await auth.verifyMagicLink({
token: tokenFromUrl,
});
if (result.status === "mfa_required") {
// Complete a configured second factor before creating a session.
} else {
const { user, session, sessionToken } = result;
}If the token is valid, it is consumed. Own Auth then creates a session or returns mfa_required when the user has a second factor enabled. The same token cannot be used again.
Errors
| Code | When |
|---|---|
expired_token | The link has expired. The default lifetime is 15 minutes. |
token_already_used | The link has already been used. |
invalid_token | The token is malformed, missing, or does not match a magic link. |
disabled_user | The user has been disabled. |
import { isAuthError } from "own-auth";
try {
const result = await auth.verifyMagicLink({
token: tokenFromUrl,
});
if (result.status === "mfa_required") {
// Show a second-factor form.
}
} catch (error) {
if (!isAuthError(error)) {
throw error;
}
switch (error.code) {
case "expired_token":
// Show: This link has expired. Request a new one.
break;
case "token_already_used":
// Show: This link has already been used.
break;
case "invalid_token":
// Show: This link is not valid.
break;
}
}How it works
- The application calls
requestMagicLink({ email }). - Own Auth generates a random token and hashes it with the token pepper.
- The hash is stored in
own_auth_tokenswith its type, expiry, email, and user ID when one exists. - The raw token is placed in the email link but is never stored.
- The user opens the link and the application sends the token to its backend.
- The backend calls
verifyMagicLink({ token }). - Own Auth hashes the incoming token and looks up the matching database record.
- If the token exists, has not expired, and has not been consumed, Own Auth consumes it and either creates a session or starts MFA.
The raw token never exists in the database. Reading own_auth_tokens does not reveal a usable magic link.
Auto-creating users
Magic links create new users by default. The user is created when a valid link is verified, not when the email is sent.
To restrict magic-link sign-in to existing users:
const auth = createOwnAuth({
tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
allowMagicLinkSignup: false,
});Configuration
const appUrl = "https://myapp.com";
const auth = createOwnAuth({
tokenPepper: process.env.OWN_AUTH_TOKEN_PEPPER,
baseUrl: appUrl,
redirectAllowlist: [appUrl],
tokenTtlMs: {
magic_link: 15 * 60 * 1000, // 15 minutes
},
});Keep the lifetime short because a magic link grants access to the account. The default is 15 minutes.
Next step
Add phone-based login with Phone login, or learn about Email verification for confirming user emails after sign-up.