Repository navigation
Password
Username or email plus password (Argon2id) against users in your database, with optional TOTP second factor, backup codes and self-registration. JSON endpoints only; your frontend renders the forms.
From examples/password/main.go (tested by its main_test.go):
provider, err := password.New(&password.Options{
Sessions: store,
Storage: options.Storage,
EnableRegistration: true, // POST /auth/password/register: open sign-up for the demo
TOTP: password.TOTPOptions{Enabled: true, Issuer: "Example"}, // optional second factor
OnLogin: grantRoles(options.AdministratorID),
})
if err != nil {
return nil, err
}
guard, err := session.NewGuard(&session.GuardOptions{
Store: store,
LoginURL: "/login", // where RequireUI sends anonymous browsers
Refreshers: []session.Refresher{provider}, // a deleted user loses access within a minute
})
if err != nil {
return nil, err
}
serveMux := http.NewServeMux()
auth.RegisterRoutes(serveMux, provider.Routes()) // the provider reads the session itself: no Guard in frontLoginURL is your frontend's login page: the password provider has no redirect flow.
You implement password.Storage (11 methods) on your users table; password.NewMemoryStorage() is for tests and examples only. Contracts that matter:
- Usernames and emails are unique and matched ignoring case (unique indexes on
lower(...)); a duplicate ispassword.ErrUserExistsfrom the unique constraint, not from a lookup before the insert. -
UpdatePasswordHash,ConsumeTOTPAttempt,ConsumeTOTPStep,ConsumeBackupCodeandSetTOTPare each one atomic statement (compare-and-swap);SetTOTPreturns(bool, error)and enables only while TOTP is off. - Not found is
password.ErrUserNotFound. - SQL for every method: docs/migration.md. Create users with
password.Hash(plain, password.DefaultParameters()); keep roles and flags in your own columns.
Prefix PathPrefix, default /auth/password. The provider reads sessions itself, so its routes need no Guard. Bodies are JSON (Content-Type: application/json, at most 64 KiB, unknown fields refused).
| Method and path | Answer | Registered when |
|---|---|---|
POST /login {"identifier","password"}
|
200 identity, or 202 {"challenge":"totp"} / {"challenge":"totp_setup"}
|
always |
POST /logout |
204; 403 forbidden for a request the browser marks Sec-Fetch-Site: cross-site
|
always |
POST /register {"username","email","password"} (at least one of username, email) |
201 identity (logged in), or 202 {"challenge":"totp_setup"} with TOTP.Required
|
EnableRegistration |
POST /totp/verify {"code","backupCode":false}
|
200 identity; "backupCode":true checks the code against the backup codes only, false against the TOTP codes only |
TOTP.Enabled |
GET /totp |
200 {"enabled"}
|
TOTP.Enabled |
POST /totp/setup {}
|
200 {"secret","otpauthUrl","issuer","accountName"}
|
TOTP.Enabled |
POST /totp/enable {"code","password"}
|
200 {"backupCodes":[...]}; a wrong code can be retried with the same setup; 409 totp_already_enabled when a concurrent enrollment won |
TOTP.Enabled |
POST /totp/disable {"password"}
|
204; a pending setup is dropped, so enable needs a new one | TOTP.Enabled |
-
TOTP.Requiredenrolls users without a second factor during login and registration (the 202totp_setupchallenge, then/totp/setupand/totp/enable) instead of failing every route. - Limits:
TOTP.Digits6 or 8,Periodat most 3600 s,Skewat most 10,BackupCodeCountat most 20; backup codes are at most 128 bytes and the alphabet and length must give at least 1,000,000 distinct codes. - Every TOTP code is accepted once (
Storage.ConsumeTOTPStep).MaximumAttempts(default 5, no "unlimited") counts wrong codes in a row across logins; then the second factor stays locked until a code is verified or your application callsStorage.ResetTOTPAttempts. -
Policy.MinimumLengthcounts characters; passwords are 1-128 bytes. Stable client codes replace translated messages. - Existing Argon2id hashes verify unchanged;
LegacyVerifier: password.VerifyBcryptupgrades bcrypt hashes at the next login, and Argon2id hashes on other parameters thanHashParametersare rehashed too (only login rehashes, not the password checks of the TOTP routes). A stored hash nothing can verify answers like a wrong password (401invalid_credentials, same cost); unknown users cost the same Argon2id verification as real ones, except users whose hash is still a legacy one: they cost the legacy verification until their first login (Security).
| Option | Default | Note |
|---|---|---|
Sessions, Storage
|
required | |
PathPrefix |
/auth/password |
segments of A-Z a-z 0-9 . _ ~ -, no trailing /, no . or .. segment |
EnableRegistration |
off | off means the route is not registered at all |
Policy.MinimumLength |
12 characters | composition rules opt-in; maximum 128 bytes |
TOTP.Enabled, TOTP.Issuer
|
off | Issuer required when enabled |
TOTP.Required |
off | users without TOTP enroll during login (202 totp_setup) |
TOTP.MaximumAttempts |
5 | wrong codes in a row across logins; then locked until Storage.ResetTOTPAttempts
|
LegacyVerifier |
none |
password.VerifyBcrypt upgrades bcrypt hashes at the next login |
OnLogin |
none | must keep UserID
|
The provider has no password-change or reset route; your application does it: confirm the current password (password.Verify against the stored hash, or a fresh login), check the new one with Policy.Validate (the provider's Options.Policy), then Storage.UpdatePasswordHash(ctx, id, currentHash, password.Hash(newPassword, parameters)). Every session of the user, including the one that asked for the change, ends within RefreshInterval (default 1 minute), and the TOTP routes refuse them at once; let the user log in again with the new password.
invalid_credentials (401: login, TOTP enable and disable), no_pending_login and invalid_totp_code (401: verify, enable), totp_setup_missing (400), totp_already_enabled, totp_not_enabled (409), user_exists (409), weak_password, invalid_username, invalid_email (400: register), forbidden (403: cross-site logout; the TOTP routes for an impersonating or inactive identity). Shape and mapping: Errors-and-Observability.
-
OnLoginmust keepidentity.UserID(theStorageuser ID); returnidentity.Clone()with changes. A replacedUserIDfails the login with 500. - The provider as
Refresherre-reads the user every minute and updates onlyUsernameandEmail; a deleted user loses the session, and so does a session opened before the user's password hash or second factor changed (UpdatePasswordHash, a login's rehash, TOTP enable or disable in another session); the TOTP routes refuse such a session at once. The session that enabled or disabled TOTP, or whose login rehashed the password, stays. Sessions from before this check (no credential stamp) end at their first refresh: users log in once more after the upgrade. A login pending its second factor (or its enrollment) is bound the same way: after a password or second-factor change it ends withno_pending_login, so a TOTP or backup code alone never completes a login with the old password. Roles added byOnLoginstay until the session ends. -
TOTP.Enabledoff while users have TOTP enrolled: their logins fail with 500. Clear their TOTP first. - Anyone with the password can lock the second factor (
MaximumAttempts); rate-limit/loginand/totp/verifyat the edge, and/registertoo (each costs an anonymous Argon2id). There is no in-process rate limit. - With
TOTP.Required, enrollment is trust-on-first-use. - Usernames are 3-50 of
[a-zA-Z0-9_]and never contain@; the oneidentifierfield is an email when it contains@.
Source: password/ (provider.go, options.go, storage.go, login.go, totp_routes.go).
Mirror of docs/wiki at v1.3.0 (486bfa3). Edit the repository, not this wiki.