Email OTP (Otp)
The passwordless OTP method emails the user a short verification code. The user enters it on the code-verify page; on success they are logged in — or, under the register-first policy, sent to the register form with a verified-email token.
How It Works
sequenceDiagram
actor U as User
participant P as Login page
participant A as Auth API
participant S as Temp key store
participant E as Email
participant B as PostAuth pipeline
U->>P: enter email
P->>A: POST /api/login
A->>S: store email:code under nonce
A->>E: send code (FuncEmailSend/Enqueue)
U->>P: enter code (+ nonce)
P->>A: POST /api/login-code-verify
A->>S: verify + consume code (single-use)
alt existing account
A->>B: authenticate → session + cookie
else new email (register-first)
A->>S: issue verified_key token
A-->>U: redirect /register?verified_key=
U->>A: submit names + verified_key
A->>B: single INSERT → session
end
- User enters their email on
/auth/loginand submits POST /auth/api/logingenerates a verification code, storesemail:codeunder a random nonce, and sends it viaFuncEmailSend/FuncEmailEnqueue- The client keeps the nonce (sessionStorage, never the URL) and lands on
/auth/login-code-verify POST /auth/api/login-code-verifychecks nonce + code (attempt-limited, single-use) and verifies the email- On success the post-auth pipeline resolves or creates the user, creates the session, and returns the redirect
Registration
Two policies, selected by ConfigShared.UserCreationPolicy:
CreateUserOnAuth(default) —PostAuthHooks.UserFindByEmailOrCreatecreates the user at verify time; incomplete profiles are gated to/auth/register(the completion form) byRegistrationCompleteCreateUserOnRegister— a verified email with no account receives averified_keytoken and lands on the register form instead; the single INSERT happens on form submit. RequiresFuncUserExists(or the passwordless find-by-email fallback)
Setup
a, err := auth.NewOtpAuth(types.ConfigOtp{
ConfigShared: types.ConfigShared{
Endpoint: "/auth",
UrlRedirectOnSuccess: "/",
UseCookies: true,
FuncTemporaryKeyGet: tempKeyGet,
FuncTemporaryKeySet: tempKeySet,
FuncUserFindByAuthToken: userFindByAuthToken,
FuncUserLogout: userLogout,
FuncUserStoreAuthToken: userStoreAuthToken,
EnableRegistration: true,
// optional: UserCreationPolicy: types.CreateUserOnRegister,
// optional: FuncEmailCaptured: captureLead,
},
FuncUserFindByEmail: userFindByEmail,
FuncUserRegister: userRegister, // required when EnableRegistration
FuncEmailSend: emailSend,
})
// Mount the built-in routes:
mux.Handle("/auth/", http.StripPrefix("", a.Router()))
Routes
| Route | Behavior |
|---|---|
GET|POST /auth/login | Email form + code-send API |
GET /auth/login-code-verify | Code entry page |
POST /auth/api/login-code-verify | Code verification → session or verified_key redirect |
GET|POST /auth/register | Register / completion form |
POST /auth/api/register-code-verify | Registration code verify → create + authenticate |
GET /auth/logout | Logout page |
Notes
- Codes are bound to a random nonce — a guessed code alone cannot be redeemed; attempts are limited (
utils.MaxCodeAttempts) - Per-email send throttle (
MaxEmailSends/EmailSendWindow) plus optional per-IP rate limiting - The nonce travels via
sessionStorage, keeping it out of history, logs, and Referer
A complete working example is available in examples/passwordless.