Documentation
¶
Index ¶
- Constants
- Variables
- func HashPassword(password string) (string, error)
- func LDAPTest(cfg store.LDAPConfig) (int, error)
- func MapsRoles(cfg store.LDAPConfig) bool
- func MatchTOTP(code, secret string) (counter int64, ok bool)
- func RolesForGroups(cfg store.LDAPConfig, groups []string) []int64
- func SectionsForGroups(cfg store.LDAPConfig, groups []string) []string
- func StepUpKey(userID int64, sessionID string) string
- func ValidateTOTP(code, secret string) bool
- func VerifyPassword(password, encoded string) (bool, error)
- func WithClaims(ctx context.Context, c *Claims) context.Context
- type Claims
- type Enrollment
- type Issued
- type LDAPResult
- type LoginLimiter
- type LoginResult
- type Middleware
- type PasswordlessError
- type RelyingParty
- type Service
- func (s *Service) BeginPasskeyLogin(ctx context.Context, rp RelyingParty, challengeToken string) (*protocol.CredentialAssertion, error)
- func (s *Service) BeginPasskeyRegistration(ctx context.Context, rp RelyingParty, userID int64, stepUp bool) (*protocol.CredentialCreation, error)
- func (s *Service) BeginPasswordlessLogin(rlKey string, rp RelyingParty) (*protocol.CredentialAssertion, string, error)
- func (s *Service) BeginTOTPEnrollment(ctx context.Context, userID int64, stepUp bool) (*Enrollment, error)
- func (s *Service) ChallengeUsername(challengeToken string) string
- func (s *Service) ConfirmTOTPEnrollment(ctx context.Context, userID int64, code, name string) error
- func (s *Service) CreateAccount(ctx context.Context, username, password, role string, readOnly bool, ...) (*store.User, error)
- func (s *Service) FinishPasskeyLogin(ctx context.Context, rp RelyingParty, rlKey, challengeToken string, ...) (*LoginResult, error)
- func (s *Service) FinishPasskeyRegistration(ctx context.Context, rp RelyingParty, userID int64, name string, ...) error
- func (s *Service) FinishPasswordlessLogin(ctx context.Context, rp RelyingParty, rlKey, ceremonyID string, ...) (*LoginResult, error)
- func (s *Service) HasPasskeys(ctx context.Context, userID int64) (bool, error)
- func (s *Service) ListFactors(ctx context.Context, userID int64) ([]store.AuthFactor, error)
- func (s *Service) Login(ctx context.Context, rlKey, username, password string, exemptMFA bool, ...) (*LoginResult, error)
- func (s *Service) NeedsSetup(ctx context.Context) (bool, error)
- func (s *Service) RemoveFactor(ctx context.Context, userID, factorID int64) error
- func (s *Service) SetPassword(ctx context.Context, userID int64, password string) error
- func (s *Service) Setup(ctx context.Context, username, password string) (*store.User, error)
- func (s *Service) VerifyMFA(ctx context.Context, rlKey, challengeToken, code string, info SessionInfo) (*LoginResult, error)
- func (s *Service) VerifyUserPassword(ctx context.Context, rlKey string, u *store.User, password string) error
- type SessionEpochSource
- type SessionInfo
- type TokenKind
- type TokenManager
Constants ¶
const MinPasswordLength = 10
MinPasswordLength is the floor for every path that sets one — setup, account creation, a change from the UI, and the offline reset. Named so the offline path cannot quietly disagree with the online one.
const SessionCookie = "dc_session"
SessionCookie is the name of the httpOnly cookie carrying the session JWT.
const TOTPIssuer = "Docker Commander"
TOTPIssuer is the label shown in authenticator apps (Google Authenticator, Authy, 1Password, …) next to the account.
Variables ¶
var ( ErrSetupDone = errors.New("auth: setup already completed") ErrInvalidCreds = errors.New("auth: invalid credentials") ErrRateLimited = errors.New("auth: too many attempts, try again later") ErrMFARequired = errors.New("auth: 2fa code required") ErrInvalidMFACode = errors.New("auth: invalid 2fa code") ErrWeakPassword = fmt.Errorf("auth: password must be at least %d characters", MinPasswordLength) ErrInvalidUsername = errors.New("auth: username must be 3-32 characters") )
Common authentication errors surfaced to the API layer.
var ErrClonedAuthenticator = errors.New("auth: this passkey's counter went backwards")
ErrClonedAuthenticator means the signature counter went backwards — the sign of a credential that exists in two places.
var ErrEnrollmentStale = errors.New("auth: this account is protected now — start again and confirm with your password")
ErrEnrollmentStale is returned when a pending enrolment was started before the account had any second factor and so was never authorised with the password, but the account has gained one since. Starting over asks for the password.
var ErrInvalidHash = errors.New("auth: invalid password hash format")
ErrInvalidHash is returned when an encoded hash cannot be parsed.
var ErrLastFactor = store.ErrLastFactor
ErrLastFactor is returned when removing a factor would leave the account with no second factor at all.
var ErrNoPasskeys = errors.New("auth: this account has no passkeys")
ErrNoPasskeys means the account has none paired.
ErrPasskeyUnavailable means this request could not be a passkey ceremony: the browser will not touch WebAuthn outside a secure context, so neither will we.
var ErrPasswordlessNotAllowed = errors.New("auth: this account must sign in with its password")
ErrPasswordlessNotAllowed means the account has not turned this on, or is one that may never use it.
var ErrTooBusy = errors.New("auth: too many sign-ins in flight, try again")
ErrTooBusy means the server is holding as many half-finished ceremonies as it is willing to. Reachable without a session, so it must be bounded.
var ErrTooManyFactors = errors.New("auth: this account already has the maximum number of authenticators")
ErrTooManyFactors is returned when an account already holds the maximum.
var ErrUserVerificationRequired = errors.New("auth: this passkey did not verify who you are; use your password")
ErrUserVerificationRequired means the authenticator answered without verifying the user — no PIN, no biometric. Enough for a second factor, not enough to BE the login.
Functions ¶
func HashPassword ¶
HashPassword derives an Argon2id hash and returns it in the standard PHC encoded string form, e.g. $argon2id$v=19$m=65536,t=3,p=2$<salt>$<hash>.
func LDAPTest ¶
func LDAPTest(cfg store.LDAPConfig) (int, error)
LDAPTest verifies the LDAP settings: dial, optional StartTLS, service bind, and a base search. Returns the number of entries under the user base.
func MapsRoles ¶ added in v1.6.0
func MapsRoles(cfg store.LDAPConfig) bool
MapsRoles reports whether any mapping hands out roles. It gates whether LDAP becomes authoritative for role membership: a config written before roles existed grants only sections, and must not silently wipe roles an admin assigned by hand.
func MatchTOTP ¶ added in v1.6.0
MatchTOTP reports whether code is valid and, if so, which time step produced it — so the caller can refuse to accept that step a second time.
The library's own validation only answers yes/no, and "yes" holds for the whole ~90-second window. That makes a single observed code (shoulder-surfed, phished through a proxy, screenshotted by malware) spendable more than once, which is precisely what a one-time password is supposed to prevent.
It re-derives the code for each step in the skew window and compares in constant time, so a wrong code leaks nothing about how wrong it was.
func RolesForGroups ¶ added in v1.6.0
func RolesForGroups(cfg store.LDAPConfig, groups []string) []int64
RolesForGroups returns the union of role ids granted to a user who belongs to the given group DNs. Matching is identical to SectionsForGroups (exact full DN, case-insensitive). Ids are not validated against the roles table here; a role deleted after the mapping was written is skipped when the roles are applied, so a stale id grants nothing rather than failing the login.
func SectionsForGroups ¶ added in v1.5.0
func SectionsForGroups(cfg store.LDAPConfig, groups []string) []string
SectionsForGroups returns the union of RBAC sections granted to a user who belongs to the given group DNs, per the config's group→section mappings. Group DNs are matched case-insensitively on the full DN (exact, not substring) to mirror the admin-group check, and unknown section names are ignored, so a mapping can never grant access beyond a real, named section.
func StepUpKey ¶ added in v1.6.0
StepUpKey buckets password re-checks per account AND per session.
Not the client address, which is what login uses: keying it there let anyone holding a session spend the address's login budget — five wrong passwords on "remove this authenticator" and nobody at that address could sign in for fifteen minutes.
But not the account alone either, which merely moves the damage onto the victim. Step-up is reachable with nothing but a session, so a stolen one could burn the account's whole budget every fifteen minutes: the owner's CORRECT password is then refused for exactly the two things they need to recover — removing the attacker's authenticator and pairing a replacement — while logins keep working, so nothing looks broken.
Per session, the attacker's stolen session spends its own budget and the owner's is untouched. Minting more sessions needs the password, which is what the attacker is trying to guess.
func ValidateTOTP ¶
ValidateTOTP reports whether code is currently valid for secret. A small skew window is allowed to tolerate clock drift between server and device.
Prefer MatchTOTP where a replay matters: this answers "is it valid", which stays true for the whole window, so the same code keeps working until it expires.
func VerifyPassword ¶
VerifyPassword reports whether password matches the encoded hash. The comparison is constant-time to avoid leaking timing information.
func WithClaims ¶ added in v1.5.0
WithClaims returns a context carrying the given claims, the counterpart to ClaimsFrom. RequireSession uses the same key after verifying a token; this is exposed for composing authenticated contexts (and tests).
Types ¶
type Claims ¶
type Claims struct {
UserID int64 `json:"uid"`
Username string `json:"usr"`
Role string `json:"role"`
Kind TokenKind `json:"knd"`
// Epoch is the account's session generation when this token was minted. The
// middleware refuses a token whose epoch is behind the account's current one,
// which is how a password change takes effect immediately instead of waiting
// out the TTL.
Epoch int64 `json:"ep,omitempty"`
jwt.RegisteredClaims
}
Claims is the JWT payload used for both session and MFA-challenge tokens.
type Enrollment ¶
type Enrollment struct {
Secret string `json:"secret"` // base32 secret, also shown for manual entry
OtpauthURL string `json:"otpauthUrl"` // otpauth:// provisioning URI
QRDataURI string `json:"qrDataUri"` // data:image/png;base64,... for <img src>
}
Enrollment holds the data needed to show a user how to add their 2FA token.
func GenerateTOTP ¶
func GenerateTOTP(accountName string) (*Enrollment, error)
GenerateTOTP creates a new TOTP secret for accountName and renders a QR code as a data URI so the frontend can display it without extra endpoints.
type Issued ¶ added in v1.6.0
Issued is a freshly minted token and the facts about it a caller needs: its id (for the session row, or for spending a challenge) and when it expires.
type LDAPResult ¶
type LDAPResult struct {
Username string
IsAdmin bool // member of the configured admin group
Groups []string // the user's group DNs (memberOf), for section mapping
Email string // the directory's mail attribute, if it publishes one
}
LDAPResult is the outcome of a successful LDAP authentication.
func LDAPAuthenticate ¶
func LDAPAuthenticate(cfg store.LDAPConfig, username, password string) (*LDAPResult, error)
LDAPAuthenticate verifies a username/password against an LDAP/AD directory: bind with the service account, search for the user, then bind as that user to validate the password. If an admin group is configured, group membership is reported so the account can be provisioned as an admin.
type LoginLimiter ¶
type LoginLimiter struct {
// contains filtered or unexported fields
}
LoginLimiter is a small in-memory fixed-window rate limiter keyed by client identity (IP or username). It throttles brute-force login attempts without any external dependency. Suitable for a single-instance local tool.
func NewLoginLimiter ¶
func NewLoginLimiter(max int, window time.Duration) *LoginLimiter
NewLoginLimiter allows max failed attempts within the given window.
func (*LoginLimiter) Allow ¶
func (l *LoginLimiter) Allow(key string) bool
Allow reports whether another attempt is permitted for key right now. It does not consume an attempt; call Fail to record a failed attempt.
func (*LoginLimiter) Fail ¶
func (l *LoginLimiter) Fail(key string)
Fail records a failed attempt for key, starting a window if needed.
func (*LoginLimiter) Reset ¶
func (l *LoginLimiter) Reset(key string)
Reset clears the counter for key after a successful login.
type LoginResult ¶
type LoginResult struct {
MFARequired bool
Token string // session token, or MFA-challenge token if MFARequired
ExpiresAt time.Time
User *store.User
}
LoginResult is returned from Login: either a finished session, or an MFA challenge the caller must satisfy via VerifyMFA.
type Middleware ¶
type Middleware struct {
// contains filtered or unexported fields
}
Middleware enforces a valid, fully-authenticated session token. It reads the token from the session cookie first, then falls back to an Authorization Bearer header (useful for API clients and tooling).
func NewMiddleware ¶
func NewMiddleware(tokens *TokenManager, epochs SessionEpochSource) *Middleware
NewMiddleware builds auth middleware backed by the given token manager and the source of session generations.
func (*Middleware) ParseSessionToken ¶
func (m *Middleware) ParseSessionToken(raw string) (*Claims, error)
ParseSessionToken validates a raw token and ensures it is a session token. Used by the WebSocket handler which authenticates before upgrading.
func (*Middleware) RequireSession ¶
func (m *Middleware) RequireSession(next http.Handler) http.Handler
RequireSession wraps next, rejecting requests without a valid session token.
type PasswordlessError ¶ added in v1.6.0
PasswordlessError carries the account a failed attempt was for.
Once the assertion has verified, the account is known — and the failures after that point are the ones worth writing down: a cloned key, a missing PIN, an account that has not opted in. Without the username the audit log cannot say who any of it happened to, and a cloned-authenticator detection is the last thing that should vanish silently.
func (*PasswordlessError) Error ¶ added in v1.6.0
func (e *PasswordlessError) Error() string
func (*PasswordlessError) Unwrap ¶ added in v1.6.0
func (e *PasswordlessError) Unwrap() error
type RelyingParty ¶ added in v1.6.0
type RelyingParty struct {
ID string // e.g. "docker.example.com"
Origin string // e.g. "https://docker.example.com"
DisplayName string
}
RelyingParty describes who is asking, in WebAuthn's terms: the id a credential is bound to, and the origin it may be used from.
Both are derived per request rather than configured, because getting them wrong is not a security failure but a usability one — a credential registered against the wrong id simply never works again. The browser is what enforces that the id matches the page's origin, so an attacker cannot use a forged Host header to mint a credential usable elsewhere: they would only produce one that is useless.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service orchestrates the authentication flows on top of the store and the crypto/token primitives in this package.
func NewService ¶
func NewService(s *store.Store, tm *TokenManager) *Service
NewService wires the auth service together.
func (*Service) BeginPasskeyLogin ¶ added in v1.6.0
func (s *Service) BeginPasskeyLogin(ctx context.Context, rp RelyingParty, challengeToken string) (*protocol.CredentialAssertion, error)
BeginPasskeyLogin issues an assertion challenge for the account named by an MFA challenge token. The token is proof the password was right; this is the second factor.
func (*Service) BeginPasskeyRegistration ¶ added in v1.6.0
func (s *Service) BeginPasskeyRegistration(ctx context.Context, rp RelyingParty, userID int64, stepUp bool) (*protocol.CredentialCreation, error)
BeginPasskeyRegistration starts pairing a passkey for an account.
stepUp says whether the caller proved the password. It is remembered with the ceremony rather than re-derived at the end; see FinishPasskeyRegistration.
func (*Service) BeginPasswordlessLogin ¶ added in v1.6.0
func (s *Service) BeginPasswordlessLogin(rlKey string, rp RelyingParty) (*protocol.CredentialAssertion, string, error)
BeginPasswordlessLogin issues a discoverable-credential challenge.
Nobody has said who they are, so this cannot be scoped to an account: the authenticator is asked what it holds for this relying party. The returned id is what ties the answer back to this challenge — an opaque, single-use handle, since there is no session and no half-finished login to key it on.
func (*Service) BeginTOTPEnrollment ¶
func (s *Service) BeginTOTPEnrollment(ctx context.Context, userID int64, stepUp bool) (*Enrollment, error)
BeginTOTPEnrollment generates a new secret + QR for the user. The secret is stored but not yet enabled until confirmed via ConfirmTOTPEnrollment.
stepUp says whether the caller proved the password to get here. It is stored with the candidate so that confirming it can be judged against the account's protection at the time the enrolment began — see ConfirmTOTPEnrollment.
func (*Service) ChallengeUsername ¶ added in v1.6.0
ChallengeUsername reports which account an MFA challenge token names, so a failed verification can be audited against it. It validates the token's signature and kind but says nothing about the code — callers must not treat a non-empty result as authentication.
func (*Service) ConfirmTOTPEnrollment ¶
ConfirmTOTPEnrollment validates the first code from the candidate authenticator and pairs it. Anything already paired keeps working: this adds a factor, it does not replace one.
func (*Service) CreateAccount ¶
func (s *Service) CreateAccount(ctx context.Context, username, password, role string, readOnly bool, sections []string) (*store.User, error)
CreateAccount creates a non-setup user account (used by admins). role is "admin" or "user"; for "user", sections and readOnly scope their access.
func (*Service) FinishPasskeyLogin ¶ added in v1.6.0
func (s *Service) FinishPasskeyLogin(ctx context.Context, rp RelyingParty, rlKey, challengeToken string, r *http.Request, info SessionInfo) (*LoginResult, error)
FinishPasskeyLogin verifies an assertion and issues a session.
rlKey is the rate-limit bucket, as for VerifyMFA: a passkey is not guessable, but the endpoint must not become a free oracle for probing which accounts exist.
func (*Service) FinishPasskeyRegistration ¶ added in v1.6.0
func (s *Service) FinishPasskeyRegistration(ctx context.Context, rp RelyingParty, userID int64, name string, r *http.Request) error
FinishPasskeyRegistration completes pairing and stores the credential.
The password check lives in "begin", but the factor is created here, and the two are minutes apart. An account with no second factor may open a ceremony without a password — there is nothing to protect yet — so if it gained one in the meantime, finishing would add an authenticator to a now-protected account on the strength of a session alone. What authorised the ceremony has to still be enough when it lands.
func (*Service) FinishPasswordlessLogin ¶ added in v1.6.0
func (s *Service) FinishPasswordlessLogin(ctx context.Context, rp RelyingParty, rlKey, ceremonyID string, r *http.Request, info SessionInfo) (*LoginResult, error)
FinishPasswordlessLogin verifies the assertion and issues a session.
rlKey is the rate-limit bucket — the client address, because until the assertion verifies there is no account to bucket on. That is also why this must not become a free oracle: every failure costs the caller budget.
func (*Service) HasPasskeys ¶ added in v1.6.0
HasPasskeys reports whether the account has any paired.
func (*Service) ListFactors ¶ added in v1.6.0
ListFactors returns the account's paired factors.
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, rlKey, username, password string, exemptMFA bool, info SessionInfo) (*LoginResult, error)
Login verifies username+password. If the account has TOTP enabled it returns an MFA challenge token; otherwise a full session token. rlKey is the rate limit bucket (typically the client IP). exemptMFA skips the 2FA step (used for localhost when the admin has allowed it).
func (*Service) NeedsSetup ¶
NeedsSetup reports whether no account exists yet (first-run wizard).
func (*Service) RemoveFactor ¶ added in v1.6.0
RemoveFactor unpairs one of the account's factors.
The last one cannot go. 2FA is mandatory here (bar the localhost exemption, which is a property of where you connect from, not of the account), so an account with zero factors is one that cannot sign in from anywhere else — a self-lockout with no admin reset behind it. Pair the replacement first, then remove the old one.
func (*Service) SetPassword ¶
SetPassword replaces a user's password (admin reset or self-change) and invalidates every session already issued for that account.
Without the second half the first is half a control: a JWT is self-contained, so nothing about changing the password reaches the copy an attacker already holds. They would keep full access for the rest of the token's twelve hours — granted by the very act meant to take it away.
func (*Service) VerifyMFA ¶
func (s *Service) VerifyMFA(ctx context.Context, rlKey, challengeToken, code string, info SessionInfo) (*LoginResult, error)
VerifyMFA completes login by validating a TOTP code against the MFA-challenge token issued by Login. rlKey is the same bucket Login uses (the client IP).
This step is rate limited for the same reason the password step is, and the reason is easy to miss: by the time it runs the attacker already has the password, so a six-digit code is the only thing left. Unthrottled, that is ~10^6 guesses — minutes of scripted requests — and the code path is cheap (no argon2), which is what makes it practical rather than theoretical.
It is keyed on the client IP *and* on the account, so rotating source addresses doesn't buy a fresh budget: the account bucket keeps counting.
func (*Service) VerifyUserPassword ¶ added in v1.6.0
func (s *Service) VerifyUserPassword(ctx context.Context, rlKey string, u *store.User, password string) error
VerifyUserPassword checks a password against the account it belongs to, local hash or directory bind, without issuing anything. It returns nil when the password is right, ErrRateLimited when the budget for this key is spent, and ErrInvalidCreds otherwise.
Used for step-up on operations a session alone must not authorise. It burns a rate-limit budget: otherwise it is a password oracle that answers as fast as you can ask, reachable by anyone holding a session.
Telling the two failures apart matters. Reporting a spent budget as "wrong password" tells the owner their own password is wrong, which is both false and the exact moment they are trying to recover an account.
type SessionEpochSource ¶ added in v1.6.0
type SessionEpochSource interface {
SessionEpoch(ctx context.Context, userID int64) (int64, error)
SessionExists(ctx context.Context, id string, userID int64) (bool, error)
TouchSession(ctx context.Context, id string) error
}
SessionEpochSource reports an account's current session generation, and ErrNotFound (or any error) if the account is gone. It also answers whether a particular session is still recorded, which is what makes revoking one from the profile take effect against a self-contained token.
type SessionInfo ¶ added in v1.6.0
SessionInfo is what a login knows about the client asking for one. Recorded on the session so its owner can recognise it later.
type TokenKind ¶
type TokenKind string
TokenKind distinguishes a fully-authenticated session token from the short-lived intermediate token issued between the password and 2FA steps.
type TokenManager ¶
type TokenManager struct {
// contains filtered or unexported fields
}
TokenManager mints and verifies HMAC-signed JWTs.
func NewTokenManager ¶
func NewTokenManager(secret []byte, sessionTTL time.Duration) *TokenManager
NewTokenManager returns a manager signing with secret. sessionTTL controls how long a logged-in session stays valid before re-authentication.