session

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package session manages opaque, revocable credentials for typed model guards. Persistence adapters own atomic writes; application code supplies verified proofs and consumes models through the ordinary auth.Guard.

Index

Constants

View Source
const EncodedSecretBytes = credential.EncodedSecretBytes
View Source
const MaxImpersonation = 24 * time.Hour

MaxImpersonation bounds any impersonation session's absolute lifetime.

View Source
const MaxLifetime = 365 * 24 * time.Hour
View Source
const MaxPageSize = 1024
View Source
const MaxSessions = 1024
View Source
const SecretBytes = credential.SecretBytes

Variables

View Source
var ActorSessionEnded = safeerror.Sentinel(fault.Missing, "the impersonating actor's session has ended")

ActorSessionEnded is the cause Resume reports (as auth.Unauthenticated) when the impersonation ended but the original actor's session had been revoked or had expired meanwhile: the actor must log in again.

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 credentials to one application/environment, guard, provider and stored model namespace. It is the explicit adapter boundary.

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 owns one authoritative session store. Every mutation is atomic and executes without automatic retry. RevokeAll serializes with creation; sessions issued after that operation are new credentials. Lookup/rotation cannot revive a removed record. Clock sampling occurs after acquiring the relevant lock. Errors may mean an unknown write outcome; they are not missing credentials. Adapters return complete validated records and enforce bounded list/prune work.

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 {
	DeadlineBackend
	ConsumePendingIn(context.Context, *database.Tx, Address, 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
	Regular              Lifetime
	Remembered           Lifetime
	Pending              Lifetime
	MaxPerSubject        int
	MaxPendingPerSubject int
	Limit                auth.LimitPolicy
	MaxConcurrent        int
	Timeout              time.Duration
}

Config contains store-wide resource limits and explicit credential lifetimes. Changing policy affects new sessions; use revocation to invalidate old sessions. MaxPerSubject caps a subject's live full sessions per guard; Limit selects whether a new login evicts the oldest (default) or is rejected with auth.CredentialLimit. Pending-MFA and impersonation sessions never count toward it: each class has its own MaxPendingPerSubject cap and always replaces its oldest session, so MaxPerSubject + 2*MaxPendingPerSubject must not exceed MaxSessions. Expired sessions never count toward any cap.

func DefaultConfig

func DefaultConfig(namespace keyspace.Namespace) Config

func (Config) Validate

func (c Config) Validate() error

type ConfirmationBackend

type ConfirmationBackend interface {
	Confirm(context.Context, Address, model.Identity, model.ID[Record]) (value.Optional[Record], error)
	ConfirmedWithin(context.Context, Address, model.Identity, model.ID[Record], time.Duration) (bool, error)
}

ConfirmationBackend stamps ConfirmedAt on one live, fully authenticated session of the subject and returns the updated record, or omits it when the session is gone. ConfirmedWithin reports whether that live session confirmed within the duration of the backend's current time. Both sample its clock.

type Creation

type Creation struct {
	ID             model.ID[Record]
	Subject        model.Identity
	Hash           Digest
	Assurance      auth.Assurance
	Remember       bool
	Lifetime       Lifetime
	Maximum        int
	PendingMaximum int
	Limit          auth.LimitPolicy
	Device         auth.Device
	// Impersonator marks an impersonation session. Impersonation sessions count
	// only against PendingMaximum, like pending MFA, so they never evict or are
	// rejected by the subject's own sessions.
	Impersonator value.Optional[Impersonator]
}

Creation contains verified identity metadata and a newly generated hash. Maximum is the live full-session bound and PendingMaximum the live pending-MFA bound, both enforced under the subject's write lock for the credential's own kind. Limit selects eviction or rejection for full sessions; pending sessions always evict their oldest. Expired rows 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 DeadlineBackend

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

DeadlineBackend performs checked creation and rechecks an existing credential's deadline before commit. Expiry rolls back all joined writes.

type Digest

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

Digest is a SHA-256 credential hash at the explicit adapter boundary. It cannot be submitted in place of the raw session secret.

func HashSecret

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

HashSecret accepts only a canonical 256-bit random session-secret encoding. Malformed inputs fail before any persistence operation.

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 ID

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

ID is a public session identifier owned by model M. It is not an authentication secret or a model primary key, and remains stable across session rotation.

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 Impersonation

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

Impersonation lets an authenticated actor of one session binding act as a model of another (or the same) binding, for support and administration. The impersonation session records the actor, its guard and its own session, so the original actor stays accessible and the session is visibly marked: Info.Impersonated, Sessions.RequireNotImpersonating and ConfirmCurrent refuse sensitive operations and mint no other credential (CurrentProof, RevokeOthers). Sessions are bounded by a maximum duration, never remembered, counted apart from the subject's own sessions, and end when the actor's own session is revoked or expires, so both bindings must share one store.

Authorize the actor (for example with a policy on the target model) before Start: the binding does not decide who may impersonate whom. Observers receive EventImpersonationStarted and EventImpersonationStopped, which an audit or outbox observer can persist.

func NewImpersonation

func NewImpersonation[A model.Identifiable, AK any, M model.Identifiable, K any](actors *Sessions[A, AK], targets *Sessions[M, K], maximum time.Duration) (*Impersonation[A, AK, M, K], error)

func (*Impersonation[A, AK, M, K]) Actor

func (i *Impersonation[A, AK, M, K]) Actor(ctx context.Context) (value.Optional[A], error)

Actor returns the original actor behind the current impersonation session of the targets guard, loaded through the actors provider, or an omitted value when the current session is not impersonated.

func (*Impersonation[A, AK, M, K]) Actors

func (i *Impersonation[A, AK, M, K]) Actors() *Sessions[A, AK]

Actors and Targets return the bound session bindings.

func (*Impersonation[A, AK, M, K]) Resume

func (i *Impersonation[A, AK, M, K]) Resume(ctx context.Context) (Issued[A, AK], error)

Resume ends the current impersonation session and returns the original actor's session with a fresh secret, for flows where actor and target share one cookie. The actor session recorded at Start is rotated in place: it keeps its ID, creation time, absolute expiry and remember policy, so impersonation can never extend an actor's session. When the actor's session was revoked, rotated or expired meanwhile, or the actor is no longer eligible, the impersonation still ends and Resume fails with auth.Unauthenticated caused by ActorSessionEnded: the actor must log in again.

func (*Impersonation[A, AK, M, K]) Start

func (i *Impersonation[A, AK, M, K]) Start(ctx context.Context, target model.Reference[M, K], duration time.Duration) (Issued[M, K], error)

Start issues an impersonation session for target, marked with the current actor session of the actors guard, lasting at most duration (and the configured maximum). The actor's own session is not revoked; Resume or Stop returns to it. Nested impersonation and self-impersonation are refused; the target must exist and be eligible through its provider.

func (*Impersonation[A, AK, M, K]) Stop

func (i *Impersonation[A, AK, M, K]) Stop(ctx context.Context) error

Stop ends the current impersonation session. Use it when the actor keeps its own credential (another cookie or token); its session is untouched.

func (*Impersonation[A, AK, M, K]) Targets

func (i *Impersonation[A, AK, M, K]) Targets() *Sessions[M, K]

func (*Impersonation[A, AK, M, K]) Validate

func (i *Impersonation[A, AK, M, K]) Validate() error

func (*Impersonation[A, AK, M, K]) WithObserver

func (i *Impersonation[A, AK, M, K]) WithObserver(observer auth.Observer) (*Impersonation[A, AK, M, K], error)

WithObserver returns a copy reporting impersonation start and stop.

type Impersonator

type Impersonator struct {
	Subject model.Identity
	Guard   auth.GuardName
	Session model.ID[Record]
}

Impersonator is the original actor of an impersonation session: its stored identity, the guard that authenticated it and that actor's own session, which Impersonation.Resume and Stop use to return. It is metadata, never authority.

func (Impersonator) Validate

func (i Impersonator) Validate() error

type Info

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

Info exposes session metadata with a concrete stored subject reference. It carries no secret/hash and does not authorize the caller to act as that subject.

func (Info[M, K]) Assurance

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

func (Info[M, K]) ConfirmedAt

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

ConfirmedAt is when the holder last re-entered its password in this session.

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 session 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]) ID

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

func (Info[M, K]) IdleExpiresAt

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

func (Info[M, K]) Impersonated

func (i Info[M, K]) Impersonated() bool

Impersonated reports whether this session was started by impersonation.

func (Info[M, K]) Impersonator

func (i Info[M, K]) Impersonator() value.Optional[Impersonator]

Impersonator is the original actor when this is an impersonation session.

func (Info[M, K]) LastSeenAt

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

func (Info[M, K]) Remembered

func (i Info[M, K]) Remembered() bool

func (Info[M, K]) Subject

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

type IssueOptions

type IssueOptions struct{ Remember bool }

IssueOptions selects persistence policy after credential verification. Pending MFA credentials cannot request remember-me persistence.

type Issued

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

Issued owns the newly generated secret. Only Secret exposes it for the explicit transport boundary; routine formatting and JSON never serialize credentials. Losing the response may leave an orphan session until expiry or revocation.

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]) Secret

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

type Lifetime

type Lifetime struct {
	Idle     time.Duration
	Absolute time.Duration
	Sliding  bool
}

Lifetime fixes server-side expiry at creation. Sliding can extend idle expiry but never the absolute deadline. Rotation never restarts the absolute lifetime.

func (Lifetime) TouchInterval

func (l Lifetime) TouchInterval() time.Duration

TouchInterval is the minimum age of LastSeenAt before a sliding session's activity is written again: max(1 minute, Idle/20), but never more than half the idle lifetime, so a short idle window still slides for an active user. Skipping more frequent writes avoids a row update per request; idle expiry may therefore arrive up to this interval earlier than Idle after the last request.

func (Lifetime) Validate

func (l Lifetime) Validate() error

type Record

type Record struct {
	ID            model.ID[Record]
	Address       Address
	Subject       model.Identity
	Hash          Digest
	Assurance     auth.Assurance
	Remember      bool
	Lifetime      Lifetime
	CreatedAt     temporal.DateTime
	LastSeenAt    temporal.DateTime
	IdleExpiresAt temporal.DateTime
	ExpiresAt     temporal.DateTime
	Device        auth.Device
	// ConfirmedAt is when the holder last re-entered its password (see
	// Sessions.ConfirmCurrent). It never outlives the session.
	ConfirmedAt value.Optional[temporal.DateTime] `json:",omitzero"`
	// Impersonator marks an impersonation session with its original actor.
	Impersonator value.Optional[Impersonator] `json:",omitzero"`
}

Record is a bounded, immutable-value adapter result. Adapters never return a raw secret. Public typed consumers receive Info or Issued, not these records.

func (Record) Format

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

func (Record) Live

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

func (Record) NeedsTouch

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

NeedsTouch reports whether a live sliding session should record activity now.

func (Record) Touch

func (r Record) Touch(now time.Time) (Record, bool, error)

Touch keeps monotonic activity across small server clock regressions. It never revives an expired record and never extends beyond its original absolute limit.

func (Record) Validate

func (r Record) Validate(address Address) error

type ResumptionBackend

type ResumptionBackend interface {
	ResumeActor(context.Context, Address, Record, Digest, func(context.Context, *database.Tx) error) (value.Optional[Record], error)
}

ResumptionBackend returns to the original actor's session after impersonation. ResumeActor runs check first, then, in the same transaction and under the subject lock, locks the exact live, fully authenticated, unimpersonated record (same hash, ID, subject, creation time and absolute expiry) and replaces its secret hash with next, recording activity. The ID, creation time, absolute expiry and remember policy never change. A revoked, rotated, expired or impersonated record returns an omitted value and changes nothing. It never retries.

type SelectiveBackend

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

SelectiveBackend revokes every session of a subject in this address except keep, under the same subject lock as creation. It returns the removed count.

type Sessions

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

Sessions binds one stored model/provider/guard/source to a shared store. Its Guard is constructed once; persisted credentials cannot be rebound to a different guard name by extracting an unscoped strategy. Construction performs no I/O.

func New

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

func (*Sessions[M, K]) CompleteMFA

func (s *Sessions[M, K]) CompleteMFA(ctx context.Context, raw secret.String, factor auth.SecondFactor[M, K], options IssueOptions) (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 (*Sessions[M, K]) ConfirmCurrent

func (s *Sessions[M, K]) ConfirmCurrent(ctx context.Context) (Info[M, K], error)

ConfirmCurrent records that the holder of the current session re-entered its password now. Call it only after verifying the password (for example with auth.PasswordLogin.Confirm); RequireConfirmed then admits sensitive actions.

func (*Sessions[M, K]) Current

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

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

func (*Sessions[M, K]) CurrentProof

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

CurrentProof returns a proof for the current request's model, for issuing a related credential (for example an API token) without constructing an unrestricted proof from a submitted identity. Sessions carry no scope grants. An impersonation session is refused with auth.ImpersonationForbidden: the impersonator must not mint an unmarked token or session for the subject.

func (*Sessions[M, K]) Guard

func (s *Sessions[M, K]) Guard() auth.Guard[M]

func (*Sessions[M, K]) Issue

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

Issue persists a verified proof. This is a trusted login boundary, not password verification. A proof must come from a trusted authentication flow; a parsed client identity or request DTO must never be promoted to a proof here.

func (*Sessions[M, K]) List

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

List returns at most MaxSessions live sessions for this model identity. It exposes typed metadata, never stored hashes or raw secrets. It does not itself authorize the calling user to inspect another user; use an ordinary policy.

func (*Sessions[M, K]) Logout

func (s *Sessions[M, K]) Logout(ctx context.Context, credential secret.String) (bool, error)

Logout revokes a presented session secret and reports EventLogout when it removed a session. Browser adapters use it for the cookie's own logout.

func (*Sessions[M, K]) Prune

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

func (*Sessions[M, K]) PruneExpired

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

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

func (*Sessions[M, K]) RequireConfirmed

func (s *Sessions[M, K]) RequireConfirmed(ctx context.Context, within time.Duration) error

RequireConfirmed returns auth.ConfirmationRequired unless the current session confirmed its password within the given duration, checked against the backend's current time rather than the scope's cached metadata.

func (*Sessions[M, K]) RequireNotImpersonating

func (s *Sessions[M, K]) RequireNotImpersonating(ctx context.Context) error

RequireNotImpersonating returns auth.ImpersonationForbidden when the current session is an impersonation session. Call it before sensitive operations such as changing the password, email or MFA factors, or deleting the account.

func (*Sessions[M, K]) Revocation

func (s *Sessions[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 (*Sessions[M, K]) Revoke

func (s *Sessions[M, K]) Revoke(ctx context.Context, credential secret.String) (bool, error)

func (*Sessions[M, K]) RevokeAll

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

func (*Sessions[M, K]) RevokeAllIn

func (s *Sessions[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 (*Sessions[M, K]) RevokeCurrent

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

RevokeCurrent revokes the session that authenticated this request, for API logout on an authenticated route. It reports EventLogout when it removed it.

func (*Sessions[M, K]) RevokeID

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

RevokeID removes a listed session belonging to this exact subject and guard. The identifier is not authority; authorize the caller before choosing a subject.

func (*Sessions[M, K]) RevokeOthers

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

RevokeOthers revokes every other session 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. Combine with the model's other credential stores for a full sign-out.

func (*Sessions[M, K]) Rotate

func (s *Sessions[M, K]) Rotate(ctx context.Context, credential secret.String) (Issued[M, K], error)

Rotate atomically changes a live session's secret. The previous secret becomes invalid, while session ID, assurance, creation time and absolute expiry persist. Parallel rotations have at most one winner. Failed/uncertain writes are not retried.

func (*Sessions[M, K]) Validate

func (s *Sessions[M, K]) Validate() error

func (*Sessions[M, K]) WithObserver

func (s *Sessions[M, K]) WithObserver(observer auth.Observer) (*Sessions[M, K], error)

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

type Store

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

Store borrows a backend and owns limits shared by its typed session bindings. The caller owns the backend's database/connection lifecycle.

func NewStore

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

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 persists session credentials through Foundry-owned PostgreSQL models and explicit migrations.
Package postgres persists session credentials through Foundry-owned PostgreSQL models and explicit migrations.

Jump to

Keyboard shortcuts

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