password

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

README

github.com/ubgo/auth/plugin/password

Lets users sign up and sign in with a password, reset a forgotten one through a link sent to their verified recovery address or number, and change it.

Install

go get github.com/ubgo/auth/plugin/password

This is its own module, so apps without passwords never get its dependency. It brings golang.org/x/crypto (argon2id). The hibp subpackage (breach checks, see below) lives in the same module and uses only the standard library.

Wire it

From examples/quickstart:

a, err := auth.New(
	auth.WithDB(db, sqlb.SQLiteDialect{}),
	auth.WithBaseURL(baseURL),
	auth.WithPlugins(
		email.New(email.Login(), email.Recover(), email.WithMailer(m)),
		password.New(),
	),
)

The plugin asks the core for identifiers, so it needs at least one identifier plugin (plugin/email, plugin/phone or plugin/username). auth.New refuses a configuration nobody could sign in or recover with, or whose limits no password could pass:

Error Text Fix
ErrNoLoginKind password: no identifier can sign in; add one with Login(), for example email.New(email.Login()) Give an identifier plugin Login().
ErrNoRecovery password: no identifier can recover an account; add one with Recover(), for example email.New(email.Recover()), or pass password.WithoutRecovery() Give an identifier plugin Recover(), or opt out by name.
ErrNoResetLinks password: reset links need auth.WithBaseURL or password.ResetLinks Set a base URL or build the links yourself.
ErrBadLengths password: MinLength must be 0 or more, and MaxLength at least 1 and at least MinLength MaxLength(0) refused every password; pass lengths a password can meet.
ErrBadArgon2 password: Argon2 needs Time and Threads of at least 1 argon2 cannot run with a Time or Threads of 0; also returned by Argon2id.Hash.

Options

Option What it does Default
MinLength(n) Shortest new password, in characters. Existing shorter passwords still sign in. 8 (DefaultMinLength)
MaxLength(n) Longest password, in bytes. Sign-in refuses a longer one before hashing, which bounds the argon2 work of an anonymous request. 256 (DefaultMaxLength)
Argon2(params) argon2id cost. A stored hash with other parameters is re-hashed at the user's next successful sign-in. No effect with WithHasher. 64 MiB, 3 passes, 2 lanes (DefaultArgon2)
WithHasher(h) Replaces argon2id as the algorithm new and re-hashed passwords are stored with: anything behind the Hasher interface (an HSM, a key-derivation service). The dummy hash for unknown accounts comes from it too. Yesterday's argon2id rows keep working only if AcceptLegacyHashes(password.Argon2id{Params: …}) lists them. Argon2id with the Argon2 parameters
p.Hasher() Not an option: returns the hasher the plugin hashes and checks with (the WithHasher one, or Argon2id with its parameters), better-auth's ctx.context.password.hash/verify, for a hook that must hash under the same policy.
AcceptLegacyHashes(v...) Verifies stored hashes in other formats, for a user base imported from another stack, and re-hashes each one with the Hasher at the user's next successful sign-in: Bcrypt() (Rails, Laravel, Spring; $2a$, $2b$, $2y$), Scrypt(params) for hexsalt:hexkey strings with BetterAuthScrypt as the better-auth parameters, DjangoPBKDF2() (pbkdf2_sha256$…), or any Verifier. Scrypt takes ScryptParams because the salt:key form carries none. Asked in order; the first that Recognizes a string decides. A Hasher that also implements Rehasher (as Argon2id does) gets its own out-of-date hashes replaced too. none: a hash in another format is a server error at sign-in
GuardCredentialStuffing(policy) better-auth Sentinel's credentialStuffing, opt-in: failed sign-ins are counted in the core rate store under an HMAC of the password keyed from auth.WithSecret (StuffingPolicy.PasswordFailures, default 10) and, with GlobalFailures, globally; over a limit a sign-in is refused with 429 auth.rate_limited before its password is checked, for the Window (default 15 minutes). LogOnly only logs. Crossing a limit emits one auth.SecurityCredentialStuffing event to the security event stream when a sink is registered (once per window and scope, outcome blocked, or logged under LogOnly). Risk: users of a sprayed password, and with GlobalFailures everyone, are refused while it lasts; warned at every start. Needs auth.WithSecret and an auth.RatePeeker store (auth.ErrRatePeekUnsupported at auth.New otherwise) off
WithoutRecovery() Runs without password reset: the forgot and reset operations and the forgot rate rule do not exist. recovery on
WithoutSignUp() Removes sign-up (the route is 404, the Go call ErrOperationNotWired) and its rate rules, for an app that creates accounts another way. The core's auth.WithDisabledOperations(password.EndpointSignUp) is the app-side way to the same end. sign-up on
SignUpWithoutSession() Sign-up creates the account and issues no session; the client signs in as a separate step. a session at sign-up
ResetByCode() Forgot sends a one-time code instead of a link, through the same recovery channel (a text for a phone, a mail for an address), and reset-code replaces reset: the client posts the identifier, the code and the new password. No base URL or ResetLinks needed. The code lives ResetTTL and is spent by the core's attempt cap after a few wrong guesses. links
ResetTTL(d) How long a reset link, or a reset code, works. 1 hour (DefaultResetTTL)
ResetLinks(fn) Builds the reset URL from the token. It must open an app page that POSTs the token with the new password. BaseURL + "/reset-password?token=…" (DefaultResetPage)
Policy(fn) An extra rule every new password must pass, checked after length. A returned *auth.Error reaches the client as is; any other error becomes ErrWeak. none
RequireClasses(n) Needs characters from at least n of four classes: lowercase, uppercase, digits, everything else. none
CheckBreached(c) Refuses new passwords a BreachChecker reports as breached. A checker error fails the request. A checker that is also a BreachMessenger (hibp with WithMessage) words the refusal; the code stays password.breached. none
KeepSessionsOnReset() A reset leaves every session alive, better-auth's default (revokeSessionsOnPasswordReset: false). Risk: a thief who already holds a session keeps it after the owner resets. Open reset links are still spent and trusted devices still forgotten. a reset signs out every session
History(n) Refuses a new password that matches the current one or any of the n-1 before it (ErrReused), at change, reset, reset-code, change-expired and SetPassword. Earlier passwords are kept as hashes in auth_password_history, created by the next migrate; n is 1 to 24 (MaxHistory), since every remembered hash costs one hash check per change. Turning it off later leaves the table, which DeleteUser then no longer clears. off
ExpireAfter(d) A password expires d after it was set. A sign-in with the right, expired password is ErrExpired and issues no session; the client posts the identifier, the old and a new password to change-expired, which changes it and signs in. The re-hash at sign-in does not restart the clock; existing sessions are not ended when a password expires. The new password must differ from the expired one. NIST advises against periodic expiry; set it when a policy demands it. off
KeepSessionsOnChange() A change leaves the other sessions alive unless the request sends revokeOtherSessions: true, better-auth's default. Same risk. Without it revokeOtherSessions is not read, so a client cannot weaken the default. a change signs out every other session

Routes and Go API

Routes are mounted under the authhttp prefix (/auth in the examples). All are POST with a JSON body.

Route Go method Requirement Rate limit
POST /auth/password/sign-up SignUp(ctx, SignUpRequest{Identifier, Password, Name, Image, Remember, Username, DisplayUsername, Fields}) Public 10/hour per IP (signUpIPLimit), 5/hour per identifier (signUpIDLimit); absent with WithoutSignUp
POST /auth/password/sign-in SignIn(ctx, SignInRequest{Identifier, Password, Remember}) Public 20/minute per IP (signInIPLimit), 5 per 15 minutes per account (signInAccountLimit)
POST /auth/password/forgot Forgot(ctx, identifier), ForgotWith(ctx, ForgotRequest{Identifier, RedirectTo}) Public 3/hour per identifier (forgotLimit); absent with WithoutRecovery
POST /auth/password/reset Reset(ctx, ResetRequest{Token, Password}) Public none; absent with WithoutRecovery or ResetByCode
POST /auth/password/reset-code ResetWithCode(ctx, ResetCodeRequest{Identifier, Code, Password}) Public 20/minute per IP, 10/hour per identifier (resetCodeLimit); exists only with ResetByCode
POST /auth/password/change Change(ctx, ChangeRequest{Current, New, RevokeOtherSessions}) FreshSignIn none
POST /auth/password/change-expired ChangeExpired(ctx, ChangeExpiredRequest{Identifier, Current, New, Remember}) Public as sign-in, 20/minute per IP and 5 per 15 minutes per account; exists only with ExpireAfter
POST /auth/password/verify Verify(ctx, password) SignedIn 20/minute per IP (verifyIPLimit)
POST /auth/password/reauthenticate Reauthenticate(ctx, password) returns auth.Reauthentication SignedIn as sign-in: 20/minute per IP, 5 per 15 minutes per signed-in user
(Go only) HasPassword(ctx, userID) none none
(Go only) SetPassword(ctx, userID, password) none none
(Go only) ImportHash(ctx, userID, hash) none: call it from an import none

Per-IP rules count HTTP requests only; Go API calls have no IP. JSON field names: identifier, password, name, image, remember, username, displayUsername, redirectTo, token, current, new, revokeOtherSessions.

  • Sign-up and sign-in return SignInResult: the user, verificationRequired (the account was created but the identifier needs verifying first, so no session was issued), secondFactorRequired (the session is pending until a second factor passes). The session token is set as the session cookie or bearer token and is never in the JSON body.
  • Forgot always answers {"status":"check_your_inbox"} (ForgotStatus). Under ResetByCode it sends a code (Message.Code, purpose auth.PurposeReset) instead of a link. redirectTo (better-auth's field) replaces the page the link opens for this one request: a site path, made absolute on the base URL, or an http(s) URL on a trusted origin (auth.Auth.TrustsReturnURL); the token is added as its token query parameter and the page POSTs it to reset. A custom scheme, a loopback address or any other target is password.redirect_invalid with nothing sent, judged on the input alone, so the refusal says nothing about the account.
  • Reset-code (ResetByCode) sets the password after the core checks the code against the account the identifier names; an unknown identifier, a wrong code and a spent or expired one all answer auth.code_invalid. Then as reset: open reset links spent, every session signed out.
  • Sign-up with username claims the handle through the registered UsernameClaimer in the same transaction (displayUsername is the form to show, checked by the claimer's display rules): a taken (auth.identity_taken) or reserved (username.reserved) handle fails the whole sign-up and leaves no account; a body with a username in an app with no claimer is ErrNoUsernames. UsernameClaimer is one method, ClaimWithDisplay(ctx, userID, handle, display string) error, found through auth.PluginOf: *username.Plugin is the default, and an app with its own handle scheme registers a plugin with that method instead, so this plugin imports no identifier plugin.
  • Reset sets the password, spends every other open reset link and signs out every session (not under KeepSessionsOnReset).
  • Change needs the current password when one exists (a user without one, such as a social sign-up, sets the first password without it), signs out every other session (under KeepSessionsOnChange only when revokeOtherSessions is true) and spends every open reset link.
  • Change-expired (ExpireAfter) is sign-in and change in one, for an expired password only: an unknown identifier, an account without a password and a wrong current password all cost one hash check and answer ErrInvalidCredentials; only the right password learns auth.ErrNotVerified, ErrNotExpired (the password has not expired: sign in and use change) or ErrReused. Then as reset: open reset links spent, every session signed out, a new session issued (pending when a second factor is on).
  • Verify checks the signed-in user's own password and changes nothing, for an app step that asks for it again. Wrong, over-long, or no password on the account is ErrInvalidCredentials, with the same argon2 cost as sign-in.
  • Reauthenticate checks the password as verify does and makes the session the user holds fresh again for FreshSignIn operations (auth.Reauthenticate), with no new session or token. When the user's second factor is due it answers secondFactorRequired with a single-use ticket for POST /auth/twofactor/reauthenticate. A wrong password ends nothing. See re-authentication.
  • SetPassword (Go only, no route) sets or replaces a user's password from server code: an admin or support flow, an import. The same rules apply, every open reset link is spent and every session of the user is signed out. Unknown user: auth.ErrUserNotFound.
  • ImportHash (Go only, no route) stores a hash from another system as the user's password, as it is: the user signs in with the old password and is re-hashed then. The string must be one the Hasher or an AcceptLegacyHashes verifier recognizes (ErrHashUnrecognized), the user must exist (auth.ErrUserNotFound) and have no password (ErrHasPassword). No session ends and nothing is sent.
  • Reset, change and SetPassword also announce the change through auth.Auth.CredentialChanged, so plugins that implement auth.CredentialListener react: the two-factor plugin forgets the user's trusted devices, because a device trusted before the response to a stolen account must not outlive it.

Importing passwords from another stack

Store each old hash string as it is with ImportHash(ctx, userID, hash) and list a verifier for its format in AcceptLegacyHashes; ImportHash refuses a string no verifier recognizes, so a wrong column fails at import time. A user signs in with the old password; the plugin verifies it with the legacy verifier and, in the same transaction, stores an argon2id hash in its place, so the legacy format disappears one user at a time and nobody is asked to reset. Until then an imported account costs its old algorithm's time at sign-in while an unknown identifier costs argon2id's: that difference is inherent in verifying foreign hashes and ends at the first sign-in. A stored string no verifier recognizes is a server error, not a wrong password, so a botched import shows up in the logs on the first sign-in rather than as a wave of "invalid credentials". Scrypt checks the password as typed; better-auth normalizes it to NFKC first, so the rare password whose NFKC form differs verifies only after a reset.

Tables and columns

Table Columns Notes
auth_password (TableNamePassword, alias pw) user_id (primary key; deleted with the user by a.DeleteUser, no foreign key), hash (argon2id PHC string, or the Hasher's format; an imported row may hold a legacy format until the user's next sign-in), updated_at One row per user who has a password. updated_at is when the password was set; the re-hash at sign-in keeps it, so it is the password's age, which ExpireAfter reads.
auth_password_history (TableNamePasswordHistory, alias pwh), only with History id, user_id (indexed; deleted with the user), hash (a replaced password's hash, as it was stored), seq (1, 2, … per user, newest highest), created_at At most n-1 rows per user; older rows are deleted as new ones are written.

Migrations 0001_password, and 0002_password_history with History, owner password. Typed columns for reads: TablePassword, UserID, Hash, UpdatedAt. This table is not part of the core's stable-column contract (stability_test.go covers auth_user, auth_identity and auth_session only), so treat its columns as internal.

Errors

Variable Code HTTP When
ErrInvalidCredentials password.invalid_credentials 401 Wrong password, unknown identifier, account without a password, over-long password at sign-in or verify, or a wrong current password at change. The same answer for all of them.
ErrTooShort password.too_short 400 New password under MinLength.
ErrTooLong password.too_long 400 New password over MaxLength.
ErrWeak password.weak 400 A Policy or RequireClasses rule failed with a plain error.
ErrBreached password.breached 400 The BreachChecker reported the password.
ErrNoUsernames password.no_usernames 400 Sign-up carried a username but no UsernameClaimer (the username plugin) is wired.
ErrHashUnrecognized password.hash_unrecognized 400 ImportHash with a string neither the Hasher nor a legacy verifier recognizes.
ErrHasPassword password.has_password 409 ImportHash for a user who already has a password.
ErrRedirectInvalid password.redirect_invalid 400 Forgot with a redirectTo that is not a site path or an http(s) URL on a trusted origin.
ErrReused password.reused 400 History: the new password matches the current one or a remembered one; change-expired: it matches the expired one.
ErrExpired password.expired 403 ExpireAfter: the right password, older than the period, at sign-in. Call change-expired.
ErrNotExpired password.not_expired 400 Change-expired with the right password that has not expired.

Core errors you will also see: auth.ErrNotVerified (right password, but the identifier's kind requires verification), auth.ErrTokenInvalid (reset link used, expired or unknown), auth.ErrFreshSessionRequired (change on an old session), auth.ErrIdentifierInvalid (sign-up with a value no identifier kind accepts), auth.ErrRateLimited.

Security notes

Property Pinned by
Sign-in is enumeration-safe in answer: unknown account and wrong password give the same error. TestPassword_SignInIsEnumerationSafe
Sign-in is enumeration-safe in time: an unknown identifier runs one argon2 check against a dummy hash made with the configured cost. TestPassword_UnknownAccountCostsAHash, TestGuard_PasswordSignInAlwaysHashesOnce, TestInit_DummyHashUsesTheConfiguredCost
Only a caller who knows the password learns that the identifier is unverified. TestPassword_RequireVerified
Signing up with an address someone already verified tells the owner, succeeds as a separate account that can never verify it, and reveals nothing to the caller, including for identifier kinds that cannot deliver messages. TestPassword_SignUpWithTakenAddressTellsTheOwner, TestPassword_SquatterCannotTakeTheAddress, TestFactors_PasswordSignUpTakenValueOfUndeliverableKind
A reset link goes only to a verified recovery identity, never to the value typed; forgot answers the same for every input. TestPassword_ForgotAndReset, TestFactors_PasswordForgotWithoutAVerifiedChannel
Reset tokens are single use, expire, and a reset signs out every session. TestPassword_ForgotAndReset, TestPassword_ResetTokenExpires, TestVerification_ConcurrentUseSucceedsOnce
A reset code is single use, checked only against the account it was issued for, and an unknown identifier answers as a wrong code; a weak new password does not spend it. TestFactors_PasswordResetByCode, TestFactors_PasswordResetByCodeSMS, TestFactors_PasswordResetByCodeFaults
A sign-up that claims a username is one transaction: a taken or reserved handle leaves no account. TestFactors_PasswordSignUpWithUsername
Any UsernameClaimer takes the claim; its refusal leaves no account; with none the field is ErrNoUsernames. TestUsernameClaimer_SignUpUsesAnyClaimer
fields sets only the auth_user columns auth.WithSignUpFields names, each of its declared type; an unknown, core or wrong-typed field is 400 auth.field_invalid and leaves no account. TestR21_PasswordSignUpFields
Change needs a fresh session and the current password, signs out other sessions, and spends open reset links. TestPassword_ChangeNeedsFreshSessionAndSignsOutOthers
A corrupt or hostile stored hash is an error, never a match, and its parameters cannot make argon2 allocate gigabytes. TestCheckPassword_RejectsMalformedHashes, FuzzParseHash, TestFactors_PasswordSignInRefusals
A stored hash in a format no configured verifier recognizes is a server error, never invalid_credentials and never a match; a legacy verifier is never handed another format's string, and the PBKDF2 iteration count and bcrypt cost of a hostile row are bounded. TestCheckPassword_DispatchesByFormat, TestFactors_PasswordUnrecognizedHashIsAServerError, TestDjangoPBKDF2_CorruptStrings, TestBcrypt_PrefixesAndCorruptStrings
An imported hash is replaced by the Hasher's hash in the same sign-in that verified it, and a mismatch never triggers a re-hash. TestFactors_PasswordLegacyHashSignsInAndIsRehashed, TestFactors_PasswordWithHasher, TestFactors_PasswordLegacyFaults
Passwords never print: Secret redacts in every format and in JSON; the session token is never in a JSON body. TestRequests_NeverPrintThePassword, TestSignInResult_NeverEncodesTheToken
Per-account limits stop guessing from many IPs. TestPassword_RateLimitPerAccount

Limits: the per-account sign-in rule also slows the real owner while someone is guessing (5 tries per 15 minutes, signInAccountLimit), which is the cost of stopping a distributed guess. The in-memory rate store (auth.NewMemoryRateStore) counts per process; with several instances pass a shared store through auth.WithRateStore, such as contrib/redis.

Breach checks: package hibp

github.com/ubgo/auth/plugin/password/hibp checks new passwords against the Have I Been Pwned range API. Only the first five hex characters of the password's SHA-1 leave the process (prefixLen); the service answers every suffix under that prefix and the match happens locally, in constant time per row. It asks for padded answers, and padding rows (count 0) are never a match.

password.New(password.CheckBreached(hibp.New()))
Option What it does Default
WithBaseURL(u) Another server, such as a self-hosted mirror. https://api.pwnedpasswords.com/range/ (DefaultBaseURL)
WithHTTPClient(hc) Replaces the HTTP client; its own timeout applies. client with a 5 second timeout (DefaultTimeout)
FailOpen() Accepts the password when the service cannot answer. fail closed: the sign-up, reset or change fails with the error
WithMessage(msg) Words the refusal, better-auth's customPasswordCompromisedMessage (WithMessage). The code and status stay; a auth.WithMessages entry for password.breached still wins in its locale. the declared message

Pinned by TestBreached_MatchesTheSuffixAndSendsOnlyThePrefix, TestBreached_FailsClosedUnlessToldOtherwise, TestBreached_TransportErrorFailsClosed.

Example

Documentation

Overview

Package password is the password sign-in method: argon2id hashes in its own auth_password table, sign-up, sign-in with any sign-in identifier, forgotten-password reset through whatever recovery channel the user has, and change.

It has no email code and no SMS code. It asks the core for the user behind an identifier and for a recovery channel, so it works the same in an email-first, phone-first or handle-only app.

Index

Constants

View Source
const (
	EndpointSignUp auth.EndpointID = "password.sign-up"
	EndpointSignIn auth.EndpointID = "password.sign-in"
	EndpointForgot auth.EndpointID = "password.forgot"
	EndpointReset  auth.EndpointID = "password.reset"
	EndpointChange auth.EndpointID = "password.change"
	EndpointVerify auth.EndpointID = "password.verify"
	// EndpointReauthenticate proves the signed-in user's password on the
	// session they hold and makes it fresh again (auth.Reauthenticate),
	// instead of a new sign-in that would create a second session.
	EndpointReauthenticate auth.EndpointID = "password.reauthenticate"
	// EndpointResetCode resets the password with a one-time code instead of
	// a link; it replaces EndpointReset under ResetByCode.
	EndpointResetCode auth.EndpointID = "password.reset-code"
)

Operation ids, the permanent names hooks, rate rules and transports address the plugin's operations by.

View Source
const (
	//ds:def id=tablenamepassword-wn2mjcwy owner=@khanakia stability=stable
	TableNamePassword  = "auth_password"
	TableAliasPassword = "pw"
	ColUserID          = "user_id"
	ColHash            = "hash"
	// ColUpdatedAt is when the password was last set: sign-up, change,
	// reset, change-expired, SetPassword or ImportHash. The re-hash at
	// sign-in replaces the hash and keeps this time, so it is the password's
	// age, which ExpireAfter reads.
	ColUpdatedAt = "updated_at"
)

The plugin's table: one row per user who has a password, keyed by the user id, so "has a password" is a primary-key lookup.

View Source
const (
	// DefaultMinLength is NIST SP 800-63B's minimum for a user-chosen
	// password. It counts characters, not bytes.
	//ds:def id=defaultminlength-8nyr776x owner=@khanakia stability=stable
	DefaultMinLength = 8
	// DefaultMaxLength is in bytes: it bounds the input argon2 hashes, which
	// is what the limit protects. 256 bytes is 64 four-byte characters, the
	// NIST floor for the maximum.
	//ds:def id=defaultmaxlength-zux6j6ma owner=@khanakia stability=stable
	DefaultMaxLength = 256
	// DefaultResetTTL: one hour, long enough to find the mail.
	//ds:def id=defaultresetttl-2vkb6wb9 owner=@khanakia stability=stable
	DefaultResetTTL = time.Hour
)

Length limits. The maximum bounds the work one request can ask of argon2; NIST asks that at least 64 characters be allowed.

View Source
const (
	//ds:def id=tablenamepasswordhistory-r25pwh02 owner=@khanakia stability=stable
	TableNamePasswordHistory  = "auth_password_history"
	TableAliasPasswordHistory = "pwh"
	ColHistoryID              = "id"
	ColHistoryUserID          = "user_id"
	ColHistoryHash            = "hash"
	// ColHistorySeq orders a user's rows, 1 for the first replaced password
	// and one more for each after it. A counter rather than created_at,
	// because two changes in one clock tick (or one second, on a coarse
	// timestamp) would tie, and pruning must know which row is oldest.
	ColHistorySeq       = "seq"
	ColHistoryCreatedAt = "created_at"
)

The history table: one row per earlier password of a user, newest kept, created only under History. Rows hold hashes, never passwords, in whatever format the Hasher wrote them; a legacy format is still compared through AcceptLegacyHashes.

View Source
const (
	// DefaultStuffingPasswordFailures failures of one password in a window
	// refuse it. Ten, because a person mistyping their own password fails a
	// few times, while a spray tries the same password against hundreds of
	// accounts.
	//
	//ds:def id=password-stuffing-defaults-7qk2wn5m owner=@khanakia stability=stable
	DefaultStuffingPasswordFailures = 10
	// DefaultStuffingWindow is the counting window and the refusal's length.
	DefaultStuffingWindow = 15 * time.Minute
)

Defaults of StuffingPolicy.

View Source
const DefaultResetPage = "/reset-password"

DefaultResetPage is the app page a reset link opens; it reads the token from the query and POSTs it with the new password to password.reset.

View Source
const EndpointChangeExpired auth.EndpointID = "password.change-expired"

EndpointChangeExpired changes an expired password and signs in, the one way in once ExpireAfter has refused a sign-in. It exists only under ExpireAfter.

View Source
const ForgotStatus = "check_your_inbox"

ForgotStatus is the one answer forgot ever gives.

View Source
const MaxHistory = 24

MaxHistory is the most passwords History may remember. Every remembered hash costs one hash check per change, so the cap bounds the work one change can ask of argon2; 24 is the largest history Active Directory allows.

View Source
const PluginName = "password"

PluginName is the plugin's permanent identity. It owns the plugin's rows in the migration ledger and its tables and columns, so renaming it would make the next migrate see a new plugin; it never changes.

View Source
const PurposeResetCode auth.Purpose = "password.reset_code"

PurposeResetCode is the one-time-code purpose of a reset code (ResetByCode). Its subject is the user id, so a code is only ever checked against the account it was issued for.

View Source
const PurposeResetToken auth.Purpose = "password.reset"

PurposeResetToken is the verification purpose of a reset link. Its Identifier is the user id.

View Source
const WarnCredentialStuffing = "" /* 207-byte string literal not displayed */

WarnCredentialStuffing is logged at every start while the guard refuses (not LogOnly): it refuses legitimate users who share a sprayed password, and under GlobalFailures every user while an attack lasts.

Variables

TablePassword and its typed columns, for an app that reads the table (reads are open; only the plugin writes it).

View Source
var (
	ErrInvalidCredentials = auth.NewError("password.invalid_credentials", http.StatusUnauthorized, "The sign-in details are not right.")
	ErrTooShort           = auth.NewError("password.too_short", http.StatusBadRequest, "Choose a longer password.")
	ErrTooLong            = auth.NewError("password.too_long", http.StatusBadRequest, "Choose a shorter password.")
	ErrWeak               = auth.NewError("password.weak", http.StatusBadRequest, "Choose a stronger password.")
	ErrBreached           = auth.NewError("password.breached", http.StatusBadRequest, "This password has appeared in a data breach. Choose another one.")

	// ErrNoUsernames: a sign-up body carried a username, but no
	// UsernameClaimer (the username plugin) is wired, so there is nothing to
	// claim it with.
	ErrNoUsernames = auth.NewError("password.no_usernames", http.StatusBadRequest, "Usernames are not available.")
	// ErrHashUnrecognized refuses ImportHash with a string that neither the
	// Hasher nor any AcceptLegacyHashes verifier recognizes, so a botched
	// export fails at import time rather than as a server error at the
	// user's first sign-in.
	ErrHashUnrecognized = auth.NewError("password.hash_unrecognized", http.StatusBadRequest, "The password hash is in no recognized format.")
	// ErrHasPassword refuses ImportHash for a user who already has a
	// password: an import must never silently replace a credential the user
	// set here; SetPassword is the deliberate replacement.
	ErrHasPassword = auth.NewError("password.has_password", http.StatusConflict, "This user already has a password.")
	// ErrRedirectInvalid refuses a forgot request whose redirectTo is not a
	// page of this site or of an origin the deployment trusts
	// (auth.Auth.TrustsReturnURL), or is a custom scheme or loopback return,
	// which may carry nothing secret; also a site path when there is no
	// auth.WithBaseURL to make it absolute. It depends on the input only, so
	// it says nothing about whether an account exists.
	//ds:def id=errredirectinvalid-password-6pk3wq8n owner=@khanakia stability=stable
	ErrRedirectInvalid = auth.NewError("password.redirect_invalid", http.StatusBadRequest, "This page cannot receive a reset link.")

	ErrNoLoginKind  = errors.New("password: no identifier can sign in; add one with Login(), for example email.New(email.Login())")
	ErrNoRecovery   = errors.New("password: no identifier can recover an account; add one with Recover(), for example email.New(email.Recover()), or pass password.WithoutRecovery()")
	ErrNoResetLinks = errors.New("password: reset links need auth.WithBaseURL or password.ResetLinks")
	// ErrBadLengths refuses a MaxLength under 1 or under MinLength, and a
	// negative MinLength: with MaxLength(0) every sign-up answered
	// password.too_long, a zero limit that looked like a working setting.
	ErrBadLengths = errors.New("password: MinLength must be 0 or more, and MaxLength at least 1 and at least MinLength")
)

Errors. The *auth.Error values are what a client sees; the plain errors are configuration mistakes auth.New refuses at start-up.

TablePasswordHistory and its typed columns, for an app that reads the table (reads are open; only the plugin writes it).

View Source
var (
	// ErrReused refuses a new password that matches the current one or one
	// History remembers. It is reached only by someone who already proved the
	// account (a session with the current password, a reset link or code), so
	// it reveals nothing to a stranger.
	//
	//ds:def id=errreused-r25pwh03 owner=@khanakia stability=stable
	ErrReused = auth.NewError("password.reused", http.StatusBadRequest, "Choose a password you have not used recently.")
	// ErrExpired refuses a sign-in with the right password once it is older
	// than ExpireAfter. Only the right password learns it, as with
	// auth.ErrNotVerified; the client then calls change-expired.
	//
	//ds:def id=errexpired-r25pwh04 owner=@khanakia stability=stable
	ErrExpired = auth.NewError("password.expired", http.StatusForbidden, "Your password has expired. Choose a new one.")
	// ErrNotExpired refuses change-expired for a password that has not
	// expired: the route is the way past an expired password, not a second
	// change route without a session. Only the right password learns it.
	ErrNotExpired = auth.NewError("password.not_expired", http.StatusBadRequest, "This password has not expired. Sign in and change it from your account.")

	// ErrHistoryRange is a configuration mistake auth.New refuses: History
	// takes 1 to MaxHistory.
	ErrHistoryRange = fmt.Errorf("password: History takes 1 to %d passwords", MaxHistory)
	// ErrExpireRange is a configuration mistake auth.New refuses: ExpireAfter
	// takes a positive duration.
	ErrExpireRange = errors.New("password: ExpireAfter takes a positive duration")
)
View Source
var BetterAuthScrypt = ScryptParams{N: 16384, R: 16, P: 1, KeyLen: 64}

BetterAuthScrypt are the parameters better-auth hashes passwords with (@better-auth/utils/password): N 16384, r 16, p 1, a 64-byte key, a 16-byte hex salt. An app moving its users from better-auth passes them to Scrypt.

View Source
var DefaultArgon2 = Argon2Params{Memory: 64 * 1024, Time: 3, Threads: 2}

DefaultArgon2 follows the OWASP guidance for argon2id: 64 MiB, 3 passes, 2 lanes. About 50 ms on a current server core.

View Source
var ErrBadArgon2 = errors.New("password: Argon2 needs Time and Threads of at least 1")

ErrBadArgon2 is returned for argon2 parameters argon2 itself cannot run: a Time or Threads of 0. argon2.IDKey panics on them, so before this check password.Argon2(Argon2Params{}) made auth.New panic in Init's dummy hash.

Functions

This section is empty.

Types

type Argon2Params

type Argon2Params struct {
	// Memory is the memory cost in KiB.
	Memory uint32
	// Time is the number of passes over the memory.
	Time uint32
	// Threads is the degree of parallelism (lanes).
	Threads uint8
}

Argon2Params are the argon2id cost parameters. They are written into every stored hash, so a hash is always checked with the cost it was made with, and a hash whose parameters differ from the plugin's is re-hashed at the next successful sign-in.

type Argon2id

type Argon2id struct {
	Params Argon2Params
}

Argon2id is the default Hasher: argon2id in the PHC string format, with the cost in Params. It is exported so an app can wrap it (log timings, run it in a worker pool) or combine it with other verifiers through WithHasher and AcceptLegacyHashes. The zero value has no usable cost; take Params from DefaultArgon2 or your own.

func (Argon2id) Hash

func (h Argon2id) Hash(pw Secret) (string, error)

Hash returns the PHC string of pw. It cannot fail: crypto/rand.Read never returns an error since Go 1.24 (it crashes the program instead, because a process without randomness must not mint salts), so the error is always nil and exists only to satisfy Hasher.

func (Argon2id) NeedsRehash

func (h Argon2id) NeedsRehash(encoded string) bool

NeedsRehash reports whether encoded was made with other parameters than Params, so the plugin can upgrade it at the next successful sign-in. A string that does not parse needs no re-hash, because Verify refuses it first.

func (Argon2id) Recognizes

func (h Argon2id) Recognizes(encoded string) bool

Recognizes reports whether encoded is an argon2id PHC string.

func (Argon2id) Verify

func (h Argon2id) Verify(pw Secret, encoded string) (bool, error)

Verify reports whether pw matches encoded. The comparison is constant-time.

type BreachChecker

type BreachChecker interface {
	Breached(ctx context.Context, password string) (bool, error)
}

BreachChecker reports whether a password is known from a breach, such as a client of the Have I Been Pwned range API (package hibp). An interface so an app can use a local list or another service instead.

type BreachMessenger

type BreachMessenger interface {
	BreachedMessage() string
}

BreachMessenger is an optional upgrade of a BreachChecker that words the refusal itself (better-auth's customPasswordCompromisedMessage; package hibp's WithMessage). A non-empty BreachedMessage replaces ErrBreached's message; the code (password.breached) and the status never change, so a client matching codes sees no difference, and a WithMessages catalogue entry for the code still wins in its locale.

type ChangeExpiredRequest

type ChangeExpiredRequest struct {
	Identifier string `json:"identifier"`
	Current    Secret `json:"current"`
	New        Secret `json:"new"`
	// Remember is the "remember me" box, as on SignInRequest.
	Remember *bool `json:"remember,omitempty"`
}

ChangeExpiredRequest is the input of change-expired: the identifier, the expired password and the new one.

func (ChangeExpiredRequest) RateIdentifier

func (r ChangeExpiredRequest) RateIdentifier() string

RateIdentifier counts change-expired attempts per account, like sign-in: the route checks a password.

type ChangeRequest

type ChangeRequest struct {
	Current Secret `json:"current"`
	New     Secret `json:"new"`
	// RevokeOtherSessions is better-auth's flag of the same name. It is read
	// only under KeepSessionsOnChange, where true signs out the other
	// sessions; without that option every change signs them out, and false
	// changes nothing.
	RevokeOtherSessions *bool `json:"revokeOtherSessions,omitempty"`
}

ChangeRequest is the input of change. Current may be empty for an account that has no password yet, such as one created through a social provider.

type ForgotRequest

type ForgotRequest struct {
	Identifier string `json:"identifier"`
	// RedirectTo, optional, is the page the reset link opens instead of the
	// configured one (better-auth's redirectTo): a path of this site or a
	// URL on a trusted origin, given the token as the "token" query
	// parameter, the page then POSTs it to reset. Anything else is
	// ErrRedirectInvalid. Ignored under ResetByCode, which sends no link.
	RedirectTo string `json:"redirectTo,omitempty"`
}

ForgotRequest is the input of forgot.

func (ForgotRequest) RateIdentifier

func (r ForgotRequest) RateIdentifier() string

RateIdentifier counts reset requests per account.

type ForgotResult

type ForgotResult struct {
	Status string `json:"status"`
}

ForgotResult is the same for every input, so the answer never reveals whether an account exists.

type Hasher

type Hasher interface {
	Verifier
	// Hash returns the string to store for pw. An error fails the sign-up,
	// reset or change; it never stores anything.
	Hash(pw Secret) (string, error)
}

Hasher is a Verifier that can also make hashes: the algorithm new and re-hashed passwords are stored with. The default is Argon2id; WithHasher replaces it, for an HSM, a key-derivation service or an app's own parameters. Every hash a Hasher makes must be one it Recognizes.

type Option

type Option func(*Plugin)

Option configures the plugin; pass options to New.

func AcceptLegacyHashes

func AcceptLegacyHashes(v ...Verifier) Option

AcceptLegacyHashes verifies stored hashes in other formats, for a user base imported from another stack: Bcrypt() for Rails, Laravel and Spring, Scrypt(BetterAuthScrypt) for better-auth, DjangoPBKDF2() for Django, or any Verifier of your own. A legacy hash is checked at sign-in, change and verify, and replaced with the Hasher's hash at the next successful sign-in, so the old format disappears one user at a time without a reset for anyone. Verifiers are asked in the order given; the first that Recognizes a stored string decides.

func Argon2

func Argon2(params Argon2Params) Option

Argon2 sets the hash cost. Existing hashes with other parameters are replaced at the user's next successful sign-in.

func CheckBreached

func CheckBreached(c BreachChecker) Option

CheckBreached refuses new passwords the checker reports as breached. A checker error fails the sign-up, reset or change with that error; the hibp checker's FailOpen is the named way to prefer availability.

func ExpireAfter

func ExpireAfter(d time.Duration) Option

ExpireAfter makes a password expire d after it was set (sign-up, change, reset, SetPassword or ImportHash; the re-hash at sign-in does not reset the clock). A sign-in with the right, expired password is refused with ErrExpired, and the client sends the identifier, the old password and a new one to change-expired, which changes it and signs in. A wrong password is ErrInvalidCredentials as always, so the refusal reveals nothing to someone who does not know the password. Sessions that already exist are not ended when the password expires; expiry is checked at sign-in. A new password must differ from the expired one, and with History from the ones it remembers. NIST SP 800-63B advises against periodic expiry; set it only when a policy requires it.

func GuardCredentialStuffing

func GuardCredentialStuffing(pol StuffingPolicy) Option

GuardCredentialStuffing turns on the guard (off by default). Each failed password sign-in, for an account that exists or not, is counted twice in the core's rate store: under a keyed HMAC of the password (the key derived from auth.WithSecret, so the store never holds the password) and, with GlobalFailures, under one global key. A sign-in whose password is over its limit, or any sign-in while the global count is over its limit, is refused with the core's 429 auth.rate_limited before the password is checked, so the answer never tells a guesser whether a refused guess was right.

Risk, and why it is opt-in: everyone whose password is the sprayed one cannot sign in for the window, and the global rule refuses every user for as long as an attack keeps the failures up. Needs auth.WithSecret and a rate store that can read a count without adding to it (an auth.RatePeeker: the core's memory, database and storage stores, and contrib/redis; auth.New fails with auth.ErrRatePeekUnsupported otherwise), shared across instances.

func History

func History(n int) Option

History refuses a new password that matches the current one or any of the n-1 before it, so n is how many passwords a user cannot go back to, counting the one in use. It applies to change, reset, change-expired and SetPassword; sign-up and ImportHash have no earlier password. Earlier passwords are kept as hashes in auth_password_history, which the plugin creates on the first migrate after History is set. Each change checks up to n hashes, so n is capped at MaxHistory. Turning History off later stops the writes and leaves the table and its rows; drop the table if you want the hashes gone, because DeleteUser only clears it while History is on.

func KeepSessionsOnChange

func KeepSessionsOnChange() Option

KeepSessionsOnChange makes a password change leave the user's other sessions alive unless the request sets revokeOtherSessions to true, better-auth's default. By default a change signs out every other session and ChangeRequest.RevokeOtherSessions cannot turn that off: a client must not be able to weaken the response to a stolen account. The same risk as KeepSessionsOnReset applies.

func KeepSessionsOnReset

func KeepSessionsOnReset() Option

KeepSessionsOnReset leaves every session of the account alive after a reset, better-auth's default (revokeSessionsOnPasswordReset: false). By default a reset signs out every session, because a reset is so often the answer to a stolen account: under this option a thief who already holds a session keeps it after the owner resets. Open reset links are still spent and the other plugins still hear that the credential changed (two-factor forgets its trusted devices).

func MaxLength

func MaxLength(n int) Option

MaxLength sets the longest password accepted, in bytes. Sign-in refuses a longer one before hashing, so the limit also bounds the argon2 work an anonymous request can cause.

func MinLength

func MinLength(n int) Option

MinLength sets the shortest password accepted, in characters. It applies to new passwords only; an existing shorter one still signs in.

func Policy

func Policy(fn func(pw string) error) Option

Policy adds a rule every new password must pass, such as a company requirement. Length is the rule that matters most and is always checked first; a policy that fails returns its own *auth.Error or, for any other error, ErrWeak.

func RequireClasses

func RequireClasses(n int) Option

RequireClasses asks for characters from at least n of four classes: lowercase, uppercase, digits, and everything else. NIST advises length over such rules; this exists because some products must have them.

func ResetByCode

func ResetByCode() Option

ResetByCode makes forgot send a one-time code instead of a link, and replaces the reset operation with reset-code, which takes the identifier, the code and the new password. For a phone-first app, where a link in a text is awkward, or a flow that keeps the user on the same screen. The code goes out through the same recovery channel the link would (an address or a number the user verified), lives ResetTTL, and is spent by the core's attempt cap after a few wrong guesses. No base URL or ResetLinks is needed.

func ResetLinks(fn func(t auth.Token) string) Option

ResetLinks builds the reset URL. It must open a page of the app that POSTs the token with the new password: a link that performed the reset on GET would be spent by mail scanners that prefetch links. Without it the link is BaseURL + DefaultResetPage + "?token=…".

func ResetTTL

func ResetTTL(d time.Duration) Option

ResetTTL overrides DefaultResetTTL, how long a reset link works. Every other open link is spent when one is used, so a longer TTL does not widen what a leaked old mail can do after a reset.

func SignUpWithoutSession

func SignUpWithoutSession() Option

SignUpWithoutSession makes sign-up create the account and issue no session: the result carries the user and no token, and the client signs in as a separate step. For an app that wants an explicit first sign-in, or a sign-up form that is not the sign-in form. An identifier kind with RequireVerified() already behaves this way.

func WithHasher

func WithHasher(h Hasher) Option

WithHasher replaces argon2id as the algorithm new and re-hashed passwords are stored with: an HSM, a key-derivation service, or your own parameters behind the Hasher interface. Stored hashes the new Hasher does not recognize (yesterday's argon2id strings, say) keep verifying only if a verifier for them is listed in AcceptLegacyHashes; pass password.Argon2id{Params: …} there to keep the old ones working. With WithHasher, Argon2 has no effect.

func WithoutRecovery

func WithoutRecovery() Option

WithoutRecovery is the named way to run without password reset. The safe configuration is the default; this one has a name you must type.

func WithoutSignUp

func WithoutSignUp() Option

WithoutSignUp removes the sign-up operation: accounts are created some other way (an invite, an admin, a social sign-in) and only sign in with a password here. The route does not exist, so a client gets 404 rather than a refusal that hints at a hidden feature.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the password sign-in method. Build it with New and pass it to auth.WithPlugins; its Go methods and its HTTP routes run the same operations.

func New

func New(opts ...Option) *Plugin

New builds the plugin. The safe configuration is the default: recovery on, OWASP argon2 cost, NIST lengths.

func (*Plugin) Change

func (p *Plugin) Change(ctx context.Context, in ChangeRequest) error

Change sets a new password for the signed-in user.

func (*Plugin) ChangeExpired

func (p *Plugin) ChangeExpired(ctx context.Context, in ChangeExpiredRequest) (SignInResult, error)

ChangeExpired changes an expired password and starts a session, the way in after a sign-in answered ErrExpired. It runs every rule of a new password, refuses the expired one itself (ErrReused), and signs out every other session and spends every open reset link, as a reset does.

func (*Plugin) Forgot

func (p *Plugin) Forgot(ctx context.Context, identifier string) (ForgotResult, error)

Forgot sends a reset link when the account exists, and answers the same either way.

func (*Plugin) ForgotWith

func (p *Plugin) ForgotWith(ctx context.Context, in ForgotRequest) (ForgotResult, error)

ForgotWith is Forgot with the whole request, such as a RedirectTo page the reset link opens.

func (*Plugin) HasPassword

func (p *Plugin) HasPassword(ctx context.Context, userID string) (bool, error)

HasPassword reports whether the user has a password, such as to show "set a password" instead of "change password" to a user who signed up through a social provider. A database failure is an error, never a false.

func (*Plugin) Hasher

func (p *Plugin) Hasher() Hasher

Hasher returns the hasher the plugin makes and checks hashes with (the WithHasher one, else Argon2id with the plugin's parameters): better-auth's ctx.context.password.hash and verify, for a hook or plugin that must hash a secret under the same policy, never a weaker one of its own.

func (*Plugin) ImportHash

func (p *Plugin) ImportHash(ctx context.Context, userID string, hash string) error

ImportHash stores a hash from another system as userID's password, from Go only: the user keeps signing in with their old password, which the Hasher or an AcceptLegacyHashes verifier checks and the plugin re-hashes at that sign-in. It is how an import writes auth_password without touching the table by hand. The string must be in a format the configuration verifies (ErrHashUnrecognized otherwise), the user must exist (auth.ErrUserNotFound) and must have no password yet (ErrHasPassword). No session ends, nothing is sent, and no rule of new passwords applies: the hash is not a password. It runs in one transaction, or joins the one in ctx.

func (*Plugin) Init

func (p *Plugin) Init(a *auth.Auth) error

Init computes the dummy hash with the configured Hasher and fills the default reset link. The dummy comes from the Hasher, not from argon2 directly, so an unknown account costs exactly what a wrong password on a current hash costs whatever the algorithm is.

func (*Plugin) Name

func (p *Plugin) Name() string

Name is PluginName, the plugin's permanent identity in the migration ledger and in column ownership.

func (*Plugin) Operations

func (p *Plugin) Operations() []auth.Operation

Operations lists every operation; sign-up exists unless WithoutSignUp, and forgot and reset (or reset-code under ResetByCode) only with recovery.

func (*Plugin) OptionErr

func (p *Plugin) OptionErr() error

OptionErr is every error the options recorded, such as auth.ErrNilOption naming an option given nil; auth.New refuses to start while it is not nil (auth.OptionChecker).

func (*Plugin) RateLimits

func (p *Plugin) RateLimits() []auth.RateRule

RateLimits bounds guessing and mail; the forgot rule exists only with recovery, because the operation does.

func (*Plugin) Reauthenticate

func (p *Plugin) Reauthenticate(ctx context.Context, password Secret) (auth.Reauthentication, error)

Reauthenticate checks the signed-in user's password and makes their current session fresh again for FreshSignIn operations, without a new session or token. When the user's second factor is due (asked as at a password sign-in), the session is not fresh yet: the result carries a ticket for the second factor's re-authentication operation. A wrong password is ErrInvalidCredentials and leaves the session as it was.

func (*Plugin) Reset

func (p *Plugin) Reset(ctx context.Context, in ResetRequest) error

Reset spends a reset token and sets a new password.

func (*Plugin) ResetWithCode

func (p *Plugin) ResetWithCode(ctx context.Context, in ResetCodeRequest) error

ResetWithCode spends a reset code sent by Forgot under ResetByCode and sets a new password. An unknown identifier and a wrong, expired or spent code are all auth.ErrCodeInvalid.

func (*Plugin) SetPassword

func (p *Plugin) SetPassword(ctx context.Context, userID string, password Secret) error

SetPassword sets or replaces userID's password from server code, with no current password and no session: an admin resetting a locked-out user, an import, a support flow. It has no route, because any client-reachable form of it is account takeover. The new password passes the same rules as sign-up, every open reset link is spent, and every session of the user is signed out, since the person holding them may not be the one the password is being set for. It runs in one transaction, or joins the one in ctx.

func (*Plugin) SignIn

func (p *Plugin) SignIn(ctx context.Context, in SignInRequest) (SignInResult, error)

SignIn checks a password and starts a session.

func (*Plugin) SignUp

func (p *Plugin) SignUp(ctx context.Context, in SignUpRequest) (SignInResult, error)

SignUp creates an account with a password.

func (*Plugin) SignsIn

func (p *Plugin) SignsIn() bool

SignsIn reports that a password is a way in, so auth.New counts the plugin as a sign-in method and the core refuses to remove a user's last one.

func (*Plugin) Tables

func (p *Plugin) Tables() []sqlb.Table

Tables declares the plugin's tables, so auth.New can refuse another plugin claiming the same name or alias; the history table only under History.

func (*Plugin) TypedMigrations

func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)

TypedMigrations creates auth_password for engine, and refuses an engine the core does not know. The table is named through the core, which knows what the app called it (auth.RenameTable, auth.WithTablePrefix).

func (*Plugin) UserTables

func (p *Plugin) UserTables() []auth.UserTable

UserTables declares that auth_password rows (and, under History, auth_password_history rows) belong to the user in user_id, so DeleteUser removes them (no foreign key does, ADR-0002).

func (*Plugin) Validate

func (p *Plugin) Validate(c *auth.Config) error

Validate refuses a configuration where nobody could sign in or recover.

func (*Plugin) Verify

func (p *Plugin) Verify(ctx context.Context, password Secret) error

Verify checks the signed-in user's password without changing anything, for an app step that wants the password typed again (showing a recovery key, deleting the account). Wrong, or no password on the account, is ErrInvalidCredentials; a user without a password costs the same argon2 work as a wrong one. It is rate limited per IP like sign-in.

type ReauthenticateRequest

type ReauthenticateRequest struct {
	Password Secret `json:"password"`
}

ReauthenticateRequest is the input of reauthenticate: the signed-in user's own password.

type Rehasher

type Rehasher interface {
	NeedsRehash(encoded string) bool
}

Rehasher is optionally implemented by a Hasher whose own format carries parameters, so it can tell that a stored hash of its own format was made with other parameters than it uses now. The plugin replaces such a hash at the user's next successful sign-in. A Hasher without it never re-hashes its own format; hashes of other formats (AcceptLegacyHashes) are always re-hashed after a match.

type ResetCodeRequest

type ResetCodeRequest struct {
	Identifier string `json:"identifier"`
	Code       string `json:"code"`
	Password   Secret `json:"password"`
}

ResetCodeRequest is the input of reset-code (ResetByCode): the identifier the code was sent for, the code, and the new password.

func (ResetCodeRequest) RateIdentifier

func (r ResetCodeRequest) RateIdentifier() string

RateIdentifier counts reset-code attempts per account.

type ResetRequest

type ResetRequest struct {
	Token    auth.WireToken `json:"token"`
	Password Secret         `json:"password"`
}

ResetRequest is the input of reset: the token from the reset link and the new password.

type ScryptParams

type ScryptParams struct {
	// N is the CPU and memory cost, a power of two.
	N int
	// R is the block size.
	R int
	// P is the parallelization factor.
	P int
	// KeyLen is the derived key length in bytes.
	KeyLen int
}

ScryptParams are the scrypt cost parameters and the derived key length of a stored "salt:key" hash. The format carries no parameters, so the verifier must be told the ones the old stack used.

type Secret

type Secret = auth.Token

Secret is a password as it travels through the plugin. It is auth.Token's redacted string: every formatting path, slog and JSON encoding print "[redacted]", so a logged request struct never shows a password. Read it only with Reveal. JSON decoding into it works like a plain string.

type SignInRequest

type SignInRequest struct {
	Identifier string `json:"identifier"`
	Password   Secret `json:"password"`
	// Remember is the "remember me" box. Nil means remember, the usual
	// default; false makes a transient session that ends with the browser.
	Remember *bool `json:"remember,omitempty"`
}

SignInRequest is the input of sign-in.

func (SignInRequest) RateIdentifier

func (r SignInRequest) RateIdentifier() string

RateIdentifier counts sign-in attempts per account.

type SignInResult

type SignInResult struct {
	User auth.UserView `json:"user"`
	// VerificationRequired is true when sign-up created the account but the
	// identifier must be verified before the first sign-in; no session was
	// issued.
	VerificationRequired bool `json:"verificationRequired"`
	// SecondFactorRequired is true when the session is pending: the client
	// must pass the user's second factor before it signs anyone in.
	SecondFactorRequired bool       `json:"secondFactorRequired"`
	Token                auth.Token `json:"-"`
}

SignInResult is the output of sign-up and sign-in.

Token is the raw session token for Go callers and bearer clients. It is never encoded into a JSON body: a cookie client must not see it in a response a script can read, and a bearer transport adds it explicitly.

type SignUpRequest

type SignUpRequest struct {
	Identifier string `json:"identifier"`
	Password   Secret `json:"password"`
	// Name is the display name; optional, an account without one has "".
	Name string `json:"name,omitempty"`
	// Image is the user's picture URL, optional; it can also be set later
	// through the core profile operation.
	Image string `json:"image,omitempty"`
	// Remember is the "remember me" box, as on SignInRequest: nil or true
	// keeps the session past the browser, false makes it transient. Ignored
	// when no session is issued.
	Remember *bool `json:"remember,omitempty"`
	// Username, optional, claims a handle for the new account in the same
	// transaction, through the registered UsernameClaimer (the username
	// plugin's ClaimWithDisplay), so an email-plus-handle sign-up is one
	// call: a taken or reserved handle fails the whole sign-up and no half
	// account is left. ErrNoUsernames when no UsernameClaimer is wired.
	Username string `json:"username,omitempty"`
	// DisplayUsername, optional, is the form of Username to show
	// (better-auth's displayUsername), checked by the claimer's display
	// rules (the username plugin's DisplayForm). Ignored without Username.
	DisplayUsername string `json:"displayUsername,omitempty"`
	// Fields sets other auth_user columns of the new account, better-auth's
	// user.additionalFields: only the columns auth.WithSignUpFields names,
	// each of its declared JSON type. Anything else is auth.ErrFieldInvalid
	// (400) before any row is written. The values go through the core's
	// column writer, in the sign-up transaction. The same columns may also
	// be sent as top-level keys, as better-auth does; one sent both ways is
	// auth.ErrFieldInvalid.
	Fields auth.Fields `json:"fields,omitempty"`
	// contains filtered or unexported fields
}

SignUpRequest is the input of sign-up.

func (SignUpRequest) RateIdentifier

func (r SignUpRequest) RateIdentifier() string

RateIdentifier counts sign-ups per identifier, so one address cannot be flooded with sign-up attempts from many IPs.

func (*SignUpRequest) UnmarshalJSON

func (r *SignUpRequest) UnmarshalJSON(data []byte) error

UnmarshalJSON keeps the top-level keys that are not the request's own, so sign-up fields may sit beside identifier and password; auth.Auth.MergeFields then refuses any that is not a declared column.

type StuffingPolicy

type StuffingPolicy struct {
	// PasswordFailures is how many failed sign-ins with one password, across
	// every account and every IP, refuse that password for the rest of the
	// Window: password spraying, one common password tried against many
	// accounts. 0 is DefaultStuffingPasswordFailures.
	PasswordFailures int
	// GlobalFailures is how many failed sign-ins across every account and
	// IP, per Window, refuse every password sign-in for the rest of it: a
	// stuffing run with leaked pairs, where each pair is tried once. 0 turns
	// the global rule off, the default, because it refuses legitimate users
	// too while an attack lasts.
	GlobalFailures int
	// Window is how long failures are counted and a refusal lasts. 0 is
	// DefaultStuffingWindow.
	Window time.Duration
	// LogOnly counts and logs a warning when a threshold is crossed but
	// refuses nothing: Sentinel's "log" action, for tuning the thresholds
	// against real traffic before turning the refusal on.
	LogOnly bool
}

StuffingPolicy configures GuardCredentialStuffing, better-auth Sentinel's credentialStuffing for an embedded library: it spots password guessing spread over many IPs and accounts, which the per-IP and per-identifier rate rules cannot see, because each address and each account only sees a few tries.

type UsernameClaimer

type UsernameClaimer interface {
	auth.Plugin
	ClaimWithDisplay(ctx context.Context, userID, handle, display string) error
}

UsernameClaimer is what sign-up needs to claim SignUpRequest.Username for the new account: the method `*username.Plugin` already has. Found through auth.PluginOf when a sign-up carries a username, so this plugin imports no identifier plugin and an app's own handle scheme can take the claim. It is called inside the sign-up transaction: an error fails the whole sign-up. display is SignUpRequest.DisplayUsername, "" when none was sent.

type Verifier

type Verifier interface {
	// Recognizes reports whether encoded is in this verifier's format. It
	// must be cheap and must not run the hash.
	Recognizes(encoded string) bool
	// Verify reports whether pw matches encoded.
	Verify(pw Secret, encoded string) (bool, error)
}

Verifier checks passwords against stored hashes of one format. The plugin asks each configured verifier whether it Recognizes a stored string before asking it to Verify, so verifiers for different formats can coexist and a row is never checked by the wrong algorithm.

Verify must compare in constant time and must report a corrupt or hostile stored string as an error, never as a mismatch: a stored string that no verifier recognizes is a bug or an attack, and sign-in reports it as a server error rather than a wrong password.

func Bcrypt

func Bcrypt() Verifier

Bcrypt returns a Verifier for bcrypt hashes, the format of Rails (has_secure_password), Laravel, Spring Security and many others. It only verifies; new and re-hashed passwords use the plugin's Hasher, so an imported user base migrates to argon2id one sign-in at a time.

bcrypt reads at most 72 bytes of a password; a longer imported password verifies on its first 72 bytes, as it did on the old stack, and is then re-hashed in full.

func DjangoPBKDF2

func DjangoPBKDF2() Verifier

DjangoPBKDF2 returns a Verifier for Django's default password format, PBKDF2-HMAC-SHA256 as "pbkdf2_sha256$iterations$salt$key". It only verifies; matches are re-hashed with the plugin's Hasher. Django's other hashers (bcrypt_sha256, argon2, scrypt) are not this format: bcrypt_sha256 hashes a SHA-256 digest, so Bcrypt cannot verify it either.

func Scrypt

func Scrypt(params ScryptParams) Verifier

Scrypt returns a Verifier for scrypt hashes stored as "hexsalt:hexkey" with the given parameters, the form better-auth and Node's scrypt examples use. It only verifies; matches are re-hashed with the plugin's Hasher.

better-auth normalizes the password to NFKC before hashing. The standard library has no NFKC, so a password is checked as typed; the rare password whose NFKC form differs from what the user types verifies only after a reset.

type VerifyRequest

type VerifyRequest struct {
	Password Secret `json:"password"`
}

VerifyRequest is the input of verify: the signed-in user's own password.

Directories

Path Synopsis
Package hibp checks passwords against the Have I Been Pwned range API, for password.CheckBreached.
Package hibp checks passwords against the Have I Been Pwned range API, for password.CheckBreached.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL