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
- Variables
- type Argon2Params
- type Argon2id
- type BreachChecker
- type BreachMessenger
- type ChangeExpiredRequest
- type ChangeRequest
- type ForgotRequest
- type ForgotResult
- type Hasher
- type Option
- func AcceptLegacyHashes(v ...Verifier) Option
- func Argon2(params Argon2Params) Option
- func CheckBreached(c BreachChecker) Option
- func ExpireAfter(d time.Duration) Option
- func GuardCredentialStuffing(pol StuffingPolicy) Option
- func History(n int) Option
- func KeepSessionsOnChange() Option
- func KeepSessionsOnReset() Option
- func MaxLength(n int) Option
- func MinLength(n int) Option
- func Policy(fn func(pw string) error) Option
- func RequireClasses(n int) Option
- func ResetByCode() Option
- func ResetLinks(fn func(t auth.Token) string) Option
- func ResetTTL(d time.Duration) Option
- func SignUpWithoutSession() Option
- func WithHasher(h Hasher) Option
- func WithoutRecovery() Option
- func WithoutSignUp() Option
- type Plugin
- func (p *Plugin) Change(ctx context.Context, in ChangeRequest) error
- func (p *Plugin) ChangeExpired(ctx context.Context, in ChangeExpiredRequest) (SignInResult, error)
- func (p *Plugin) Forgot(ctx context.Context, identifier string) (ForgotResult, error)
- func (p *Plugin) ForgotWith(ctx context.Context, in ForgotRequest) (ForgotResult, error)
- func (p *Plugin) HasPassword(ctx context.Context, userID string) (bool, error)
- func (p *Plugin) Hasher() Hasher
- func (p *Plugin) ImportHash(ctx context.Context, userID string, hash string) error
- func (p *Plugin) Init(a *auth.Auth) error
- func (p *Plugin) Name() string
- func (p *Plugin) Operations() []auth.Operation
- func (p *Plugin) OptionErr() error
- func (p *Plugin) RateLimits() []auth.RateRule
- func (p *Plugin) Reauthenticate(ctx context.Context, password Secret) (auth.Reauthentication, error)
- func (p *Plugin) Reset(ctx context.Context, in ResetRequest) error
- func (p *Plugin) ResetWithCode(ctx context.Context, in ResetCodeRequest) error
- func (p *Plugin) SetPassword(ctx context.Context, userID string, password Secret) error
- func (p *Plugin) SignIn(ctx context.Context, in SignInRequest) (SignInResult, error)
- func (p *Plugin) SignUp(ctx context.Context, in SignUpRequest) (SignInResult, error)
- func (p *Plugin) SignsIn() bool
- func (p *Plugin) Tables() []sqlb.Table
- func (p *Plugin) TypedMigrations(engine string, t auth.SQLTypes) ([]auth.Migration, error)
- func (p *Plugin) UserTables() []auth.UserTable
- func (p *Plugin) Validate(c *auth.Config) error
- func (p *Plugin) Verify(ctx context.Context, password Secret) error
- type ReauthenticateRequest
- type Rehasher
- type ResetCodeRequest
- type ResetRequest
- type ScryptParams
- type Secret
- type SignInRequest
- type SignInResult
- type SignUpRequest
- type StuffingPolicy
- type UsernameClaimer
- type Verifier
- type VerifyRequest
Constants ¶
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.
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.
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.
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.
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.
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.
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.
const ForgotStatus = "check_your_inbox"
ForgotStatus is the one answer forgot ever gives.
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.
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.
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.
const PurposeResetToken auth.Purpose = "password.reset"
PurposeResetToken is the verification purpose of a reset link. Its Identifier is the user id.
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 ¶
var ( TablePassword = sqlb.Table{Name: TableNamePassword, Alias: TableAliasPassword} UserID = sqlb.Col[string](TablePassword, ColUserID) Hash = sqlb.Col[string](TablePassword, ColHash) UpdatedAt = sqlb.Col[time.Time](TablePassword, ColUpdatedAt) )
TablePassword and its typed columns, for an app that reads the table (reads are open; only the plugin writes it).
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.
var ( TablePasswordHistory = sqlb.Table{Name: TableNamePasswordHistory, Alias: TableAliasPasswordHistory} HistoryID = sqlb.Col[string](TablePasswordHistory, ColHistoryID) HistoryUserID = sqlb.Col[string](TablePasswordHistory, ColHistoryUserID) HistoryHash = sqlb.Col[string](TablePasswordHistory, ColHistoryHash) HistorySeq = sqlb.Col[int64](TablePasswordHistory, ColHistorySeq) HistoryCreatedAt = sqlb.Col[time.Time](TablePasswordHistory, ColHistoryCreatedAt) )
TablePasswordHistory and its typed columns, for an app that reads the table (reads are open; only the plugin writes it).
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") )
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.
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.
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 ¶
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 ¶
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 ¶
Recognizes reports whether encoded is an argon2id PHC string.
type BreachChecker ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
MinLength sets the shortest password accepted, in characters. It applies to new passwords only; an existing shorter one still signs in.
func Policy ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Name is PluginName, the plugin's permanent identity in the migration ledger and in column ownership.
func (*Plugin) Operations ¶
Operations lists every operation; sign-up exists unless WithoutSignUp, and forgot and reset (or reset-code under ResetByCode) only with recovery.
func (*Plugin) OptionErr ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) Verify ¶
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 ¶
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 ¶
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 ¶
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.