Skip to content

About

Better Auth plugin for Chrome's Email Verification Protocol, with automatic fallback to any other sign-in method.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Repository files navigation

better-auth-evp

npm version Better Auth License: MIT TypeScript

Email Verification Protocol (EVP) plugin for Better Auth, with automatic fallback to whatever other sign-in method you already have.

Live preview → - a minimal sign-in screen wired up with this plugin, source in evp-demo repo.

What is EVP?

EVP is an experimental, Chrome-only browser capability (currently gated behind an origin trial) that lets a user prove they own an email address without typing an OTP or clicking a magic link. The browser talks to the user's mailbox provider directly and, if the user is signed in there, hands your form a signed token proving ownership - all triggered by the normal act of filling in and submitting an email field.

Because this depends on: the user running an origin-trial build of Chrome, their mailbox provider having implemented the issuer side of the protocol, and the user being signed into that mailbox in the browser - it will not work for most users today. This plugin is pure progressive enhancement: wire it up, try it first, and fall back to your existing sign-in method (OTP, magic link, password, ...) whenever it doesn't pan out.

Installation

npm install better-auth-evp

Uses email-verification-api (does the actual SD-JWT/DNS/issuer verification, built by the folks at Resend) as a dependency.

Peer Dependencies

  • better-auth ^1.5.0
  • zod ^3.0.0 || ^4.0.0

Server Setup

import { betterAuth } from "better-auth";
import { emailVerificationProtocol } from "better-auth-evp";

export const auth = betterAuth({
  // ...
  account: {
    accountLinking: {
      enabled: true,
      // Add "email-verification-protocol" alongside your other trusted
      // providers (e.g. "email-otp", "magic-link") so a user who already
      // has an account can also sign into it via EVP, instead of getting
      // a separate, unlinked account for the same email.
      trustedProviders: ["email-otp", "email-verification-protocol"],
    },
  },
  plugins: [
    emailVerificationProtocol({
      // Must match the origin your Chrome origin-trial token, DNS
      // `_email-verification` record, etc. were issued for.
      origin: "https://example.com",
      allowedEmailDomains: ["example.com"],
      disableSignUp: false,
      userFields: (verified) => ({
        // any additional fields for a newly created user
      }),
    }),
    // Keep your existing sign-in plugin(s) around for the fallback path,
    // e.g. emailOTP(), magicLink(), ...
  ],
});

Client Setup

import { createAuthClient } from "better-auth/react";
import { emailVerificationProtocolClient } from "better-auth-evp/client";

export const authClient = createAuthClient({
  plugins: [emailVerificationProtocolClient()],
});

Origin Trial Token

As a participating site you must register for the origin trial and serve the token on any page that renders the email form.

To get your token:

  1. Go to the EVP origin trial registration page and sign in.
  2. Enter your web origin (and check the box if you want to match subdomains too).
  3. Accept the terms.
  4. Copy the generated token.

Then serve it as a meta tag in the <head> of any page that renders the email form (we don't use the Origin-Trial HTTP header):

<meta http-equiv="origin-trial" content="YOUR_TOKEN" />

This plugin does not manage that token for you - it's static per-origin configuration, not something to fetch from an API.

Form Markup

<input name="email" type="email" autocomplete="email" />
<input type="hidden" name="token" nonce="{nonce}" autocomplete="email-verification-token" />

{nonce} comes from authClient.evp.getNonce() and must be re-fetched for every attempt.

Usage Example (progressive enhancement)

import { emailVerificationProtocolClient } from "better-auth-evp/client";

async function handleEmailSubmit(email: string, form: HTMLFormElement) {
  const tokenInput = form.elements.namedItem("token") as HTMLInputElement;

  if (tokenInput.value) {
    const { nonce } = await authClient.evp.getNonce();
    const result = await authClient.evp.verify({
      email,
      token: tokenInput.value,
      nonce,
    });

    if (result.data?.verified) {
      // User is signed in already - redirect and stop here.
      return;
    }
  }

  // EVP unsupported/unavailable/failed - fall back to your normal flow.
  await authClient.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
}

API

Server (auth.api)

  • evpGetNonce() - GET /evp/get-nonce - issues a single-use nonce, valid for nonceExpiresIn seconds (default 120).
  • evpVerify({ email, token, nonce }) - POST /evp/verify - verifies the token and, on success, creates a session (and a user, unless disableSignUp is set). Returns { verified: false, reason } instead of throwing on any expected failure (invalid/expired nonce, disallowed email domain, verification failure, email mismatch, sign-up disabled).

Client (authClient.evp)

  • getNonce()
  • verify({ email, token, nonce })

Options

Option Type Default Description
origin string (required) This relying party's absolute origin, used as audience.
nonceExpiresIn number 120 Seconds a nonce stays valid.
allowedEmailDomains string[] optional, unrestricted if omitted Restricts which email domains /evp/verify will even attempt to verify. The email field in your own form is client-side validation only and can be bypassed by calling the API directly - without this option, a caller can make the server perform a DNS lookup + issuer JWKS fetch against any domain they choose (SSRF/abuse surface). Strongly recommended whenever your app only expects a fixed set of domains.
disableSignUp boolean false Reject verified emails with no existing account.
userFields (verified) => T - Extra fields for a newly created user.
onVerified (verified & { userId }) => void | Promise<void> - Side-effect hook after a session is created.
verify custom verification function verifyEmailToken from email-verification-api Override for testing.

License

MIT

About

Better Auth plugin for Chrome's Email Verification Protocol, with automatic fallback to any other sign-in method.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages