token

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package token manages model-owned personal and renewable access credentials. Persistence adapters own atomic refresh/reuse behavior. Guards resolve current models and use auth.AccessScopes as a credential ceiling, never as permissions.

Index

Constants

View Source
const MaxFamilyTickets = 64
View Source
const MaxLifetime = 365 * 24 * time.Hour
View Source
const MaxPrefixBytes = 16

MaxPrefixBytes bounds Config.Prefix.

View Source
const MaxPruneFamilies = 1024
View Source
const MaxRotations = 4096
View Source
const MaxTicketLifetime = 5 * time.Minute

MaxTicketLifetime and MaxFamilyTickets bound single-use handshake tickets.

View Source
const MaxTokens = 1024

Variables

This section is empty.

Functions

This section is empty.

Types

type Address

type Address struct {
	Namespace keyspace.Namespace
	Guard     auth.GuardName
	Provider  auth.ProviderName
	Model     string
}

Address binds persisted tokens to an application, environment, model/provider and guard. It is an infrastructure boundary, not an application identity API.

func (Address) Key

func (a Address) Key() (string, error)

func (Address) SubjectKey

func (a Address) SubjectKey(identity model.Identity) (string, error)

func (Address) Validate

func (a Address) Validate() error

type Backend

Backend is an authoritative hash-only store. Every mutation owns one transaction and runs without automatic retries. Stable subject locks serialize creation, revocation and refresh. Sample server time after the relevant lock is acquired. Lookup only accepts the family's current access generation. Refresh atomically consumes the current generation and creates its successor, preserving the family lifetime and grant. A consumed refresh hash revokes the entire family, committing that revocation before returning an omitted result. Never roll it back merely because the public refresh operation is rejected. At the rotation limit, revoke rather than accumulate unbounded history. Retain consumed generations until the family expires or is revoked. Concurrent use may yield one success followed by family revocation; no replay grace period is implied. An unknown commit outcome returns an error, never a partial record or new secret.

type CheckedBackend

type CheckedBackend interface {
	CreateChecked(context.Context, Address, Creation, func(context.Context, *database.Tx) error) (Record, error)
}

CheckedBackend verifies a proof's current model in the SAME transaction as creation, before acquiring credential subject locks. The check must lock the model and may run once only. A failed check creates nothing; never retry it.

type CompletionBackend

type CompletionBackend interface {
	CheckedBackend
	ConsumePendingIn(context.Context, *database.Tx, Address, Record) error
	CreateBefore(context.Context, Address, Creation, temporal.DateTime, func(context.Context, *database.Tx) error) (Record, error)
}

CompletionBackend can replace a pending-MFA credential in checked creation. ConsumePendingIn joins the supplied transaction, locks and revalidates the exact pending record/hash/identity/expiry, and deletes it provisionally. It rejects full credentials. CreateBefore runs the check and creates through the ordinary creation path, then rechecks the pending deadline before commit. Both methods preserve rollback, never retry, and return no secret on failure.

type Config

type Config struct {
	Namespace            keyspace.Namespace
	Personal             Lifetime
	Renewable            Lifetime
	Challenge            Lifetime
	MaxPerSubject        int
	MaxPendingPerSubject int
	Limit                auth.LimitPolicy
	MaxRotations         int
	AccessGrace          time.Duration
	Prefix               string
	MaxConcurrent        int
	Timeout              time.Duration
	TicketLifetime       time.Duration
	MaxTicketsPerFamily  int
}

Config bounds the store and fixes new tokens' lifetimes. MaxPerSubject caps a subject's live personal/renewable families per guard; Limit selects rejection with auth.CredentialLimit (default) or evicting the oldest. Pending-MFA challenge tokens only count toward MaxPendingPerSubject and always replace the oldest pending one. Expired families never count. AccessGrace keeps a refreshed family's previous access token valid for this long after the refresh (never beyond its own expiry), so requests already in flight with it do not fail; zero disables it. Prefix is prepended to newly issued access/refresh secrets for secret scanning (for example "myapp_"); secrets with or without a prefix are accepted, since only the random part is hashed. TicketLifetime bounds a single-use handshake ticket from IssueTicket (between one second and MaxTicketLifetime; zero disables tickets), and MaxTicketsPerFamily caps one family's unexpired tickets, dropping the oldest.

func DefaultConfig

func DefaultConfig(namespace keyspace.Namespace) Config

func (Config) Validate

func (c Config) Validate() error

type Creation

type Creation struct {
	ID             model.ID[Record]
	Subject        model.Identity
	Name           string
	Scopes         []auth.AccessScopeName
	Mode           Mode
	Assurance      auth.Assurance
	AccessHash     Digest
	RefreshHash    value.Optional[Digest]
	Lifetime       Lifetime
	RotationLimit  uint32
	Maximum        int
	PendingMaximum int
	Limit          auth.LimitPolicy
	Device         auth.Device
}

Creation is a trusted, bounded issuance request. Backend.Create enforces Maximum live personal/renewable families and PendingMaximum live challenge families, each counting only its own kind, while holding the stable subject lock. Limit selects rejection or eviction for full families; challenges always evict their oldest. Expired families never count.

func (Creation) At

func (c Creation) At(address Address, now time.Time) (Record, error)

func (Creation) Validate

func (c Creation) Validate(address Address) error

type Digest

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

Digest is a token-store hash. It is distinct from a session digest and cannot substitute for an opaque credential supplied at a transport boundary.

func HashSecret

func HashSecret(value secret.String) (Digest, error)

HashSecret accepts the canonical 256-bit random encoding, optionally preceded by a Config.Prefix-style prefix. Only the random part is hashed, so changing the configured prefix never invalidates existing tokens.

func ParseDigest

func ParseDigest(text string) (Digest, error)

func (Digest) Equal

func (d Digest) Equal(other Digest) bool

func (Digest) Format

func (Digest) Format(s fmt.State, _ rune)

func (Digest) Hex

func (d Digest) Hex() string

func (Digest) IsZero

func (d Digest) IsZero() bool

type GraceBackend

type GraceBackend interface {
	LookupWithin(context.Context, Address, Digest, time.Duration) (value.Optional[Record], error)
}

GraceBackend additionally accepts the generation immediately before the family's current one, with Record.SupersededAt set, while the backend's clock is within grace of the successor's issue time and before its own access expiry. It never accepts older generations, and refresh-token reuse still revokes the family.

type ID

type ID[M any] struct {
	// contains filtered or unexported fields
}

ID is a model-owned token family identifier, never an authentication secret. It remains stable across refresh. Explicit subject authorization is still needed.

func ParseID

func ParseID[M any](text string) (ID[M], error)

func (ID[M]) IsZero

func (id ID[M]) IsZero() bool

func (ID[M]) MarshalText

func (id ID[M]) MarshalText() ([]byte, error)

func (ID[M]) String

func (id ID[M]) String() string

type Info

type Info[M, K any] struct {
	// contains filtered or unexported fields
}

Info exposes immutable metadata and a concrete stored model reference. It contains neither raw credentials nor hashes and does not authorize its caller.

func (Info[M, K]) AccessExpiresAt

func (i Info[M, K]) AccessExpiresAt() temporal.DateTime

func (Info[M, K]) Assurance

func (i Info[M, K]) Assurance() auth.Assurance

func (Info[M, K]) CreatedAt

func (i Info[M, K]) CreatedAt() temporal.DateTime

func (Info[M, K]) Device

func (i Info[M, K]) Device() auth.Device

Device is the client metadata captured when the token family was issued.

func (Info[M, K]) ExpiresAt

func (i Info[M, K]) ExpiresAt() temporal.DateTime

func (Info[M, K]) Format

func (Info[M, K]) Format(s fmt.State, _ rune)

func (Info[M, K]) Generation

func (i Info[M, K]) Generation() uint32

func (Info[M, K]) ID

func (i Info[M, K]) ID() ID[M]

func (Info[M, K]) IssuedAt

func (i Info[M, K]) IssuedAt() temporal.DateTime

func (Info[M, K]) LastSeenAt

func (i Info[M, K]) LastSeenAt() temporal.DateTime

func (Info[M, K]) Mode

func (i Info[M, K]) Mode() Mode

func (Info[M, K]) Name

func (i Info[M, K]) Name() string

func (Info[M, K]) RefreshExpiresAt

func (i Info[M, K]) RefreshExpiresAt() value.Optional[temporal.DateTime]

func (Info[M, K]) Scopes

func (i Info[M, K]) Scopes() auth.AccessScopes[M]

func (Info[M, K]) Subject

func (i Info[M, K]) Subject() model.Reference[M, K]

type IssueOptions

type IssueOptions[M any] struct {
	Name    string
	Scopes  auth.AccessScopes[M]
	Refresh bool
}

IssueOptions is chosen by trusted credential verification, not mass-assigned from a login DTO. Requested scopes must fit this binding's declared ceiling. A scoped proof can only issue a subset of its own grants.

type Issued

type Issued[M, K any] struct {
	// contains filtered or unexported fields
}

Issued retains raw credentials for explicit transport delivery only. Routine formatting and JSON do not disclose them. Nonrenewable tokens omit refresh.

func (Issued[M, K]) AccessSecret

func (i Issued[M, K]) AccessSecret() secret.String

func (Issued[M, K]) Format

func (Issued[M, K]) Format(s fmt.State, _ rune)

func (Issued[M, K]) Info

func (i Issued[M, K]) Info() Info[M, K]

func (Issued[M, K]) RefreshSecret

func (i Issued[M, K]) RefreshSecret() value.Optional[secret.String]

type Lifetime

type Lifetime struct{ Access, RefreshIdle, Absolute time.Duration }

Lifetime preserves a family's absolute expiry across every refresh. Access activity never extends refresh idle expiry. Only successful refresh does so.

func (Lifetime) Validate

func (l Lifetime) Validate(mode Mode) error

type Lifetimes

type Lifetimes struct{ Personal, Renewable, Challenge Lifetime }

Lifetimes are one guard's issuance lifetimes by mode.

type Mode

type Mode uint8

Mode distinguishes nonrenewable personal credentials, renewable pairs and short-lived MFA challenges. A challenge never carries ordinary access scopes.

const (
	Personal Mode = iota + 1
	Renewable
	Challenge
)

type Option

type Option func(*options) error

Option configures one token binding at construction.

func WithLifetimes

func WithLifetimes(lifetimes Lifetimes) Option

WithLifetimes shortens this guard's issuance lifetimes, for example admin tokens that expire sooner than user tokens issued from the same store. A zero Lifetime keeps the store's for that mode. The store's lifetimes are the ceiling: no duration may exceed the store's for its mode, so the configured store bounds every guard. Each lifetime must be valid for its mode, and the renewable access lifetime must still cover Config.AccessGrace. A family keeps the lifetime it was issued with across refreshes.

type Record

type Record struct {
	ID               model.ID[Record]
	Address          Address
	Subject          model.Identity
	Name             string
	Scopes           []auth.AccessScopeName
	Mode             Mode
	Assurance        auth.Assurance
	AccessHash       Digest
	RefreshHash      value.Optional[Digest]
	Lifetime         Lifetime
	RotationLimit    uint32
	Generation       uint32
	CreatedAt        temporal.DateTime
	IssuedAt         temporal.DateTime
	LastSeenAt       temporal.DateTime
	AccessExpiresAt  temporal.DateTime
	RefreshExpiresAt value.Optional[temporal.DateTime]
	ExpiresAt        temporal.DateTime
	Device           auth.Device
	// SupersededAt is set only when Lookup returns the generation immediately
	// before the family's current one: the successor's issue time. Its access
	// secret authenticates only within Config.AccessGrace of that time.
	SupersededAt value.Optional[temporal.DateTime] `json:",omitzero"`
}

Record is the explicit adapter boundary for the current token generation. It contains no raw secret. Adapters transfer ownership of the bounded scope slice; typed application code receives immutable Info instead of this record.

func (Record) Format

func (Record) Format(s fmt.State, _ rune)

func (Record) Live

func (r Record) Live(now time.Time) bool

func (Record) LiveAccess

func (r Record) LiveAccess(now time.Time) bool

func (Record) LiveAccessWithin

func (r Record) LiveAccessWithin(now time.Time, grace time.Duration) bool

LiveAccessWithin reports access validity, allowing a superseded generation only until grace after its successor was issued (never past its own expiry).

func (Record) LiveRefresh

func (r Record) LiveRefresh(now time.Time) bool

func (Record) Refreshed

func (r Record) Refreshed(now time.Time, access, refresh Digest) (Record, bool, error)

Refreshed computes the successor of a current live generation. The adapter must first lock and re-read the family, detect consumed-token reuse, and ensure this record is current. False means no successor; it must never revive a dead family.

func (Record) Validate

func (r Record) Validate(address Address) error

type RefreshRevocationBackend

type RefreshRevocationBackend interface {
	RevokeRefresh(context.Context, Address, Digest) (value.Optional[model.Identity], error)
}

RefreshRevocationBackend revokes the family that issued a refresh digest, whether it is the current or previous generation's or a consumed one, under the subject lock shared with issuance. It returns the removed family's subject, omitted when no family matches. Expired families are removed too.

type SelectiveBackend

type SelectiveBackend interface {
	RevokeOthers(context.Context, Address, model.Identity, model.ID[Record]) (uint64, error)
}

SelectiveBackend revokes every family of a subject in this address except keep, under the subject lock shared with issuance, and returns the count.

type Store

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

Store borrows a backend and shares bounded operation capacity across bindings. The application owns the backend pool's lifecycle.

func NewStore

func NewStore(backend Backend, config Config) (*Store, error)

type Ticket

type Ticket[M, K any] struct {
	// contains filtered or unexported fields
}

Ticket is a single-use handshake credential for the token family that authenticated its issuing request, for transports that cannot send an Authorization header, such as a browser WebSocket. Secret() is an explicit transport boundary; routine formatting and JSON never disclose it.

func (Ticket[M, K]) ExpiresAt

func (t Ticket[M, K]) ExpiresAt() temporal.DateTime

func (Ticket[M, K]) Family

func (t Ticket[M, K]) Family() ID[M]

func (Ticket[M, K]) Format

func (Ticket[M, K]) Format(s fmt.State, _ rune)

func (Ticket[M, K]) Secret

func (t Ticket[M, K]) Secret() secret.String

type TicketBackend

TicketBackend stores single-use handshake tickets as hashes. IssueTicket stores one for the subject's live family, first dropping that family's expired tickets and the oldest beyond max-1, and reports omitted expiry when the family is no longer live. RedeemTicket deletes the ticket in the statement that reads it, so a ticket redeems at most once; an unknown or expired ticket is omitted. Revoking, refreshing past or expiring a family never revives a ticket, and deleting a family deletes its tickets. LookupFamily reads a family's current generation for a re-check and is omitted unless the family is live. PruneTickets deletes at most limit expired tickets of the address.

type TicketRecord

type TicketRecord struct {
	Family    model.ID[Record]
	Subject   model.Identity
	ExpiresAt temporal.DateTime
}

TicketRecord is a redeemed ticket: the family and subject it was issued for.

type Tokens

type Tokens[M model.Identifiable, K any] struct {
	// contains filtered or unexported fields
}

Tokens binds one stored model/provider/guard/source and declared scope ceiling. Its guard is constructed once; persisted tokens cannot be rebound by extracting an unscoped strategy. Construction performs no I/O. It issues with the store's lifetimes unless WithLifetimes shortens them for this guard.

func New

func New[M model.Identifiable, K any](store *Store, name auth.GuardName, provider auth.Provider[M, K], source auth.CredentialName, allowed auth.AccessScopes[M], opts ...Option) (*Tokens[M, K], error)

func (*Tokens[M, K]) CompleteMFA

func (t *Tokens[M, K]) CompleteMFA(ctx context.Context, raw secret.String, factor auth.SecondFactor[M, K], options IssueOptions[M]) (Issued[M, K], error)

CompleteMFA replaces a live pending credential from THIS guard with one fresh authenticated credential. The lookup is only a locator; checked creation revalidates and consumes it with the current model and factor in one transaction. Full, expired, revoked, wrong-guard or already-consumed credentials are rejected. No reusable authenticated proof escapes, and failures return no new secrets. Options belong to the server's login policy, not unvalidated client claims.

func (*Tokens[M, K]) Current

func (t *Tokens[M, K]) Current(ctx context.Context) (Info[M, K], error)

Current returns the metadata of the token that authenticated this guard in the current auth scope, including its effective scopes (for example to mark "this device" in List results by comparing IDs). It reuses the scope's verification; no second lookup runs.

func (*Tokens[M, K]) CurrentProof

func (t *Tokens[M, K]) CurrentProof(ctx context.Context) (auth.Proof[M, K], error)

CurrentProof returns a proof for the current request's model that retains the presented token's scope grants. Issuing a token from it can only narrow those grants, unlike auth.NewProof, which is unrestricted.

func (*Tokens[M, K]) Guard

func (t *Tokens[M, K]) Guard() auth.Guard[M]

func (*Tokens[M, K]) Issue

func (t *Tokens[M, K]) Issue(ctx context.Context, proof auth.Proof[M, K], options IssueOptions[M]) (Issued[M, K], error)

Issue persists a trusted verified proof. It does not verify a password or a submitted identity. Requested scopes cannot exceed this binding's declaration or the input proof's grant. Pending MFA has fixed expiry and cannot refresh.

func (*Tokens[M, K]) IssueTicket

func (t *Tokens[M, K]) IssueTicket(ctx context.Context) (Ticket[M, K], error)

IssueTicket mints a single-use ticket for the token family that authenticated this guard in the current request. Only its hash is stored. It expires after Config.TicketLifetime, the first redemption consumes it, and revoking the family deletes it. Mount it behind this binding's guard and deliver it through a no-store credential response; never put it in a URL.

func (*Tokens[M, K]) List

func (t *Tokens[M, K]) List(ctx context.Context, reference model.Reference[M, K]) ([]Info[M, K], error)

List returns bounded current-family metadata. An expired access credential can still have a live refresh grant; callers can inspect both expiry values.

func (*Tokens[M, K]) Logout

func (t *Tokens[M, K]) Logout(ctx context.Context, access secret.String) (bool, error)

Logout revokes the family of a presented access token and reports EventLogout when it removed one.

func (*Tokens[M, K]) LogoutRefresh

func (t *Tokens[M, K]) LogoutRefresh(ctx context.Context, refresh secret.String) (bool, error)

LogoutRefresh revokes the family of a presented refresh secret, for a logout authenticated by the refresh credential itself, such as a browser's HttpOnly refresh cookie: an expired access token does not prevent it. A current, previous or consumed refresh secret of the family revokes it, as replaying one would; an unknown one revokes nothing and is not an error, so logout stays idempotent. A malformed secret is Unauthenticated, as with Revoke. It reports EventLogout when it removed a family.

func (*Tokens[M, K]) Prune

func (t *Tokens[M, K]) Prune(ctx context.Context, limit int) (uint64, error)

Prune removes at most limit expired families with their bounded generation history. Stable subject rows remain for issuance/revocation serialization.

func (*Tokens[M, K]) PruneExpired

func (t *Tokens[M, K]) PruneExpired(ctx context.Context, batch, maxBatches int) (uint64, error)

PruneExpired prunes expired token families of this guard in batches of batch families, stopping after a short batch or maxBatches, and reports the families removed. Schedule it from the application calendar or the configured housekeeping schedule; the auth package does not depend on the scheduler.

func (*Tokens[M, K]) RedeemTicket

func (t *Tokens[M, K]) RedeemTicket(ctx context.Context, raw secret.String) (value.Optional[auth.BoundCredential], error)

RedeemTicket consumes a ticket issued by this binding. A redeemed ticket becomes a bound credential for TicketSource, which each later auth scope re-verifies against the live token family, its subject's current eligibility and the binding's scope ceiling. An unknown, expired or already redeemed ticket is omitted; the caller treats that as unauthenticated.

func (*Tokens[M, K]) Refresh

func (t *Tokens[M, K]) Refresh(ctx context.Context, raw secret.String) (Issued[M, K], error)

Refresh consumes one renewable secret atomically. Reuse of a consumed secret revokes the entire family, including a concurrent successful successor. Clients must serialize refresh and reauthenticate after an uncertain/failed outcome; the framework never retries or returns partial credentials.

func (*Tokens[M, K]) Revocation

func (t *Tokens[M, K]) Revocation() (auth.Revocation[M, K], error)

Revocation contributes this bound guard to auth.NewRevocations. Construct the group once and pass its Invalidate method to password-reset/domain callbacks.

func (*Tokens[M, K]) Revoke

func (t *Tokens[M, K]) Revoke(ctx context.Context, access secret.String) (bool, error)

func (*Tokens[M, K]) RevokeAll

func (t *Tokens[M, K]) RevokeAll(ctx context.Context, reference model.Reference[M, K]) (uint64, error)

func (*Tokens[M, K]) RevokeAllIn

func (t *Tokens[M, K]) RevokeAllIn(ctx context.Context, tx *database.Tx, reference model.Reference[M, K]) (uint64, error)

RevokeAllIn joins a caller's transaction. Its count is provisional and the caller must propagate errors. It never acquires a model lock; credential-change orchestration must lock that model first. Unsupported adapters fail closed.

func (*Tokens[M, K]) RevokeCurrent

func (t *Tokens[M, K]) RevokeCurrent(ctx context.Context) (bool, error)

RevokeCurrent revokes the family of the token that authenticated this request (API logout on an authenticated route). It reports EventLogout when it removed the family.

func (*Tokens[M, K]) RevokeID

func (t *Tokens[M, K]) RevokeID(ctx context.Context, reference model.Reference[M, K], id ID[M]) (bool, error)

RevokeID removes one family belonging to this exact model subject and guard. Authorize the caller before supplying a subject; the ID is not authority.

func (*Tokens[M, K]) RevokeOthers

func (t *Tokens[M, K]) RevokeOthers(ctx context.Context) (uint64, error)

RevokeOthers revokes every other token family of the current subject in this guard while keeping the current one ("log out other devices"). It runs under the subject lock shared with issuance and reports EventOtherDevicesLoggedOut.

func (*Tokens[M, K]) TicketSource

func (t *Tokens[M, K]) TicketSource() auth.CredentialName

TicketSource is the credential source that redeemed tickets authenticate: this binding's guard source.

func (*Tokens[M, K]) Touch

func (t *Tokens[M, K]) Touch(ctx context.Context, access secret.String) (bool, error)

Touch records explicit access activity without extending access, refresh or absolute expiry. Ordinary guard verification remains read-only.

func (*Tokens[M, K]) Validate

func (t *Tokens[M, K]) Validate() error

func (*Tokens[M, K]) WithObserver

func (t *Tokens[M, K]) WithObserver(observer auth.Observer) (*Tokens[M, K], error)

WithObserver returns a binding sharing this guard that reports EventLogin for full issuance (including MFA completion), EventLogout for Logout, LogoutRefresh and RevokeCurrent, and EventOtherDevicesLoggedOut for RevokeOthers.

type TransactionalBackend

type TransactionalBackend interface {
	RevokeAllIn(context.Context, *database.Tx, Address, model.Identity) (uint64, error)
}

TransactionalBackend joins a caller-owned transaction. It must verify pool ownership, restore schema state, preserve rollback, and never commit or retry.

Directories

Path Synopsis
Package postgres implements authoritative token persistence through explicit Foundry migrations and generated model queries.
Package postgres implements authoritative token persistence through explicit Foundry migrations and generated model queries.

Jump to

Keyboard shortcuts

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