User accounts & auth

env.AUTH is a complete authentication system for your app's own end-users — email + password, sessions, email verification, password reset, and email-based two-factor. It manages its own tables and session cookie; you call methods and return the responses it hands back.

Sign up & sign in

signUp and signIn return a result object. On success it includes a response — a Response with the session cookie already set. Return it (or copy its Set-Cookie) so the browser is signed in:

// api/login.js
export default {
  async fetch(request, env, ctx) {
    const { email, password } = await request.json();
    const r = await env.AUTH.signIn({ email, password });

    if (!r.ok) return Response.json({ error: r.error }, { status: 401 });

    if (r.requires2fa) {
      // a code was emailed; collect it, then call verifyTwoFactorOtp
      await env.AUTH.sendTwoFactorOtp({ otpToken: r.otpToken });
      return Response.json({ requires2fa: true, otpToken: r.otpToken });
    }

    return r.response; // session cookie set
  },
};

signUp({ email, password, name? }) works the same way and returns { ok, user, response }.

Reading the current user

On any request, read who's signed in. getUser returns the user or null; requireUser throws a 401 response if there's no session:

const user = await env.AUTH.getUser(request);
// => { id, email, name, emailVerified, twoFactorEnabled, createdAt } | null

Scope your data queries to user.id.

Two-factor (email OTP)

When a user has 2FA enabled, signIn returns requires2fa: true and an otpToken instead of a session. Email the code, then verify it to complete sign-in:

  • sendTwoFactorOtp({ otpToken }) — email the code.
  • verifyTwoFactorOtp({ otpToken, code }) — returns { ok, user, response }; return the response to set the session.
  • enableTwoFactor(request) / disableTwoFactor(request, { password }).

Sign in with Microsoft or Google

On top of email + password, env.AUTH can show a “Sign in with Microsoft” or “Sign in with Google” button on its sign-in screen — handy when your users already live in Microsoft 365 / Entra ID or Google Workspace and shouldn't need a separate password. It's an additional front door into the same env.AUTH, not a separate system.

A user who signs in this way becomes an ordinary env.AUTH account — the same _cr_users record you'd get from email + password. If an account with that email already exists, the sign-in links to it; otherwise a new, pre-verified one is created. Everything downstream is identical: getUser/requireUser return the same user, the same session cookie is set, and your code never has to know how they signed in. The provider's own MFA stands in for the password and email-OTP steps.

Enable a provider by setting its environment variables on the app:

  • Microsoft: register https://<your-app-domain>/_cr/auth/sso/microsoft/callback as a Web redirect URI in your Entra app registration, then set CR_AUTH_MS_CLIENT_ID and CR_AUTH_MS_CLIENT_SECRET (as a secret). Optional CR_AUTH_MS_TENANT locks sign-in to one organization.
  • Google: register https://<your-app-domain>/_cr/auth/sso/google/callback as an authorized redirect URI, then set CR_AUTH_GOOGLE_CLIENT_ID and CR_AUTH_GOOGLE_CLIENT_SECRET (as a secret). Optional CR_AUTH_GOOGLE_HD locks to one Workspace domain.

Then link a button on your own login page straight to the start URL — no OAuth code to write:

<a href="/_cr/auth/sso/microsoft/start?next=/dashboard">
  Sign in with Microsoft
</a>

/_cr/auth/sso/<provider>/start sends the user to the provider and comes back to /_cr/auth/sso/<provider>/callback, which establishes a normal env.AUTH session and redirects to next (a path on your app; defaults to /). If sign-in is refused — e.g. an unverified email, or an existing password account it isn't allowed to link — the user is sent to next with a ?cr_sso_error=… query param your page can read to show a message. It works whether or not your app uses the built-in MCP server; existing env.AUTH users are preserved (matched by email).

The full Entra step-by-step — single-tenant vs multi-tenant (many-customers) and the optional claims a SaaS app needs so existing accounts link safely — is on App as an MCP server. The same CR_AUTH_* config powers both your app's pages and (if you use it) the built-in MCP sign-in screen, which also renders these buttons automatically.

Password reset

A two-step flow. First the user asks for a reset; env.AUTH emails them a code. Then they enter that code plus a new password on a page you build, and you call resetPassword.

env.AUTH emails a code, not a magic link — it can't know your app's URL. So you expose your own reset page (e.g. /auth/reset) where the user pastes the code. The code expires in 1 hour and is single-use.
  1. Request a reset. Your “forgot password” form posts the email here. requestPasswordReset always returns { ok: true } — even if no such account exists — so you can never leak which emails are registered. Show the same neutral “check your email” message either way.
    // api/forgot-password.js
    export default {
      async fetch(request, env) {
        const { email } = await request.json();
        await env.AUTH.requestPasswordReset(email);
        // Always identical — never reveal whether the account exists.
        return Response.json({ ok: true });
      },
    };
  2. Reset with the code. Your reset page collects the code + new password and posts them here. On success, every existing session for that user is revoked — a reset kicks any attacker off every device, and the user signs in fresh.
    // api/reset-password.js  (your /auth/reset page posts here)
    export default {
      async fetch(request, env) {
        const { token, newPassword } = await request.json();
        const r = await env.AUTH.resetPassword({ token, newPassword });
        if (!r.ok) {
          // "weak_password"  — newPassword under 8 characters
          // "invalid_token"  — wrong, expired, or already-used code
          return Response.json({ error: r.error }, { status: 400 });
        }
        return Response.json({ ok: true }); // old sessions gone; user signs in again
      },
    };

Other methods

  • signOut(request) → a Response that clears the session cookie.
  • updateUser(request, { name }) — update profile fields.
  • changePassword(request, { current, new }) — revokes other sessions on success.
  • sendVerificationEmail(request) · verifyEmail(token).
  • Password reset — requestPasswordReset(email) · resetPassword({ token, newPassword }). See Password reset above for the full flow.

Sessions & storage

Sessions use an HttpOnly; Secure; SameSite=Lax cookie with a 30-day lifetime. User and session data live in the reserved _cr_users, _cr_sessions, and _cr_email_tokens tables in your app's database — don't touch these directly (see Database).

Verification, reset, and 2FA emails are sent through env.EMAIL automatically — there's nothing to configure.