Documentation
¶
Overview ¶
Package auth is part of the Redoubt control plane. See CLAUDE.md for its role.
Index ¶
- Constants
- Variables
- func GenerateRecoveryCodes(ctx context.Context, n int) (plain, hashes []string, err error)
- func GenerateTOTP(issuer, account string) (secret, otpauthURL string, err error)
- func HashPassword(ctx context.Context, pw string) (string, error)
- func LoadOrCreateBootstrapToken(path string) (string, error)
- func NeedsRehash(hash string) bool
- func NormalizeEmail(email string) string
- func TOTPCounter(t time.Time) int64
- func TOTPEnabled(u db.User) bool
- func TokenEqual(a, b string) bool
- func ValidateAPIToken(ctx context.Context, st *store.Store, plain string) (db.User, db.ApiToken, error)
- func ValidateEmail(email string) (string, error)
- func ValidateTOTP(secret, code string, now time.Time) bool
- func ValidateTOTPCounter(secret, code string, now time.Time, lastUsed int64) (ok bool, counter int64)
- func VerifyPassword(ctx context.Context, hash, pw string) (bool, error)
- func VerifyRecoveryCode(ctx context.Context, hashes []string, code string) (index int, ok bool, err error)
- type LoginResult
- type Permission
- type Role
- type Service
- func (s *Service) BootstrapOwner(ctx context.Context, presented, expected, email, password string) (db.User, error)
- func (s *Service) ChangePassword(ctx context.Context, user db.User, ...) error
- func (s *Service) ChangeRole(ctx context.Context, actor db.User, userID string, role Role) (db.User, error)
- func (s *Service) CompleteTOTP(ctx context.Context, sessionID, code string) error
- func (s *Service) ConfirmTOTP(ctx context.Context, user db.User, sessionID, code string) ([]string, error)
- func (s *Service) CreateAPIToken(ctx context.Context, actor db.User, name string) (string, db.ApiToken, error)
- func (s *Service) CreateUser(ctx context.Context, actor db.User, email, password string, role Role) (db.User, error)
- func (s *Service) DeleteUser(ctx context.Context, actor db.User, userID string) error
- func (s *Service) EnrollTOTP(ctx context.Context, user db.User, issuer string, ...) (TOTPEnrollment, error)
- func (s *Service) GetUser(ctx context.Context, actor db.User, userID string) (db.User, error)
- func (s *Service) ListSessions(ctx context.Context, actor db.User) ([]db.Session, error)
- func (s *Service) ListUsers(ctx context.Context, actor db.User) ([]db.User, error)
- func (s *Service) Login(ctx context.Context, email, password, ip, userAgent string) (LoginResult, error)
- func (s *Service) Logout(ctx context.Context, sessionID string) error
- func (s *Service) ResetPassword(ctx context.Context, actor db.User, userID, newPassword string) error
- func (s *Service) RevokeAPIToken(ctx context.Context, actor db.User, tokenID string) error
- func (s *Service) RevokeSession(ctx context.Context, actor db.User, sessionID string) error
- func (s *Service) SetDisabled(ctx context.Context, actor db.User, userID string, disabled bool) (db.User, error)
- func (s *Service) SweepLoginAttempts(ctx context.Context) (int64, error)
- type Sessions
- func (s *Sessions) ClearCookie() *http.Cookie
- func (s *Sessions) Cookie(value string) *http.Cookie
- func (s *Sessions) Create(ctx context.Context, userID, ip, userAgent string, totpVerified bool) (string, db.Session, error)
- func (s *Sessions) Get(ctx context.Context, sessionID string) (db.Session, error)
- func (s *Sessions) MarkTOTPVerified(ctx context.Context, sessionID string) error
- func (s *Sessions) Revoke(ctx context.Context, sessionID string) error
- func (s *Sessions) RevokeAll(ctx context.Context, userID string) error
- func (s *Sessions) RevokeOthers(ctx context.Context, userID, keepSessionID string) error
- func (s *Sessions) Validate(ctx context.Context, cookieValue string) (db.Session, db.User, error)
- func (s *Sessions) ValidatePending(ctx context.Context, cookieValue string) (sess db.Session, user db.User, pending bool, err error)
- type TOTPEnrollment
Constants ¶
const ( // MinPasswordChars is the minimum password length in Unicode characters (NIST SP 800-63B // recommends at least 8 for user-chosen secrets; this platform holds production // credentials, so it asks for 12). MinPasswordChars = 12 // MaxPasswordBytes bounds the argon2 input so that a multi-megabyte password cannot be used // as a denial-of-service vector. MaxPasswordBytes = 1024 // ArgonConcurrency is the maximum number of argon2 derivations in flight at once. Each one // allocates argonMemory (64 MiB) for its duration; platformd runs under a 512 MiB cgroup // limit, so 3 x 64 MiB = 192 MiB is the ceiling an unauthenticated burst can pin (D-021). // Callers that cannot get a slot within ArgonWait receive ErrBusy instead of queueing memory. ArgonConcurrency = 4 // 4 × 64 MiB peak; small-VPS safe and enough for parallel logins // ArgonWait is how long a caller waits for a free derivation slot before ErrBusy. ArgonWait = 15 * time.Second // bounded queueing before 503; login bursts drain in well under this )
Argon2id parameters (OWASP Password Storage Cheat Sheet, "argon2id, m=64 MiB, t=3, p=1" class). NeedsRehash reports hashes produced with anything weaker so that parameters can be raised later and old hashes upgraded transparently on the next successful login.
const ( DefaultLockoutThreshold = 5 DefaultLockoutWindow = 15 * time.Minute DefaultIPFailureThreshold = 50 DefaultIPThrottleDelay = 2 * time.Second )
Brute-force defaults (OWASP ASVS 2.2.1 / NIST SP 800-63B §5.2.2).
Two scopes are tracked in login_attempts. The *account* scope ("user:<email>") hard-locks after DefaultLockoutThreshold failures inside DefaultLockoutWindow. The *source* scope ("ip:<addr>") never locks: in both shipped topologies every client shares one address (127.0.0.1 through the SSH tunnel, or Traefik's container IP), so a hard lock there would let anyone with five bad passwords deny login to every user (D-021). Instead, once an address exceeds DefaultIPFailureThreshold failures in the window, every further login attempt from it is delayed by DefaultIPThrottleDelay before the password is even checked — slowing password spraying without ever refusing a correct login.
const ( ActionLogin = "auth.login" ActionLogout = "auth.logout" ActionLockout = "auth.lockout" ActionThrottle = "auth.throttle" ActionTOTP = "auth.totp" ActionTOTPEnroll = "auth.totp.enroll" ActionTOTPConfirm = "auth.totp.confirm" ActionTokenCreate = "auth.token.create" // #nosec G101 -- audit action name, not a credential ActionTokenRevoke = "auth.token.revoke" // #nosec G101 -- audit action name, not a credential ActionBootstrap = "auth.bootstrap" ActionUserCreate = "user.create" ActionPasswordChange = "user.password_change" )
Audit action names emitted by Service.
const ( // CookieName is the name of the session cookie. // The __Host- prefix makes browsers refuse the cookie unless it is Secure, Path=/ and // host-only, so no same-site page can plant or fixate a session (see D-023). CookieName = "__Host-redoubt_session" // DefaultSessionTTL is the absolute session lifetime. DefaultSessionTTL = 12 * time.Hour // DefaultIdleTTL is the maximum inactivity before a session expires. DefaultIdleTTL = time.Hour )
Session cookie settings.
const ( TOTPPeriod = 30 TOTPDigits = otp.DigitsSix // TOTPSkew is the number of adjacent time steps accepted on either side of "now". TOTPSkew = 1 // RecoveryCodeCount is the number of recovery codes issued at enrolment. RecoveryCodeCount = 10 )
TOTP parameters: the RFC 6238 defaults (SHA-1, 6 digits, 30 s) so that every authenticator app works. SHA-1 here is an HMAC key-derivation, not a collision-sensitive use.
const ( ActionUserList = "user.list" ActionRoleChange = "user.role_change" ActionUserDisable = "user.disable" ActionUserEnable = "user.enable" ActionUserDelete = "user.delete" ActionPasswordReset = "user.password_reset" ActionSessionRevoke = "auth.session.revoke" )
User-management audit actions (E2.2). Every method below records exactly one event per call: "ok" when the change was made, "denied" when RBAC or an invariant refused it.
const APITokenPrefix = "rdt_"
APITokenPrefix identifies Redoubt API tokens (so leaked tokens are recognisable by secret scanners) and lets ValidateAPIToken reject foreign strings before hashing.
const BootstrapTokenBytes = 32
BootstrapTokenBytes is the entropy of the bootstrap token (256 bits).
Variables ¶
var ( // ErrWeakPassword is returned by HashPassword for passwords shorter than MinPasswordChars. ErrWeakPassword = fmt.Errorf("auth: password must be at least %d characters", MinPasswordChars) // ErrPasswordTooLong is returned by HashPassword for passwords longer than MaxPasswordBytes. ErrPasswordTooLong = fmt.Errorf("auth: password must be at most %d bytes", MaxPasswordBytes) // ErrMalformedHash is returned when a stored hash is not a well-formed argon2id PHC string. ErrMalformedHash = errors.New("auth: malformed password hash") // ErrBusy is returned when no argon2 slot became free within ArgonWait. It is transient: the // handler maps it to 503 + Retry-After and the client should simply retry. ErrBusy = errors.New("auth: server busy, retry later") )
var ( ErrInvalidCredentials = errors.New("auth: invalid credentials") ErrLockedOut = errors.New("auth: too many failed attempts, try again later") ErrForbidden = errors.New("auth: forbidden") ErrInvalidEmail = errors.New("auth: invalid email address") ErrInvalidTOTP = errors.New("auth: invalid one-time code") ErrBootstrapClosed = errors.New("auth: bootstrap is closed: an owner already exists") ErrUserExists = errors.New("auth: a user with that email already exists") )
Sentinel errors. Every credential failure surfaces as ErrInvalidCredentials so that neither error text nor timing distinguishes "no such user" from "wrong password".
var ( // ErrUserNotFound is returned for an unknown user id. ErrUserNotFound = errors.New("auth: user not found") // ErrSelfTarget is returned when an actor tries to change its own role, disable, delete or // admin-reset its own account: those need a second administrator (or, for the password, // ChangePassword with the current one). ErrSelfTarget = errors.New("auth: you cannot do that to your own account") // ErrLastOwner is returned when a change would leave the platform without an active Owner. ErrLastOwner = errors.New("auth: the last active owner cannot be demoted, disabled or deleted") // ErrSessionNotFound is returned by RevokeSession for a session that is not the actor's. ErrSessionNotFound = errors.New("auth: session not found") )
Sentinel errors of user management.
var AllPermissions = []Permission{ PermUsersManage, PermAppsManage, PermAppsDeploy, PermAppsView, PermSecretsWrite, PermLogsView, PermBilling, PermDestructive, PermAuditRead, PermSettingsManage, }
AllPermissions lists every permission.
var AllRoles = []Role{RoleOwner, RoleAdmin, RoleDeployer, RoleViewer}
AllRoles lists every role, most privileged first.
var ErrInvalidTokenName = errors.New("auth: token name must be 1-128 characters")
ErrInvalidTokenName is returned by CreateAPIToken for an empty or over-long name.
var ErrTOTPAlreadyEnabled = errors.New("auth: TOTP already enabled")
ErrTOTPAlreadyEnabled is returned when enrolling a user that already has TOTP enabled.
var ErrTOTPNotEnrolled = errors.New("auth: totp not enrolled")
ErrTOTPNotEnrolled is returned when a second factor is required but the user has none.
var ErrTOTPNotPending = errors.New("auth: no pending TOTP enrollment")
ErrTOTPNotPending is returned when confirming without a pending enrollment.
var ErrTOTPRequired = fmt.Errorf("%w: second factor required", ErrUnauthenticated)
ErrTOTPRequired is returned by Validate for a live session whose user has TOTP enabled but which has not yet presented the second factor. It wraps ErrUnauthenticated so that every consumer that only checks errors.Is(err, ErrUnauthenticated) fails closed; only the TOTP completion path (via ValidatePending) may accept such a session (golden rule 6).
var ErrUnauthenticated = errors.New("auth: unauthenticated")
ErrUnauthenticated is returned for any session or token that is missing, malformed, unknown, revoked, expired, idle too long, or belongs to a disabled user. Callers must not distinguish these cases to the client.
Functions ¶
func GenerateRecoveryCodes ¶
GenerateRecoveryCodes returns n single-use recovery codes ("XXXXX-XXXXX", 10 symbols from a 32-symbol alphabet) together with their argon2id hashes. The plaintexts are shown to the user once; only the hashes are stored (as a JSON array in users.recovery_codes). Each hash goes through the argon2 admission gate, so ctx bounds the total wait.
func GenerateTOTP ¶
GenerateTOTP creates a new TOTP secret for account at issuer. It returns the base32 secret (for the caller to age-encrypt and store) and the otpauth:// URL to render as a QR code. Both are secrets: show once, never log.
func HashPassword ¶
HashPassword derives an argon2id hash of pw and returns it as a PHC-format string ("$argon2id$v=19$m=65536,t=3,p=1$<salt>$<hash>", base64 without padding). It enforces the password policy: at least MinPasswordChars characters and at most MaxPasswordBytes bytes. It waits for an argon2 slot (ErrBusy after ArgonWait, or ctx's error).
func LoadOrCreateBootstrapToken ¶
LoadOrCreateBootstrapToken returns the bootstrap API token stored at path, generating it with crypto/rand and writing it with mode 0600 (O_EXCL) on first use. The token is never logged (D-011); the CLI reads it from the data directory on the host.
func NeedsRehash ¶
NeedsRehash reports whether hash was produced with parameters weaker than the current ones (or is malformed) and should be replaced the next time the plaintext is available.
func NormalizeEmail ¶
NormalizeEmail lower-cases and trims an email address. It performs no validation; see ValidateEmail.
func TOTPCounter ¶
TOTPCounter returns the RFC 6238 time-step counter for t.
func TOTPEnabled ¶
TOTPEnabled reports whether the user must present a second factor.
func TokenEqual ¶
TokenEqual compares two tokens in constant time (via SHA-256 digests so lengths never leak).
func ValidateAPIToken ¶
func ValidateAPIToken(ctx context.Context, st *store.Store, plain string) (db.User, db.ApiToken, error)
ValidateAPIToken resolves a presented bearer value to its token and user. It returns ErrUnauthenticated for unknown, revoked, or malformed tokens and for disabled users. On success it refreshes last_used_at at most once per minute.
func ValidateEmail ¶
ValidateEmail normalises email and checks that it is a single bare RFC 5322 address (no display name, no angle brackets, no whitespace) of at most 254 bytes.
func ValidateTOTP ¶
ValidateTOTP reports whether code is valid for secret at now, accepting TOTPSkew adjacent steps. It does not protect against reuse of a code; use ValidateTOTPCounter for login.
func ValidateTOTPCounter ¶
func ValidateTOTPCounter(secret, code string, now time.Time, lastUsed int64) (ok bool, counter int64)
ValidateTOTPCounter validates code against secret at now (±TOTPSkew steps) and returns the time-step counter the code was accepted for. Counters at or below lastUsed are rejected, so storing the returned counter and passing it back on the next attempt makes every code single-use (RFC 6238 §5.2). Every candidate step is compared in constant time and the loop never exits early.
func VerifyPassword ¶
VerifyPassword reports whether pw matches the argon2id PHC hash. The comparison is constant time. A malformed hash is an error (it indicates a corrupt or foreign record, not a wrong password); a wrong password is (false, nil); ErrBusy (or ctx's error) means the check did not run because no argon2 slot was available.
func VerifyRecoveryCode ¶
func VerifyRecoveryCode(ctx context.Context, hashes []string, code string) (index int, ok bool, err error)
VerifyRecoveryCode checks code against every hash (never stopping early, so the cost does not reveal which slot matched) and returns the index of the matching hash. The caller must remove that index from the stored list so the code cannot be used again. A gate error (ErrBusy or ctx's error) aborts the check and is returned so the caller can retry rather than treat it as a wrong code.
Types ¶
type LoginResult ¶
type LoginResult struct {
// CookieValue is the session bearer value to set with Sessions.Cookie.
CookieValue string
// NeedsTOTP is true when the user has a second factor enrolled and the session is not yet
// fully authenticated; the caller must complete it with CompleteTOTP before granting access.
NeedsTOTP bool
User db.User
Session db.Session
}
LoginResult is the outcome of a successful password check.
type Permission ¶
type Permission string
Permission is a capability checked in the service layer (golden rule 6). There is deliberately no "reveal secret" permission: secret values are write-only for every role by construction, and no permission can be added to change that.
const ( PermUsersManage Permission = "users.manage" // invite, change roles, remove users PermAppsManage Permission = "apps.manage" // create / configure apps, addons, servers PermAppsDeploy Permission = "apps.deploy" // deploy and roll back PermAppsView Permission = "apps.view" // list and inspect apps PermSecretsWrite Permission = "secrets.write" // set / rotate / delete (never read) PermLogsView Permission = "logs.view" // build, deploy and runtime logs (redacted) PermBilling Permission = "billing" // billing and plan changes PermDestructive Permission = "destructive" // delete apps, addons, data volumes PermAuditRead Permission = "audit.read" // read and export the audit log PermSettingsManage Permission = "settings.manage" // platform settings )
Permissions, one per row of the RBAC matrix in docs/SECURITY.md §6.
type Role ¶
type Role string
Role is one of the four built-in roles (docs/SECURITY.md §6). Roles are ordered by privilege only for display; authorisation always goes through Can.
const ( RoleOwner Role = "owner" RoleAdmin Role = "admin" RoleDeployer Role = "deployer" RoleViewer Role = "viewer" )
The four roles. The database CHECK constraint on users.role admits exactly these values.
func (Role) Can ¶
func (r Role) Can(p Permission) bool
Can reports whether the role holds the permission. Unknown roles and unknown permissions are always denied.
type Service ¶
type Service struct {
Store *store.Store
Sessions *Sessions
Audit audit.Sink
// Now is overridable for tests.
Now func() time.Time
// LockoutThreshold is the number of failures within LockoutWindow that locks an account
// (default DefaultLockoutThreshold).
LockoutThreshold int
// LockoutWindow is both the failure-counting window and the lock duration
// (default DefaultLockoutWindow).
LockoutWindow time.Duration
// IPFailureThreshold is the number of failures within LockoutWindow from one source address
// after which its attempts are delayed (default DefaultIPFailureThreshold). Never a lock.
IPFailureThreshold int
// IPThrottleDelay is the delay applied to a throttled address (default DefaultIPThrottleDelay).
IPThrottleDelay time.Duration
Logger *slog.Logger
// TOTPSecret returns the user's decrypted TOTP secret. It is supplied by the caller (the
// secrets package owns the age key) so that this package never touches ciphertext.
TOTPSecret func(ctx context.Context, user db.User) (string, error)
// contains filtered or unexported fields
}
Service is the authentication and user-management service. RBAC checks live here (golden rule 6); every state change is audited (golden rule 5); no method ever logs or returns a password, code, secret, or token in an error or audit detail (golden rule 3).
func (*Service) BootstrapOwner ¶
func (s *Service) BootstrapOwner(ctx context.Context, presented, expected, email, password string) (db.User, error)
BootstrapOwner creates the first Owner account. It succeeds only while no user exists and the presented bootstrap token equals the expected one (D-011). After the first user exists the token is useless regardless of its value.
func (*Service) ChangePassword ¶
func (s *Service) ChangePassword(ctx context.Context, user db.User, oldPassword, newPassword, keepSessionID string) error
ChangePassword replaces the user's password after verifying the current one and revokes every other session of the user (keepSessionID, typically the caller's own, survives; pass "" to revoke all).
func (*Service) ChangeRole ¶
func (s *Service) ChangeRole(ctx context.Context, actor db.User, userID string, role Role) (db.User, error)
ChangeRole sets the target's role. Rules: the actor needs PermUsersManage; nobody changes their own role; only an Owner may grant Owner or change an Owner's role (an Admin cannot touch an Owner, nor mint a role above its own); and the last active Owner cannot be demoted (checked inside the same transaction as the update so two concurrent demotions cannot race past each other). The change is effective on the target's next request without re-login, because Sessions.Validate re-reads the user row. Audited as user.role_change with from/to.
func (*Service) CompleteTOTP ¶
CompleteTOTP verifies a one-time code for a session that logged in with a password and marks the session fully authenticated. Codes are single-use: a replay within the validity window is rejected. A wrong code counts towards the account's lockout and the source's throttle.
func (*Service) ConfirmTOTP ¶
func (s *Service) ConfirmTOTP(ctx context.Context, user db.User, sessionID, code string) ([]string, error)
ConfirmTOTP validates the first code against the pending secret, enables TOTP, generates recovery codes (returned once, stored hashed) and marks the calling session as verified. The accepted code is remembered so it cannot be replayed at CompleteTOTP; a wrong code counts towards the account's lockout and the source's throttle exactly like a wrong login code.
func (*Service) CreateAPIToken ¶
func (s *Service) CreateAPIToken(ctx context.Context, actor db.User, name string) (string, db.ApiToken, error)
CreateAPIToken mints a bearer token for actor and audits it (auth.token.create). The plaintext ("rdt_" + 32 random bytes base64url) is returned once; the database stores its SHA-256 and the audit event carries only the token id and name. API tokens bypass TOTP by design, so the caller must be a fully authenticated, enabled user.
func (*Service) CreateUser ¶
func (s *Service) CreateUser(ctx context.Context, actor db.User, email, password string, role Role) (db.User, error)
CreateUser creates a user with the given role on behalf of actor. The actor needs PermUsersManage; only an Owner may create another Owner (an Admin must not be able to mint a role above its own).
func (*Service) DeleteUser ¶
DeleteUser removes the user and, through the schema's cascades, every session and API token. Owner only (PermDestructive); not self; the last active Owner cannot be deleted. Audited as user.delete.
func (*Service) EnrollTOTP ¶
func (s *Service) EnrollTOTP(ctx context.Context, user db.User, issuer string, encrypt func(string) ([]byte, error)) (TOTPEnrollment, error)
EnrollTOTP generates a TOTP secret for user, stores it encrypted (via encrypt) with totp_enabled = 0, and returns the one-time provisioning data. Confirm with ConfirmTOTP. The audit event carries no detail at all: the secret and the otpauth URL are credentials.
func (*Service) ListSessions ¶
ListSessions returns the actor's own live (unrevoked, unexpired, not idle) sessions, newest first. Every authenticated user may see their own sessions.
func (*Service) ListUsers ¶
ListUsers returns every user (PermUsersManage). Rows carry hashes and ciphertext; callers that render them must project to a view (httpapi.UserView) and never expose those columns.
func (*Service) Login ¶
func (s *Service) Login(ctx context.Context, email, password, ip, userAgent string) (LoginResult, error)
Login verifies email + password, applies brute-force controls, and opens a session. The returned error is ErrInvalidCredentials for a malformed or unknown email, a wrong password, or a disabled account alike; ErrLockedOut when the account is locked; ErrBusy when no argon2 slot was available. A throttled source address is delayed, never refused.
func (*Service) Logout ¶
Logout revokes the session and audits it. Unknown or already-revoked sessions return ErrUnauthenticated.
func (*Service) ResetPassword ¶
func (s *Service) ResetPassword(ctx context.Context, actor db.User, userID, newPassword string) error
ResetPassword sets a new password for the target without knowing the old one and revokes every session of the target. An Owner may reset any other account; an Admin only accounts of strictly lower rank (Deployer, Viewer) — never a peer or an Owner, so an Admin cannot take over another administrator. Not self (use ChangePassword). Audited as user.password_reset; the password never appears anywhere.
func (*Service) RevokeAPIToken ¶
RevokeAPIToken revokes tokenID if it belongs to actor and audits the outcome (auth.token.revoke). Revoking an unknown or foreign token is a silent no-op for the caller (so token ids cannot be probed) but is recorded as a denied event.
func (*Service) RevokeSession ¶
RevokeSession ends one of the actor's own sessions (self-service; a foreign or unknown id is ErrSessionNotFound and audited as denied, so ids cannot be probed). Audited as auth.session.revoke.
func (*Service) SetDisabled ¶
func (s *Service) SetDisabled(ctx context.Context, actor db.User, userID string, disabled bool) (db.User, error)
SetDisabled disables (or re-enables) the target. Disabling revokes every session and API token of the user in the same transaction, so access ends immediately; re-enabling never restores them. Rules: PermUsersManage; not self; an Admin cannot touch an Owner; the last active Owner cannot be disabled. Audited as user.disable / user.enable.
func (*Service) SweepLoginAttempts ¶
SweepLoginAttempts deletes login_attempts rows whose counting window has passed and that hold no unexpired lock, so that failed attempts against never-existing emails cannot accumulate forever. It runs automatically after failures (rate-limited) and may be called by a scheduler.
type Sessions ¶
type Sessions struct {
Store *store.Store
// TTL is the absolute lifetime (default DefaultSessionTTL).
TTL time.Duration
// IdleTTL is the inactivity timeout (default DefaultIdleTTL).
IdleTTL time.Duration
// Now is overridable for tests.
Now func() time.Time
}
Sessions issues and validates server-side sessions. The client holds a random bearer value in a cookie; the database stores only its SHA-256, so a database leak yields nothing usable.
func (*Sessions) ClearCookie ¶
ClearCookie builds a cookie that deletes the session cookie in the browser.
func (*Sessions) Cookie ¶
Cookie builds the session cookie: HttpOnly (unreadable from JavaScript), Secure, SameSite=Strict (no cross-site sends, so CSRF cannot ride the cookie), Path=/.
Secure is unconditional (D-022). platformd itself only ever speaks plain HTTP (cmd/platformd calls ListenAndServe; there is no TLS listener in any mode): in production Traefik terminates TLS in front of it, and in dev / SSH-tunnel use the dashboard is reached at http://127.0.0.1 or http://localhost, which browsers treat as a secure context, so they accept and send Secure cookies there. Any other plain-HTTP exposure of platformd therefore cannot log in through a browser at all — which is the intended failure mode.
func (*Sessions) Create ¶
func (s *Sessions) Create(ctx context.Context, userID, ip, userAgent string, totpVerified bool) (string, db.Session, error)
Create opens a session for userID and returns the cookie value to hand to the client. The value is 32 random bytes (base64url); only its SHA-256 is stored.
func (*Sessions) Get ¶
Get returns a session by ID, applying the same liveness rules as Validate (but not touching last_seen_at and not loading the user).
func (*Sessions) MarkTOTPVerified ¶
MarkTOTPVerified records that the session completed its second factor.
func (*Sessions) RevokeOthers ¶
RevokeOthers ends every live session of a user except keepSessionID.
func (*Sessions) Validate ¶
Validate resolves a cookie value to its fully authenticated session and user. It returns ErrUnauthenticated when the session is unknown, revoked, past its absolute expiry, idle longer than IdleTTL, or the user is disabled, and ErrTOTPRequired (which also satisfies errors.Is(err, ErrUnauthenticated)) when the user has TOTP enabled and the session has not completed it. On success it refreshes last_seen_at at most once per minute.
func (*Sessions) ValidatePending ¶
func (s *Sessions) ValidatePending(ctx context.Context, cookieValue string) (sess db.Session, user db.User, pending bool, err error)
ValidatePending is Validate for the one caller that must see half-authenticated sessions: the TOTP completion endpoint. It applies every liveness rule of Validate but returns a pending session (user has TOTP enabled, session not yet verified) with pending = true instead of ErrTOTPRequired. Callers must grant nothing beyond TOTP completion while pending is true.
type TOTPEnrollment ¶
TOTPEnrollment is returned once from EnrollTOTP: the secret and provisioning URL are shown to the user exactly one time and never again (write-only, golden rule 3).