authstore

package
v0.1.147 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: GPL-2.0, GPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package authstore re-exports the composite store interface from the standalone github.com/cplieger/auth/v2 library so subflux call sites can continue to refer to authstore.AuthStore without depending on the library's import path directly.

The auth library publishes store.Composite, composed of UserStore + SessionPersister + PasskeyStore + KeyStore + OIDCStateStore. Subflux uses the library's domain types (auth.User, auth.Session, auth.Key, auth.PasskeyCredential) directly, so AuthDB satisfies this interface with zero adapter glue.

This tiny package also keeps the historical role of breaking a test-time import cycle:

auth/_test → store/ → store/authdb/ → authstore/

authstore/ remains leaf and only imports the auth library.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AuthStore

type AuthStore = authlibstore.Composite

AuthStore is the composite store interface implemented by the concrete authdb persistence layer and consumed by auth/ and server/.

This is a type alias of github.com/cplieger/auth/v2/store.Composite — the library is the single source of truth for the contract.

type Store added in v0.1.54

type Store struct {
	// contains filtered or unexported fields
}

Store is the auth persistence layer. Durable state (users, passkeys, API keys) lives in bbolt buckets in the SAME file as the core store; ephemeral state (sessions, OIDC login states) lives only in the in-memory maps below.

The *bbolt.DB handle is SHARED with the core store (internal/boltstore), which owns it. The auth store never closes the handle — its Close only stops the background sweeper.

Bucket bootstrap is owned by the core store, which creates the auth buckets alongside the core buckets in one Update on Open.

func New added in v0.1.54

func New(db *bbolt.DB) *Store

New constructs a Store over a *bbolt.DB handle shared with the core store. It allocates the in-memory ephemeral maps and the sweeper stop channel and seeds the sweeper timings with the package defaults, but does not start the sweeper; call Open for that.

func (*Store) BatchUpdateSessionActivity added in v0.1.54

func (s *Store) BatchUpdateSessionActivity(_ context.Context, tokenHashes []string, now time.Time) error

BatchUpdateSessionActivity touches the last-activity time of many sessions in one locked pass (memory only). Consumed by the server's session-activity batcher via sessionbatch.BatchUpdater (Requirement 16.7). An empty slice is a no-op, and absent hashes are skipped, matching the old store's per-hash UPDATE semantics.

func (*Store) CleanupExpiredOIDCStates added in v0.1.54

func (s *Store) CleanupExpiredOIDCStates(_ context.Context, now time.Time, maxAge time.Duration) (int64, error)

CleanupExpiredOIDCStates evicts every OIDC state whose creation instant is strictly before now-maxAge and returns the count evicted. The strict (exclusive) cutoff matches sessions.go's CleanupExpiredSessions, so a state exactly at the cutoff is kept rather than evicted (Requirement 10.3).

func (*Store) CleanupExpiredSessions added in v0.1.54

func (s *Store) CleanupExpiredSessions(_ context.Context, now time.Time, idleTimeout, absTimeout time.Duration) (int64, error)

CleanupExpiredSessions evicts every session past its idle OR absolute timeout and returns the count evicted (Requirement 10.3). A session is expired when last_activity is strictly before now-idleTimeout OR created_at is strictly before now-absTimeout, matching the exclusive cutoff comparison of the old SQLite delete (last_activity < idleCutoff OR created_at < absCutoff). Live sessions are kept.

func (*Store) Close added in v0.1.54

func (s *Store) Close() error

Close stops the background sweeper and waits for it to exit. It is safe to call multiple times and safe to call without a prior Open: the stop channel is closed exactly once via closeOnce, so a double Close never panics, and an unopened Store has no goroutine to wait on. Close NEVER closes the shared *bbolt.DB handle — the core store owns that handle and is the only place it is closed.

func (*Store) ConsumeOIDCState added in v0.1.54

func (s *Store) ConsumeOIDCState(_ context.Context, state string) (nonce, codeVerifier, redirectURI string, err error)

ConsumeOIDCState atomically returns and removes the OIDC state for the given token (single-use). The read and delete happen under one s.mu.Lock(), so two concurrent consumes of the same state can never both succeed: exactly one observes the entry and deletes it, the other sees it already gone. A second consume of an already-consumed (or never-created, or swept) state returns errOIDCStateNotFound (Requirement 16.3, anti-replay).

func (*Store) CreateAPIKey added in v0.1.54

func (s *Store) CreateAPIKey(_ context.Context, key *auth.Key) error

CreateAPIKey inserts a new API key, rejecting a duplicate key hash with errConflict before any write (Requirement 9.3 hash uniqueness), and sets the surrogate ID on the supplied struct. CreatedAt is stamped to now when zero, mirroring the SQLite CURRENT_TIMESTAMP default. The primary row and its ix_apikey_user entry are written together in one Update, crash-durable on commit (Requirements 9.1, 9.2); on a conflict nothing is written.

func (*Store) CreateOIDCState added in v0.1.54

func (s *Store) CreateOIDCState(_ context.Context, state, nonce, codeVerifier, redirectURI string) error

CreateOIDCState records an in-flight OIDC login state in memory, keyed by the opaque state token. The stored instant is the creation time; OIDC states never touch disk (Requirement 10.1).

func (*Store) CreatePasskey added in v0.1.54

func (s *Store) CreatePasskey(_ context.Context, cred *auth.PasskeyCredential) error

CreatePasskey inserts a new WebAuthn credential, rejecting a duplicate credential id with errConflict before any write (Requirement 9.3), and sets the surrogate ID on the supplied struct. CreatedAt is stamped to now when zero, mirroring the SQLite CURRENT_TIMESTAMP default. The primary row and its ix_passkey_user entry are written together in one Update, crash-durable on commit (Requirements 9.1, 9.2).

func (*Store) CreateSession added in v0.1.54

func (s *Store) CreateSession(_ context.Context, sess *auth.Session) error

CreateSession stores a new session in memory, keyed by its token hash. A copy is stored so a later mutation of the caller's struct does not affect stored state. Sessions never touch disk (Requirement 10.1).

func (*Store) CreateUser added in v0.1.54

func (s *Store) CreateUser(_ context.Context, user *auth.User) error

CreateUser inserts a new user, enforcing case-insensitive username uniqueness and (issuer, sub) uniqueness against the index buckets before the put, and sets the surrogate ID on the supplied struct (Requirements 9.2, 9.3). A duplicate username (case-insensitive) or duplicate (issuer, sub) returns errConflict and writes nothing. CreatedAt is stamped to now when zero and UpdatedAt is set equal to it, mirroring the SQLite CURRENT_TIMESTAMP defaults.

func (*Store) DeleteAPIKey added in v0.1.54

func (s *Store) DeleteAPIKey(_ context.Context, id, userID int64) error

DeleteAPIKey removes the API key identified by surrogate id, but only when it belongs to userID (Requirement 16.4, mirroring the SQLite `DELETE ... WHERE id=? AND user_id=?`). It resolves the key hash via a user-scoped index walk, so it can only ever delete the supplied user's own key. It deletes the primary row and its ix_apikey_user entry in one Update; a non-matching (id, userID) is a no-op returning nil.

func (*Store) DeletePasskey added in v0.1.54

func (s *Store) DeletePasskey(_ context.Context, id, userID int64) error

DeletePasskey removes the passkey identified by surrogate id, but only when it belongs to userID (Requirement 16.4). Like RenamePasskey it resolves the credential id via a user-scoped index walk, so it can only ever delete the supplied user's own passkey. It deletes the primary row and its ix_passkey_user entry in one Update; a non-matching (id, userID) is a no-op returning nil.

func (*Store) DeleteSession added in v0.1.54

func (s *Store) DeleteSession(_ context.Context, tokenHash string) error

DeleteSession removes the session with the given token hash. Deleting an absent session is a no-op returning nil (matching a DELETE affecting zero rows).

func (*Store) DeleteUser added in v0.1.54

func (s *Store) DeleteUser(_ context.Context, id int64) error

DeleteUser removes the user and cascades to its passkeys, API keys, and sessions (Requirement 9.4). The durable parts — the user row, its ix_user_name / ix_user_oidc entries, and every child passkey / API key with their ix_passkey_user / ix_apikey_user entries — are deleted in one Update so the cascade is atomic and crash-durable on commit; the user's in-memory sessions are dropped after commit. Deleting a non-existent user is a no-op returning nil, matching the SQLite DELETE affecting zero rows.

func (*Store) DeleteUserSessions added in v0.1.54

func (s *Store) DeleteUserSessions(_ context.Context, userID int64, exceptHash string) error

DeleteUserSessions removes all of a user's sessions except the one identified by exceptHash (the keep-one exception used by "log out everywhere else" to keep the current session, Requirement 16.5). Pass exceptHash="" to drop all of the user's sessions (the DeleteUser cascade path); no real session carries an empty hash, so nothing is spuriously kept.

func (*Store) GetAPIKeyByHash added in v0.1.54

func (s *Store) GetAPIKeyByHash(_ context.Context, hash string) (*auth.Key, error)

GetAPIKeyByHash looks up an API key by its hash (the API-auth hot path), returning (nil, nil) when not found (matching the old store's sql.ErrNoRows -> nil mapping). Decoding fails closed (auth bucket).

func (*Store) GetPasskeyByCredentialID added in v0.1.54

func (s *Store) GetPasskeyByCredentialID(_ context.Context, credID []byte) (*auth.PasskeyCredential, error)

GetPasskeyByCredentialID looks up a passkey by its credential id (the WebAuthn login hot path), returning (nil, nil) when not found (matching the old store's sql.ErrNoRows -> nil mapping). Decoding fails closed.

func (*Store) GetPasskeysByUserID added in v0.1.54

func (s *Store) GetPasskeysByUserID(_ context.Context, userID int64) ([]auth.PasskeyCredential, error)

GetPasskeysByUserID returns all of a user's passkeys, ordered by creation time then surrogate id (matching the old store's `ORDER BY created_at`). It walks ix_passkey_user for the user and dereferences each credential id; decoding fails closed (auth bucket).

func (*Store) GetSessionByHash added in v0.1.54

func (s *Store) GetSessionByHash(_ context.Context, tokenHash string) (*auth.Session, error)

GetSessionByHash returns a copy of the session with the given token hash, or (nil, nil) when none exists (matching the old store's sql.ErrNoRows -> nil mapping). A copy is returned so callers cannot mutate the stored session through the returned pointer.

func (*Store) GetUserByEmail added in v0.1.54

func (s *Store) GetUserByEmail(_ context.Context, email string) (*auth.User, error)

GetUserByEmail looks up a user case-insensitively by email, returning (nil, nil) when not found (Requirement 16.1). email is not indexed (it is not unique in the schema), so this is a fail-closed scan of auth_users comparing the ASCII-folded email of each row.

func (*Store) GetUserByID added in v0.1.54

func (s *Store) GetUserByID(_ context.Context, id int64) (*auth.User, error)

GetUserByID looks up a user by surrogate id, returning (nil, nil) when no such user exists (matching the old store's sql.ErrNoRows -> nil mapping).

func (*Store) GetUserByOIDCSub added in v0.1.54

func (s *Store) GetUserByOIDCSub(_ context.Context, issuer, sub string) (*auth.User, error)

GetUserByOIDCSub looks up a user by (issuer, sub) via ix_user_oidc, returning (nil, nil) when not found. An empty sub never matches, mirroring the SQLite partial index keyed only on rows with oidc_sub != ”. Matching on subject alone would be unsafe: a subject is unique only within its issuer.

func (*Store) GetUserByUsername added in v0.1.54

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

GetUserByUsername looks up a user case-insensitively by username via ix_user_name, returning (nil, nil) when not found (Requirement 16.1).

func (*Store) ListAPIKeysByUserID added in v0.1.54

func (s *Store) ListAPIKeysByUserID(_ context.Context, userID int64) ([]auth.Key, error)

ListAPIKeysByUserID returns all API keys for a user, ordered by creation date descending then surrogate id descending (newest first), matching the old store's `ORDER BY created_at DESC`. It walks ix_apikey_user for the user and dereferences each key hash; decoding fails closed (auth bucket).

func (*Store) ListUsers added in v0.1.54

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

ListUsers returns all users ordered by username (case-insensitive, then by id), matching the old store's `ORDER BY username` over a COLLATE NOCASE column. Decoding fails closed (auth bucket).

func (*Store) Open added in v0.1.54

func (s *Store) Open() error

Open starts the single background sweeper goroutine. It evicts expired sessions and OIDC states once per sweep interval until Close signals stop (Requirements 10.3, 10.4). Open is idempotent: a second call while the sweeper is already running is a no-op, so exactly one goroutine ever runs.

func (*Store) PasskeyCountForUser added in v0.1.54

func (s *Store) PasskeyCountForUser(_ context.Context, userID int64) (int, error)

PasskeyCountForUser returns the number of passkeys registered for a user (Requirement 16.7). It is a key-only prefix count over ix_passkey_user with no primary dereference.

func (*Store) RenamePasskey added in v0.1.54

func (s *Store) RenamePasskey(_ context.Context, id, userID int64, name string) error

RenamePasskey sets the friendly name of the passkey identified by surrogate id, but only when it belongs to userID (Requirement 16.4). It resolves the credential id by a user-scoped walk of ix_passkey_user, so a passkey owned by a different user is never visited and cannot be renamed. A non-matching (id, userID) is a no-op returning nil, matching the SQLite UPDATE affecting zero rows.

func (*Store) SetSessionTimeouts added in v0.1.134

func (s *Store) SetSessionTimeouts(idle, absolute time.Duration)

SetSessionTimeouts overrides the sweeper's session idle/absolute timeouts with configured values. The composition root calls it after loading config, and the server's hot reload calls it again when the settings change, so the sweeper evicts on the SAME cutoffs the request-path session validator enforces (sweeper eviction of an in-memory session is a hard logout, so a shorter sweeper timeout would silently cap a longer configured one). Non-positive values leave the current timeout unchanged. Safe for concurrent use with a running sweeper.

func (*Store) UpdatePasskeyAfterLogin added in v0.1.54

func (s *Store) UpdatePasskeyAfterLogin(_ context.Context, credID []byte, signCount uint32, flags auth.PasskeyFlags) error

UpdatePasskeyAfterLogin persists the post-login authenticator flags and a durable, MONOTONIC sign_count: the stored value is set to max(stored, incoming) so a lower incoming counter (a replay or a cloned authenticator) can never regress it and defeat clone detection (Requirement 9.5, CVE-2023-45669). A missing credential is a no-op returning nil, matching the SQLite UPDATE that affects zero rows. The single-key Put is crash-durable on commit.

func (*Store) UpdateSessionActivity added in v0.1.54

func (s *Store) UpdateSessionActivity(_ context.Context, tokenHash string, now time.Time) error

UpdateSessionActivity touches a session's last-activity time (memory only, Requirement 10.2). An update for an absent session is a no-op returning nil, matching the SQLite UPDATE that affects zero rows.

func (*Store) UpdateUser added in v0.1.54

func (s *Store) UpdateUser(_ context.Context, user *auth.User) error

UpdateUser updates all mutable fields for the user, re-keying ix_user_name and ix_user_oidc when the username or (issuer, sub) change (delete-old, add-new), and re-checking uniqueness for the new keys (Requirement 9.3). It preserves the original CreatedAt and stamps UpdatedAt to now. An update for a non-existent id is a no-op returning nil, matching the SQLite UPDATE that affects zero rows.

func (*Store) UserCount added in v0.1.54

func (s *Store) UserCount(_ context.Context) (int, error)

UserCount returns the number of user accounts, used for first-boot detection (Requirement 16.2). The auth_users bucket is small, so this is a cursor count rather than a maintained counter.

Directories

Path Synopsis
Package authstoretest provides a shared, engine-agnostic behavioral contract suite for the composite auth store (cplieger/auth/store.Composite).
Package authstoretest provides a shared, engine-agnostic behavioral contract suite for the composite auth store (cplieger/auth/store.Composite).

Jump to

Keyboard shortcuts

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