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 } | nullScope 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 theresponseto 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/callbackas a Web redirect URI in your Entra app registration, then setCR_AUTH_MS_CLIENT_IDandCR_AUTH_MS_CLIENT_SECRET(as a secret). OptionalCR_AUTH_MS_TENANTlocks sign-in to one organization. - Google: register
https://<your-app-domain>/_cr/auth/sso/google/callbackas an authorized redirect URI, then setCR_AUTH_GOOGLE_CLIENT_IDandCR_AUTH_GOOGLE_CLIENT_SECRET(as a secret). OptionalCR_AUTH_GOOGLE_HDlocks 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).
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.- Request a reset. Your “forgot password” form posts the email here.
requestPasswordResetalways 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 }); }, }; - 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)→ aResponsethat 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).