apitoken

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package apitoken is the scoped API-token model (plan §17.1): the keystone integration primitive — a new front DOOR, never a new trust PATH. A token is strictly LESS privileged than a browser session: its scope grammar has NO SYMBOL for a Tier-1 capability (key/allowlist/bind), nor for reveal-secret / raw-Caddy / setup-run / mint-another-token — those are simply not expressible, the same structural impossibility that makes the Tier-2/3 files dashboard-writable.

Tokens are minted ONLY over the CLI/SSH (web minting would be a privesc surface), stored argon2id-hashed (the id scopes the lookup so exactly one verify runs per request), carry a mandatory expiry + a non-empty CIDR set (a token is an auth factor, NOT an allowlist bypass), and are constant-time compared + revocable.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("apitoken: not found")

ErrNotFound means no token row matched the id (an unknown/expired/garbage id looks identical to a wrong secret to the caller — no enumeration oracle).

Functions

func CIDRUnion

func CIDRUnion(records []Record, now int64) []netip.Prefix

CIDRUnion returns the union of every ACTIVE token's CIDR set — the precomputed set the IP allowlist checks the unspoofable peer against BEFORE any bearer is parsed (so a token id can't be an enumeration oracle and an unknown bearer can't trigger a DB lookup before the peer is admitted).

func ParseCIDRs

func ParseCIDRs(cidrs []string) ([]netip.Prefix, error)

ParseCIDRs validates a non-empty CIDR set, rejecting a 0-bit prefix (0.0.0.0/0 or ::/0 would turn the bounded CI exception into a global allowlist bypass).

func ParseScopes

func ParseScopes(scopes []string) ([]string, error)

ParseScopes validates every requested scope, rejecting the empty set and any unknown/over-broad symbol.

func SplitBearer

func SplitBearer(bearer string) (id, secret string, ok bool)

SplitBearer extracts the (id, secret) from a bearer, or ok=false on any malformed input. It does NOT hit the DB — the caller looks up by id, so a malformed/unknown token is never an unauthenticated DB-lookup oracle.

func ValidID

func ValidID(s string) bool

ValidID reports whether s is a well-formed token id (for the store lookup).

func ValidScope

func ValidScope(s string) bool

ValidScope reports whether s is a permissible scope. The ONLY shapes are the read scopes and deploy:write:<slug> — there is deliberately no grammar for a Tier-1 / reveal / caddy / setup / mint capability, so a token can never express more than the API exposes.

Types

type Minted

type Minted struct {
	Plaintext string
	Record    Record
}

Minted is the freshly-minted token: the one-time plaintext (shown ONCE) + the stored Record.

func Mint

func Mint(scopes, cidrs []string, ttl time.Duration, now time.Time) (Minted, error)

Mint creates a token. ttl must be positive (mandatory expiry); scopes + cidrs are validated. The plaintext "hmt_<id>_<secret>" is returned ONCE; only the hash is stored.

type Record

type Record struct {
	ID        string
	Hash      string // argon2id-encoded
	Scopes    []string
	CIDRs     []netip.Prefix
	ExpiresAt int64
	Revoked   bool
}

Record is one stored token (no plaintext secret — only the argon2id hash).

func (Record) Allows

func (r Record) Allows(scope string) bool

Allows reports whether the token's scopes permit the requested capability.

func (Record) VerifySecret

func (r Record) VerifySecret(secret string, now int64) bool

VerifySecret constant-time-checks the presented secret against the record and its active window. The argon2id verify ALWAYS runs first (even for an expired/revoked record) so the cost is identical whether or not the token is active — an expired/revoked id must not be distinguishable by latency from a live one (the active() check used to short-circuit before argon2, which leaked a revocation/expiry timing oracle; M19 review). The id already selected the single record, so the verify still runs at most ONCE per request.

type Store

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

Store persists api_tokens. The id selects exactly one row, so a request runs at most one argon2id verify; minting is CLI-only (no web path constructs a Store write for token creation).

func NewStore

func NewStore(db *store.DB) *Store

NewStore builds a Store.

func (*Store) ActiveCIDRUnion

func (s *Store) ActiveCIDRUnion(ctx context.Context, now time.Time) ([]netip.Prefix, error)

ActiveCIDRUnion returns the union of every currently-active token's CIDR set — the precomputed allowlist addition the IP gate checks the peer against BEFORE any bearer is parsed.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id string) (Record, error)

Get loads one token by id. It returns ErrNotFound for any id that is malformed or absent — the caller cannot distinguish "no such token" from "wrong secret".

func (*Store) Insert

func (s *Store) Insert(ctx context.Context, r Record, label string, now time.Time) error

Insert persists a freshly-minted Record (the plaintext is never stored). label is an informational operator note.

func (*Store) List

func (s *Store) List(ctx context.Context) ([]Record, error)

List returns all tokens (for the CLI listing + the CIDR-union recompute on startup/SIGHUP).

func (*Store) Revoke

func (s *Store) Revoke(ctx context.Context, id string) error

Revoke marks a token revoked (idempotent). A revoked token fails active() and is excluded from the CIDR union on the next recompute.

func (*Store) RevokeAppScoped

func (s *Store) RevokeAppScoped(ctx context.Context, slug string) (int64, error)

RevokeAppScoped revokes any token whose ONLY capability is deploying THIS app (scopes == "deploy:write:<slug>"), used by the app-delete teardown. It deliberately leaves multi-scope tokens alone — yanking a token that also serves other apps would be collateral damage, and a leftover scope for a now-gone app is inert (its deploy route no longer resolves). Returns the number of tokens revoked.

func (*Store) TouchLastUsed

func (s *Store) TouchLastUsed(ctx context.Context, id string, now time.Time)

TouchLastUsed records a best-effort last-use timestamp (never gates auth — a failure here must not deny a valid request, so callers ignore the error).

Jump to

Keyboard shortcuts

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