auth

package
v0.76.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 18, 2026 License: AGPL-3.0 Imports: 16 Imported by: 0

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

View Source
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.

View Source
const SessionTTL = 30 * 24 * time.Hour

SessionTTL is how long an issued session stays valid.

Variables

View Source
var ErrAlreadyRegistered = errors.New("auth: username already registered")

ErrAlreadyRegistered is returned by BeginRegistration when the username is already taken.

View Source
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).

View Source
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

func BearerToken(r *http.Request) (string, bool)

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

func IsHTTPS(r *http.Request) bool

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

func RequireAdmin(next http.Handler) http.Handler

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

func RequireAuth(store *Store) func(http.Handler) http.Handler

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

func SessionToken(r *http.Request) (string, bool)

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

type Credential struct {
	ID        string
	Label     string
	CreatedAt time.Time
}

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

func NewService(wa *webauthn.WebAuthn, store *Store) *Service

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

func (s *Service) CreateSession(ctx context.Context, u *User) (token string, err error)

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

func (s *Service) FinishLogin(ctx context.Context, username string, r *http.Request) (*User, error)

FinishLogin completes a login ceremony, persisting the authenticator's bumped signature counter so a cloned authenticator is caught on a later login.

func (*Service) FinishRegistration

func (s *Service) FinishRegistration(ctx context.Context, username string, r *http.Request) (*User, error)

FinishRegistration completes a registration ceremony begun for username, attaching the new passkey to the account BeginRegistration already created.

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 NewStore

func NewStore(db *sql.DB) *Store

NewStore wraps db. Init must be called once before any other method.

func (*Store) AddCredential

func (s *Store) AddCredential(ctx context.Context, userID []byte, cred webauthn.Credential) error

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

func (s *Store) DeleteAPIToken(ctx context.Context, userID []byte, id string) error

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

func (s *Store) DeleteCeremony(ctx context.Context, username, kind string) error

DeleteCeremony clears the in-flight ceremony once it's finished, win or lose — it's single-use either way.

func (*Store) DeleteSession

func (s *Store) DeleteSession(ctx context.Context, token string) error

DeleteSession revokes a session token — logout, or an admin revoking another user's session.

func (*Store) DeleteUnregisteredUser added in v0.7.1

func (s *Store) DeleteUnregisteredUser(ctx context.Context, username string) error

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

func (s *Store) DeleteUser(ctx context.Context, userID []byte) error

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

func (s *Store) GetUserByID(ctx context.Context, id []byte) (*User, error)

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

func (s *Store) GetUserByUsername(ctx context.Context, username string) (*User, error)

GetUserByUsername returns ErrNotFound when no such user is registered.

func (*Store) HasAnyRegisteredUser added in v0.11.0

func (s *Store) HasAnyRegisteredUser(ctx context.Context) (bool, error)

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

func (s *Store) Init(ctx context.Context) error

Init creates the schema if it doesn't already exist. Safe to call every startup.

func (*Store) ListAPITokens added in v0.64.0

func (s *Store) ListAPITokens(ctx context.Context, userID []byte) ([]*APIToken, error)

ListAPITokens returns userID's own live tokens, oldest first.

func (*Store) ListCredentials added in v0.73.0

func (s *Store) ListCredentials(ctx context.Context, userID []byte) ([]*Credential, error)

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

func (s *Store) ListUsers(ctx context.Context) ([]*User, error)

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

func (s *Store) RemoveCredential(ctx context.Context, userID []byte, credID string) error

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

func (s *Store) RevokeUser(ctx context.Context, userID []byte) error

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

func (s *Store) UserForAPIToken(ctx context.Context, raw string) (*User, error)

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.

func (*Store) UserForSession

func (s *Store) UserForSession(ctx context.Context, token string) (*User, error)

UserForSession returns the user a valid, unexpired session token belongs to, or ErrNotFound.

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

func UserFromContext(ctx context.Context) (*User, bool)

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

func (u *User) WebAuthnDisplayName() string

WebAuthnDisplayName satisfies webauthn.User with the account's display name.

func (*User) WebAuthnID

func (u *User) WebAuthnID() []byte

WebAuthnID satisfies webauthn.User with the account's random handle.

func (*User) WebAuthnName

func (u *User) WebAuthnName() string

WebAuthnName satisfies webauthn.User with the account's username.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL