Documentation
¶
Overview ¶
Package login is the first step of signing in (ADR 0020), shared by every surface that signs a person in with an address and a password: it normalizes the address once, consults the throttle (ADR 0025), looks the account up and checks the password against a decoy hash when there is none, applies the verification gate (ADR 0030), counts a failure and mints the second-factor challenge. It returns one Result and holds no database client: an adapter supplies the Store, the throttle's counter, the challenge and, after an Authenticated result, the session.
The order is the module's contract, and is asserted without HTTP:
- the throttle is consulted first, so a correct password is refused while the address's delay runs (else guessing until one attempt succeeds would skip it);
- the password is always hashed, against a decoy for an unknown address, so the answer's time does not say which addresses exist;
- the gate precedes the failure count and the challenge, and counts nothing;
- an error from counting a failure refuses the attempt (a broken counter never lets guessing through);
- the password step resets the counter only when no second factor follows: the second step resets it, else knowing the password would buy a fresh round of code guesses on every sign-in;
- a Passkey login (ADR 0052) finds the account by the credential, consults that account's throttle before verifying, and counts no failure: it replaces both steps.
Index ¶
- Variables
- type Account
- type Config
- type Delay
- type Module
- func (m *Module[P]) CountFailure(ctx context.Context, address string) error
- func (m *Module[P]) Passkey(ctx context.Context, a PasskeyAssertion) (PasskeyResult[P], error)
- func (m *Module[P]) Password(ctx context.Context, address, password string) (Result[P], error)
- func (m *Module[P]) Prove(ctx context.Context, address, passwordHash, password string) (ProofResult, error)
- func (m *Module[P]) SecondFactor(ctx context.Context, challenge, code string) (SecondResult[P], error)
- func (m *Module[P]) Throttled(ctx context.Context, address string) (delay Delay, throttled bool, err error)
- type Outcome
- type PasskeyAssertion
- type PasskeyOutcome
- type PasskeyResult
- type Pending
- type Proof
- type ProofResult
- type Result
- type SecondOutcome
- type SecondResult
- type Store
- type Throttle
Constants ¶
This section is empty.
Variables ¶
var ( // ErrChallengeInvalid is what an adapter's Parse and Verify return for a challenge // that no longer stands (forged, expired, spent, moved, factor gone). ErrChallengeInvalid = errors.New("login: challenge invalid") // ErrInvalidCode is what an adapter's Verify returns for a wrong code. ErrInvalidCode = errors.New("login: invalid code") )
var ErrPasskeyInvalid = errors.New("login: passkey response invalid")
ErrPasskeyInvalid is what an adapter's PasskeyVerify returns for a response that does not verify: a bad signature, no user verification, a spent or expired challenge.
var ErrPasskeyUnknown = errors.New("login: passkey unknown")
ErrPasskeyUnknown is what an adapter's PasskeyFind returns for a response that names no credential (malformed, or a credential nobody registered).
Functions ¶
This section is empty.
Types ¶
type Account ¶
type Account[P any] struct { Principal P // PasswordHash is empty for an account without a password. PasswordHash string // SecondFactor reports that a second step follows a right password. SecondFactor bool }
Account is what a Store knows about an address.
type Config ¶
type Config[P any] struct { Throttle Throttle Store Store[P] // Gate, when set, reports that an account may not sign in yet (an Unverified User, // ADR 0030). It is consulted for every known account, before the password result is // acted on. Gate func(P) bool // Challenge mints the second step's token for an account with a second factor. Challenge func(ctx context.Context, principal P) (string, error) // Parse turns a challenge token into its Pending, or returns ErrChallengeInvalid. Parse func(ctx context.Context, token string) (Pending[P], error) // Verify checks a code for a Pending under the adapter's lock (a TOTP code, or a // recovery code, which it spends). It returns ErrInvalidCode for a wrong code and // ErrChallengeInvalid when the challenge stopped standing (the factor is gone, or a // racing second step moved it first). Verify func(ctx context.Context, p Pending[P], code string) error // PasskeyFind names the account (Pending.Principal and the throttle key Pending.Address) // a Passkey response belongs to, by its credential id, or returns ErrPasskeyUnknown. It // verifies nothing. Left nil, the surface has no Passkey login. PasskeyFind func(ctx context.Context, a PasskeyAssertion) (Pending[P], error) // PasskeyVerify checks the response for the account PasskeyFind named: signature, // challenge, user verification, single use. It returns ErrPasskeyInvalid when it does not // hold. PasskeyVerify func(ctx context.Context, p Pending[P], a PasskeyAssertion) error }
Config wires the module to one surface.
type Delay ¶
Delay is the throttle's refusal: how long the address must still wait, the failure threshold and the throttle's clock. A throttled result of any step carries it.
type Module ¶
type Module[P any] struct { // contains filtered or unexported fields }
Module is the password step.
func (*Module[P]) CountFailure ¶
CountFailure counts one wrong second-step code of an already proven password on the address's budget, the same budget a wrong password feeds (a profile action that asks for the password and then a code, such as User deletion).
func (*Module[P]) Passkey ¶
func (m *Module[P]) Passkey(ctx context.Context, a PasskeyAssertion) (PasskeyResult[P], error)
Passkey runs the Passkey login for a response as the browser sent it (ADR 0052). The account is found by the credential, never by an address. Order: an unknown credential is refused without counting; the throttle of the account found is consulted before the response is verified, so a correct response is refused while the delay runs; the verification gate refuses an account that may not sign in yet, with the same answer and no count; a response that does not verify is the same refusal and counts nothing (without the private key a guess cannot succeed); a verified response resets the counter, and a reset that fails is logged, not fatal. A Passkey replaces the password and the Second factor, so no second step follows.
func (*Module[P]) Prove ¶
func (m *Module[P]) Prove(ctx context.Context, address, passwordHash, password string) (ProofResult, error)
Prove is the one path every password-gated action takes (second-factor enrol, disable and regenerate, a profile password or email change): the same throttle as the first step (consulted first, so a right password is refused while the delay runs), the same decoy for an account without a password, and a failure counted on the same budget as a login failure. The caller holds the account, so it passes its address and hash.
func (*Module[P]) SecondFactor ¶
func (m *Module[P]) SecondFactor(ctx context.Context, challenge, code string) (SecondResult[P], error)
SecondFactor runs the second step for a challenge and a code as typed. Order: a bad challenge is refused without counting; the throttle is consulted before the code, so a correct code is refused while the delay runs; a wrong code counts against the same budget as a wrong password (a counting error refuses the attempt); a committed second factor resets the counter, and a reset that fails is logged, not fatal.
func (*Module[P]) Throttled ¶
func (m *Module[P]) Throttled(ctx context.Context, address string) (delay Delay, throttled bool, err error)
Throttled consults the address's delay without proving anything, for a profile action whose proof is not the password (a mailed link) but which then takes a second-step code the caller counts with CountFailure: while the delay runs a code is not even checked, so the code cannot be guessed past the budget. It is the same throttle Prove consults first.
type Outcome ¶
type Outcome int
Outcome is what the password step decided.
const ( // Throttled: the address's delay is running. Result.Wait, Limit and Now say how long. Throttled Outcome = iota + 1 // InvalidCredentials: an unknown address, a wrong password or an account without a // password, indistinguishably. The failure is counted. InvalidCredentials // Gated: the verification gate refused the account. Nothing is counted and no // challenge is minted. Gated // SecondFactor: the password is right and a second step follows. Result.Challenge // is its token. SecondFactor // Authenticated: the password is right and nothing else is needed. The counter is // reset; the adapter issues the session. Authenticated )
type PasskeyAssertion ¶
PasskeyAssertion is what the browser sent to finish a Passkey login: the ceremony cookie that carries the challenge and the authenticator's response.
type PasskeyOutcome ¶
type PasskeyOutcome int
PasskeyOutcome is what the Passkey step decided (ADR 0052).
const ( // PasskeyThrottled: the delay of the User the credential names is running. PasskeyThrottled PasskeyOutcome = iota + 1 // PasskeyFailed is every other refusal, indistinguishably: an unknown credential, a // response that does not verify (no user verification included), a spent or expired // challenge, an account the verification gate refuses. Nothing is counted. PasskeyFailed // PasskeyAuthenticated: the credential is proven and the person verified. The counter // is reset; the adapter issues the session. No Second factor follows. PasskeyAuthenticated )
type PasskeyResult ¶
type PasskeyResult[P any] struct { Outcome PasskeyOutcome Principal P // Delay is set for a PasskeyThrottled result. Delay }
PasskeyResult is the one answer of the Passkey step. Principal is set for PasskeyAuthenticated.
type Pending ¶
type Pending[P any] struct { Principal P // Address is the account's throttle key (normalized again by the module). Address string // Method names how the first step proved the person; ProviderLoginAt is the time of // a provider login the challenge follows (zero for a password login). Method string ProviderLoginAt time.Time // Binding is the adapter's own single-use proof, handed back to Verify. Binding string }
Pending is a challenge the adapter parsed: the account it was minted for, the address that counts the second step's failures and what the session needs to know.
type Proof ¶
type Proof int
Proof is what proving the password of a signed-in account decided.
const ( // ProofThrottled: the address's delay is running. ProofResult.Wait, Limit and Now say how long. ProofThrottled Proof = iota + 1 // ProofWrong: the password does not match (or the account has none). The failure is // counted on the same budget as a login failure. ProofWrong // ProofHolds: the password is right. ProofHolds )
type ProofResult ¶
ProofResult is the one answer of Prove.
type Result ¶
type Result[P any] struct { Outcome Outcome Principal P // Delay is set for a Throttled result. Delay // Challenge is the second step's token, for a SecondFactor result. Challenge string }
Result is the one answer of the password step. Principal is set for Gated, SecondFactor and Authenticated.
type SecondOutcome ¶
type SecondOutcome int
SecondOutcome is what the second step decided.
const ( // SecondThrottled: the address's delay is running. SecondResult.Wait, Limit and Now // say how long. SecondThrottled SecondOutcome = iota + 1 // SecondInvalidChallenge: the challenge is forged, expired, spent, moved or its // account is gone. It has no trusted address, so nothing is counted. SecondInvalidChallenge // SecondInvalidCode: the code is wrong. The failure is counted against the same // budget as a wrong password. SecondInvalidCode // SecondAuthenticated: the code is right. The counter is reset; the adapter issues // the session. SecondAuthenticated )
type SecondResult ¶
type SecondResult[P any] struct { Outcome SecondOutcome Principal P // Delay is set for a SecondThrottled result. Delay Method string ProviderLoginAt time.Time }
SecondResult is the one answer of the second step. Principal, Method and ProviderLoginAt (the last two come from the challenge) are set for SecondAuthenticated.
type Store ¶
type Store[P any] interface { Lookup(ctx context.Context, address string) (acc Account[P], ok bool, err error) }
Store looks an account up by its normalized address; ok is false when there is none.
type Throttle ¶
type Throttle interface {
Delay(ctx context.Context, address string) (time.Duration, error)
RecordFailure(ctx context.Context, address string) error
RecordSuccess(ctx context.Context, address string) error
Limit() int
Now() time.Time
}
Throttle is the failure counter of one login surface (a User's, or a Staff member's: each has its own, so one cannot lock the other). Addresses reach it normalized.