Documentation
¶
Overview ¶
Package auth is passkey (WebAuthn) registration and login: no password ever stored or transmitted, a session cookie once a ceremony succeeds, and the store both ceremonies and sessions persist to (SQLite).
Index ¶
- Constants
- Variables
- func BearerToken(r *http.Request) (string, bool)
- func ClearSessionCookie(w http.ResponseWriter, secure bool)
- func IsHTTPS(r *http.Request) bool
- func RequireAdmin(next http.Handler) http.Handler
- func RequireAuth(store *Store) func(http.Handler) http.Handler
- func SessionToken(r *http.Request) (string, bool)
- func SetSessionCookie(w http.ResponseWriter, token string, secure bool)
- type APIToken
- type Credential
- type Service
- func (s *Service) BeginAddCredential(ctx context.Context, u *User) (*protocol.CredentialCreation, error)
- func (s *Service) BeginLogin(ctx context.Context, username string) (*protocol.CredentialAssertion, error)
- func (s *Service) BeginRegistration(ctx context.Context, username, displayName string) (*protocol.CredentialCreation, error)
- func (s *Service) CreateSession(ctx context.Context, u *User) (token string, err error)
- func (s *Service) FinishAddCredential(ctx context.Context, u *User, label string, r *http.Request) (*Credential, error)
- func (s *Service) FinishLogin(ctx context.Context, username string, r *http.Request) (*User, error)
- func (s *Service) FinishRegistration(ctx context.Context, username string, r *http.Request) (*User, error)
- type Store
- func (s *Store) AddCredential(ctx context.Context, userID []byte, cred webauthn.Credential) error
- func (s *Store) CreateAPIToken(ctx context.Context, userID []byte, label string, expiresAt time.Time) (raw string, tok *APIToken, err error)
- func (s *Store) CreateSession(ctx context.Context, userID []byte, ttl time.Duration) (token string, err error)
- func (s *Store) CreateUser(ctx context.Context, username, displayName string, isAdmin bool) (*User, error)
- func (s *Store) DeleteAPIToken(ctx context.Context, userID []byte, id string) error
- func (s *Store) DeleteCeremony(ctx context.Context, username, kind string) error
- func (s *Store) DeleteSession(ctx context.Context, token string) error
- func (s *Store) DeleteUnregisteredUser(ctx context.Context, username string) error
- func (s *Store) DeleteUser(ctx context.Context, userID []byte) error
- func (s *Store) GetUserByID(ctx context.Context, id []byte) (*User, error)
- func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error)
- func (s *Store) HasAnyRegisteredUser(ctx context.Context) (bool, error)
- func (s *Store) Init(ctx context.Context) error
- func (s *Store) ListAPITokens(ctx context.Context, userID []byte) ([]*APIToken, error)
- func (s *Store) ListCredentials(ctx context.Context, userID []byte) ([]*Credential, error)
- func (s *Store) ListUsers(ctx context.Context) ([]*User, error)
- func (s *Store) LoadCeremony(ctx context.Context, username, kind string) (webauthn.SessionData, error)
- func (s *Store) RemoveCredential(ctx context.Context, userID []byte, credID string) error
- func (s *Store) RevokeUser(ctx context.Context, userID []byte) error
- func (s *Store) SaveCeremony(ctx context.Context, username, kind string, session webauthn.SessionData, ...) error
- func (s *Store) SetCredentialLabel(ctx context.Context, userID []byte, credID []byte, label string) error
- func (s *Store) UpdateCredential(ctx context.Context, userID []byte, cred webauthn.Credential) error
- func (s *Store) UserForAPIToken(ctx context.Context, raw string) (*User, error)
- func (s *Store) UserForSession(ctx context.Context, token string) (*User, error)
- type User
Constants ¶
const CookieName = "forge_dashboard_session"
CookieName is the session cookie forge-dashboard sets and reads. Its value is an opaque token — Store.UserForSession is what resolves it to a user, never anything decoded client-side.
const SessionTTL = 30 * 24 * time.Hour
SessionTTL is how long an issued session stays valid.
Variables ¶
var ErrAlreadyRegistered = errors.New("auth: username already registered")
ErrAlreadyRegistered is returned by BeginRegistration when the username is already taken.
var ErrLastCredential = errors.New("auth: can't remove the account's last remaining passkey")
ErrLastCredential is returned by RemoveCredential when credID is userID's only remaining passkey — this app is WebAuthn-only with no password fallback, so deleting it would lock the account out entirely (#355).
var ErrNotFound = errors.New("auth: not found")
ErrNotFound is returned by a lookup that found nothing — a sentinel rather than a typed error, since every caller only ever needs to know which kind, not reach anything out of it.
Functions ¶
func BearerToken ¶ added in v0.64.0
BearerToken reads an "Authorization: Bearer <token>" header from r, if any — a personal API token, checked by RequireAuth as a fallback when there's no session cookie, so a script can authenticate the same way a signed-in browser does with no WebAuthn ceremony to perform.
func ClearSessionCookie ¶
func ClearSessionCookie(w http.ResponseWriter, secure bool)
ClearSessionCookie expires the session cookie immediately — logout.
func IsHTTPS ¶
IsHTTPS reports whether r itself arrived over TLS, directly or via a reverse proxy that sets the standard forwarded-proto header (Traefik, in front of this service's own deployment, terminates TLS and always sets it) — the deciding factor for whether the session cookie can carry Secure without locking out a plain-HTTP local dev run.
func RequireAdmin ¶ added in v0.9.0
RequireAdmin wraps next so it only ever runs for the designated admin — everyone else gets a 403. Compose it inside RequireAuth (RequireAuth(store)(RequireAdmin(next))), since it reads the user RequireAuth already put in context rather than looking one up itself.
func RequireAuth ¶
RequireAuth wraps next so it only ever runs for a request carrying a valid, unexpired session cookie or a valid personal API token (Authorization: Bearer <token>) — everyone else gets a 401 JSON body, never the handler underneath. Cookie checked first, Bearer as the fallback: the common case (a browser) never pays for a second lookup, and a script sending both would be unusual enough that "cookie wins" is a fine tiebreak rather than something worth its own rule.
func SessionToken ¶
SessionToken reads the session cookie from r, if any.
func SetSessionCookie ¶
func SetSessionCookie(w http.ResponseWriter, token string, secure bool)
SetSessionCookie sets token as the session cookie on w. secure mirrors whether the request itself arrived over HTTPS — see isHTTPS, called by the handler that has the *http.Request this cookie is a response to.
Types ¶
type APIToken ¶ added in v0.64.0
type APIToken struct {
ID string
Label string
CreatedAt time.Time
// ExpiresAt is always set (#356: mandatory at creation, no "never
// expires" option) — UserForAPIToken rejects a token once this
// passes, the same way it rejects one it's never heard of. A row
// from before this field existed reads back as the zero Time, which
// is always in the past, so it's rejected too rather than treated as
// permanently valid just because the schema changed underneath it.
ExpiresAt time.Time
// LastUsedAt is nil until the token first authenticates a request —
// the same "hasn't happened yet, not zero-value" shape
// dashboard.RateLimit's own nilable fields use elsewhere in this
// codebase.
LastUsedAt *time.Time
}
APIToken is a personal API token's own metadata — never the raw value, which only Store.CreateAPIToken ever returns, once, at creation time. Matches components.schemas.APIToken.
type Credential ¶ added in v0.73.0
Credential is one registered passkey's own display metadata (#355) — never the credential itself, which stays on the authenticator that created it and never reaches this server at all. ID is base64url of the underlying webauthn.Credential.ID, matching how a credential ID already appears everywhere else in a WebAuthn ceremony (excludeCredentials, allowCredentials) rather than inventing a second encoding.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service drives WebAuthn registration and login ceremonies against the configured relying party, persisting every step through Store.
func NewService ¶
NewService returns a Service.
func (*Service) BeginAddCredential ¶ added in v0.73.0
func (s *Service) BeginAddCredential(ctx context.Context, u *User) (*protocol.CredentialCreation, error)
BeginAddCredential starts a registration ceremony for an additional passkey on u's already-authenticated account (#355) — the same underlying WebAuthn ceremony BeginRegistration drives for a brand-new account, but against a user that already exists and, critically, already has credentials: the library's own excludeCredentials (built from u.WebAuthnCredentials()) is what stops a browser from letting someone re-register the same authenticator as a second, redundant entry.
func (*Service) BeginLogin ¶
func (s *Service) BeginLogin(ctx context.Context, username string) (*protocol.CredentialAssertion, error)
BeginLogin starts a login ceremony for an existing username.
func (*Service) BeginRegistration ¶
func (s *Service) BeginRegistration(ctx context.Context, username, displayName string) (*protocol.CredentialCreation, error)
BeginRegistration starts a registration ceremony for a brand-new username. Returns ErrAlreadyRegistered if that username already has an account.
func (*Service) CreateSession ¶
CreateSession issues a session token for u, ready to set as a cookie.
func (*Service) FinishAddCredential ¶ added in v0.73.0
func (s *Service) FinishAddCredential(ctx context.Context, u *User, label string, r *http.Request) (*Credential, error)
FinishAddCredential completes a ceremony BeginAddCredential started, attaching a new passkey labeled label to u's account. label is required — #355's whole point is that a nickname is prompted for at registration time, not derived from the authenticator.
func (*Service) FinishLogin ¶
FinishLogin completes a login ceremony, persisting the authenticator's bumped signature counter so a cloned authenticator is caught on a later login.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists users, their passkey credentials, in-flight WebAuthn ceremonies, and sessions. It owns no driver of its own — cmd wires up whichever database/sql driver is registered (modernc.org/sqlite) and hands Store a plain *sql.DB.
func (*Store) AddCredential ¶
AddCredential appends cred to userID's stored credentials — a brand-new passkey, from a just-finished registration ceremony.
func (*Store) CreateAPIToken ¶ added in v0.64.0
func (s *Store) CreateAPIToken(ctx context.Context, userID []byte, label string, expiresAt time.Time) (raw string, tok *APIToken, err error)
CreateAPIToken generates a brand-new personal API token for userID, labeled label, valid until expiresAt (#356 — mandatory, no "never expires" option; the caller, internal/api's handler, enforces the future/366-day-cap validation before this is ever called). raw is the only time the actual credential is ever returned — only its hash is stored, so losing it means generating a new one, the same "show once" handling a real secret needs.
func (*Store) CreateSession ¶
func (s *Store) CreateSession(ctx context.Context, userID []byte, ttl time.Duration) (token string, err error)
CreateSession issues a new opaque session token for userID, valid for ttl.
func (*Store) CreateUser ¶
func (s *Store) CreateUser(ctx context.Context, username, displayName string, isAdmin bool) (*User, error)
CreateUser registers a brand-new account with no credentials yet — AddCredential attaches the first one once the registration ceremony that follows actually succeeds.
func (*Store) DeleteAPIToken ¶ added in v0.64.0
DeleteAPIToken revokes id, scoped to userID so one user can never revoke another's token by guessing an id — idempotent, same as DeleteUser's own sessions cleanup: deleting one that's already gone, or never belonged to userID, is not an error.
func (*Store) DeleteCeremony ¶
DeleteCeremony clears the in-flight ceremony once it's finished, win or lose — it's single-use either way.
func (*Store) DeleteSession ¶
DeleteSession revokes a session token — logout, or an admin revoking another user's session.
func (*Store) DeleteUnregisteredUser ¶ added in v0.7.1
DeleteUnregisteredUser removes username's account if — and only if — it has no credentials attached: an abandoned registration (begun, never finished, so nothing was ever actually attached to the username) rather than a real account. A no-op if the account has since gained a credential, or doesn't exist at all.
func (*Store) DeleteUser ¶ added in v0.9.0
DeleteUser removes the account and every session it holds — irreversible, and the caller's job to also clean up anything outside this store (saved forge credentials, a running dashboard refresh).
func (*Store) GetUserByID ¶
GetUserByID returns ErrNotFound when no such user is registered. id is the raw WebAuthn user handle, the same bytes User.ID holds.
func (*Store) GetUserByUsername ¶
GetUserByUsername returns ErrNotFound when no such user is registered.
func (*Store) HasAnyRegisteredUser ¶ added in v0.11.0
HasAnyRegisteredUser reports whether any account has completed registration — a row with no credentials (BeginRegistration begun, never finished) doesn't count. The first username to make this true is the one that becomes admin.
func (*Store) Init ¶
Init creates the schema if it doesn't already exist. Safe to call every startup.
func (*Store) ListAPITokens ¶ added in v0.64.0
ListAPITokens returns userID's own live tokens, oldest first.
func (*Store) ListCredentials ¶ added in v0.73.0
ListCredentials returns userID's own passkeys, oldest first — driven by the account's real webauthn.Credential list (the source of truth for what can actually authenticate), not credential_metadata alone, so a credential somehow missing its label row still appears rather than silently vanishing from the list. defaultLabel fills that gap: a credential from before this feature existed reads back with no metadata row at all.
func (*Store) ListUsers ¶ added in v0.9.0
ListUsers returns every registered account, for the admin area.
func (*Store) LoadCeremony ¶
func (s *Store) LoadCeremony(ctx context.Context, username, kind string) (webauthn.SessionData, error)
LoadCeremony returns ErrNotFound when there's no in-flight ceremony of that kind for username, or it already expired.
func (*Store) RemoveCredential ¶ added in v0.73.0
RemoveCredential deletes credID from userID's account, scoped to that user so one account's own credential id can never be guessed into deleting another's — the same shape DeleteAPIToken's own scoping uses. Idempotent for an id that's unknown or already gone (matching DeleteAPIToken), but never for the account's last one: that's always ErrLastCredential, not silently skipped, so a caller (or a stale list somehow down to one) can't accidentally lock the account out.
func (*Store) RevokeUser ¶ added in v0.9.0
RevokeUser signs userID out everywhere and clears every registered passkey, without deleting the account itself — they have to register a new passkey from scratch to get back in, same as BeginRegistration reclaiming an abandoned one. Also revokes every API token: "signed out everywhere" has to mean everywhere a request can authenticate as this user, not just the session-cookie path.
func (*Store) SaveCeremony ¶
func (s *Store) SaveCeremony(ctx context.Context, username, kind string, session webauthn.SessionData, ttl time.Duration) error
SaveCeremony records the in-flight WebAuthn session data for username's registration or login attempt, replacing any earlier one of the same kind — a user can only ever be partway through one ceremony at a time.
func (*Store) SetCredentialLabel ¶ added in v0.73.0
func (s *Store) SetCredentialLabel(ctx context.Context, userID []byte, credID []byte, label string) error
SetCredentialLabel names credID (userID's own passkey) label — called once, right after AddCredential, for both the very first passkey a brand-new account registers and every one added afterward (#355), so Settings always has something to show even for an account that predates this feature having a UI of its own.
func (*Store) UpdateCredential ¶
func (s *Store) UpdateCredential(ctx context.Context, userID []byte, cred webauthn.Credential) error
UpdateCredential replaces the stored credential sharing cred's ID — FinishLogin bumps the authenticator's signature counter on every successful login, and that has to be persisted to catch a cloned authenticator on a later login.
func (*Store) UserForAPIToken ¶ added in v0.64.0
UserForAPIToken returns the user raw belongs to, or ErrNotFound — the Bearer-token equivalent of UserForSession. An expired token (or a legacy row with no recorded expiry at all — see APIToken.ExpiresAt) is rejected the same way an unknown one is, not a distinct error a caller could special-case into still trusting it (#356). Also records this as the token's most recent use, best-effort: a failure to persist that doesn't fail a request the token has already authenticated.
type User ¶
type User struct {
ID []byte
Username string
DisplayName string
IsAdmin bool
Credentials []webauthn.Credential
CreatedAt time.Time
}
User is a registered account. It implements webauthn.User directly, so it can be handed straight to the library's Begin/Finish calls with no adapter.
func UserFromContext ¶
UserFromContext returns the authenticated user a RequireAuth-wrapped handler is running for. Only ever called from inside such a handler, so the zero value (found false) means RequireAuth itself has a bug, not a condition a caller needs to handle gracefully.
func (*User) WebAuthnCredentials ¶
func (u *User) WebAuthnCredentials() []webauthn.Credential
WebAuthnCredentials satisfies webauthn.User with every passkey on file.
func (*User) WebAuthnDisplayName ¶
WebAuthnDisplayName satisfies webauthn.User with the account's display name.
func (*User) WebAuthnID ¶
WebAuthnID satisfies webauthn.User with the account's random handle.
func (*User) WebAuthnName ¶
WebAuthnName satisfies webauthn.User with the account's username.