Documentation
¶
Overview ¶
Package session declares the typed-Go-heavy Protocol primitive that bundles session-related protocol decisions for accesscore (and any future cell that owns server-side session state).
This package is the protocol vocabulary plus a Store interface, an in- memory implementation (MemStore), and the Protocol-driven storetest conformance suite. The PG-backed Store implementation and the cell-side composition root wiring land in later phases of the same plan (docs/plans/202605082145-034-pg-corecell-b-route-plan.md, S3+S5 / S4).
MemStore scope (intentional design tradeoffs):
- Dev / test only. The production session path is the PG Store landing in S3+S5; cells inject *Protocol + Store via composition root in S4.
- No GC and no capacity bound. Expired sessions remain Get-able by design (ADR-Session D3 fail-closed: callers decide via Session fields, not by absence). PG store handles purge via janitor / TTL cron in S3+S5; the protocol vocabulary itself does not own GC.
- O(n) RevokeForSubject. The mem implementation scans the session map under a single RWMutex; this matches the existing same-tier mem primitives (cells/accesscore/internal/mem/session_repo.go, runtime/auth/refresh/memstore) and is acceptable at dev/test subject counts. PG store delivers indexed revoke at scale via UPDATE ... WHERE user_id = $1.
- No instrumentation (slog / metrics). Observability is a cell-layer responsibility per GoCell layering (cells/, not runtime/); accesscore wires slog around Store calls in S4.
The protocol decisions encoded here are governed by:
- docs/architecture/202605101400-adr-credential-session-protocol.md (D1 jti-only token model / D2 AuthzEpoch ordering / D3 fail-closed credential events / D4 refresh-vs-session co-lifecycle / D5 same-tx revocation / D6 sealed FingerprintMode)
- docs/architecture/202605101400-adr-admin-invariant.md (admin-related domain rules; orthogonal to this package)
- docs/architecture/202605101200-adr-typed-go-heavy-protocol-primitives.md (the typed-Go-heavy paradigm this package instantiates)
Construction:
proto, err := session.NewProtocol(
session.WithFingerprint(session.FingerprintJTIRef{}),
session.WithOrdering(session.OrderingAuthzEpoch{}),
session.WithRevokeOn(
session.CredentialEventPasswordReset,
session.CredentialEventLock,
session.CredentialEventDelete,
session.CredentialEventRoleRevoke,
),
)
Composition root only (cellmodules/accesscore/module.go):
proto, err := session.NewProtocol(...)
session.NewProtocol must only be called from cmd/* (or from this package's own storetest sub-package, which constructs the canonical test Protocol) — cells must consume an injected *Protocol, never construct their own. This boundary is enforced by archtest SESSION-PROTOCOL-COMPOSITION-ROOT-01 (active; cell consumers begin arriving in S4 of the plan above).
Index ¶
- func ValidateCredentialEvent(e CredentialEvent) bool
- type CredentialEvent
- type FingerprintJTIRef
- type FingerprintMode
- type MemStore
- func (m *MemStore) Create(_ context.Context, t tenant.TenantID, s *Session) error
- func (m *MemStore) Get(_ context.Context, id string) (*ValidateView, error)
- func (m *MemStore) RepoReady(_ context.Context) error
- func (m *MemStore) Revoke(_ context.Context, id string) error
- func (m *MemStore) RevokeForSubject(_ context.Context, subjectID string, event CredentialEvent, ...) error
- type Option
- type OrderingAuthzEpoch
- type OrderingModel
- type Protocol
- type Session
- type Store
- type ValidateView
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ValidateCredentialEvent ¶
func ValidateCredentialEvent(e CredentialEvent) bool
ValidateCredentialEvent reports whether e is a declared CredentialEvent. Used to reject open-int values like CredentialEvent(99) at store boundaries.
Exported so that adapter-layer stores (e.g. adapters/postgres.PGSessionStore) can delegate to this single predicate instead of maintaining a parallel switch. Having one source of truth prevents silent divergence when a new CredentialEvent constant is added in the future.
Types ¶
type CredentialEvent ¶
type CredentialEvent int
CredentialEvent enumerates credential state changes that revoke active sessions and refresh chains for the affected subject (ADR D3 — fail-closed by default; permission removal routes through CredentialEventRoleRevoke).
const ( // CredentialEventPasswordReset is emitted when a user's password is reset // (forced reset, self-service change, or operator-initiated). CredentialEventPasswordReset CredentialEvent = iota + 1 // CredentialEventLock is emitted when an account transitions to a locked // state (manual lock or auto-lock from failed-login threshold). CredentialEventLock // CredentialEventDelete is emitted when a user is deleted. CredentialEventDelete // CredentialEventRoleRevoke is emitted when any role assignment changes // for the user (revoke, downgrade, or permission-set change). CredentialEventRoleRevoke // CredentialEventRefreshReuse is emitted when refresh-token reuse is // detected beyond the reuse-grace window. The credentialinvalidate funnel // passes this event to RevokeForSubject so the security response is // identical to a full credential revocation. // // This event is NOT part of allCredentialEvents (and therefore not required // by WithRevokeOnAll / NewProtocol's completeness check). It is a // security-response event triggered by the refresh-store's reuse detection // path, not a user-lifecycle credential-state transition. CredentialEventRefreshReuse )
func (CredentialEvent) String ¶
func (e CredentialEvent) String() string
String returns a stable textual label suitable for diagnostics, storetest case names, and slog attributes.
type FingerprintJTIRef ¶
type FingerprintJTIRef struct{}
FingerprintJTIRef stores the JWT jti claim reference (RFC 9068 §2.2.4) on the server side. Session rows persist {sid, jti}; no token plaintext or HMAC fingerprint is stored. The authz_epoch_at_issue column was restored in S4d (migration 026 for sessions, migration 027 for refresh_tokens) as the row-level credential provenance source of truth; sessionvalidate compares the live users.authz_epoch against the session row's authz_epoch_at_issue, not against a JWT claim. See ADR 202605101400 §A8 (A1 RETRACTED).
type FingerprintMode ¶
type FingerprintMode interface {
// contains filtered or unexported methods
}
FingerprintMode is sealed: only types declared in this package may implement it (the marker method fingerprintModeOK is unexported). Callers select a concrete fingerprint shape at composition root via WithFingerprint.
Future opaque-token deployments may add a sibling type (e.g. FingerprintHMACSha256) without breaking the existing API; jti-only is the only shape supported today (see ADR D1).
type MemStore ¶
type MemStore struct {
// contains filtered or unexported fields
}
MemStore is an in-memory Store implementation for dev and tests. It is not a production substrate — the PG-backed Store landing in S3+S5 owns the production path. Three properties follow from the dev/test scope and are documented design choices, not gaps:
- Single RWMutex over a map[ID]*Session. RevokeForSubject scans under the write lock (O(n) in subject count). Acceptable at dev/test scale; PG handles batch revoke via SQL with indexed user_id.
- No GC and no capacity ceiling. Expired sessions remain Get-able (ADR-Session D3); cleanup is a PG janitor concern.
- No instrumentation. Observability is a cell-layer concern (S4 wires slog/metrics around Store calls in accesscore).
The contract test suite exercises concurrent access (`go test -race`) and the protocol decisions encoded in *Protocol drive RevokeForSubject scoping. See package doc for the full scope rationale.
func NewMemStore ¶
NewMemStore constructs a MemStore. Both protocol and clk are strong- dependency wiring (they are not replaceable defaults); typed-nil and bare nil are rejected at construction so misconfiguration surfaces at startup rather than at the first request.
runtime-api.md §Option 范式分层 — one or two unconditional dependencies are passed positionally; Option pattern only becomes warranted at ≥ 3 deps or when an accumulator (e.g. WithRevokeOn) appears.
func (*MemStore) Create ¶
Create persists s with the given tenant t. t must be a non-empty canonical UUID; empty TenantID returns ErrValidationFailed. Protocol-shape validation rejects records that violate the configured FingerprintMode (e.g. empty JTI under FingerprintJTIRef); duplicate IDs return ErrSessionConflict. Stored value is a defensive copy so caller mutations cannot bleed into the store.
func (*MemStore) Get ¶
Get returns the validate projection of the session keyed by id. Revoked sessions are still returned (caller inspects RevokedAt); GC eligibility (Session.ExpiresAt) is intentionally not exposed — validate paths must not gate on it.
func (*MemStore) RepoReady ¶
RepoReady implements healthz.RepoProber. In-memory store is always ready — there is no external relation or schema that can go missing. Returns nil unconditionally (MemStore convention per kernel/healthz.RepoProber godoc).
func (*MemStore) Revoke ¶
Revoke marks the session keyed by id dead. Idempotent: missing IDs and already-revoked sessions both succeed without modifying state. Once RevokedAt is set it is never re-stamped (append-only revoke semantics — ADR-Session D3 fail-closed by default).
func (*MemStore) RevokeForSubject ¶
func (m *MemStore) RevokeForSubject(_ context.Context, subjectID string, event CredentialEvent, tok credentialfence.FenceToken) error
RevokeForSubject marks every active session belonging to subjectID dead. The CredentialEvent argument is informational under the current protocol (D3 fail-closed by default — every event triggers identical revoke scoping); future protocols may scope per-event when sealed alternatives are added. Missing subjects yield no error (always succeeds).
type Option ¶
Option mutates a Protocol during NewProtocol. Options are applied in order; each Option may return an error to short-circuit construction.
func WithFingerprint ¶
func WithFingerprint(fp FingerprintMode) Option
WithFingerprint declares the token fingerprint mode.
Both bare-nil and typed-nil are rejected by NewProtocol so the fingerprint mode is never silently absent. Pattern mirrors runtime/http/router.WithRateLimiter (see runtime-api.md §Option 范式分层 — strong-dependency wiring option).
func WithOrdering ¶
func WithOrdering(om OrderingModel) Option
WithOrdering declares the login-vs-revoke ordering primitive (ADR D2).
Both bare-nil and typed-nil are rejected by NewProtocol so the ordering model is never silently absent. Pattern mirrors runtime/http/router.WithRateLimiter (see runtime-api.md §Option 范式分层 — strong-dependency wiring option).
func WithRevokeOn ¶
func WithRevokeOn(events ...CredentialEvent) Option
WithRevokeOn declares a set of credential events that revoke active sessions and refresh chains for the affected subject (ADR D3).
Each event must be a declared CredentialEvent constant; unknown values are rejected. NewProtocol additionally requires the complete set of 4 events to be declared (ADR D3 fail-closed) — prefer WithRevokeOnAll() over manual enumeration.
This is a builder-style option: multiple WithRevokeOn calls accumulate; duplicates are deduplicated, preserving the order of first occurrence. Calling WithRevokeOn() with zero events is a misuse and is rejected at construction (≥1 event required for fail-closed semantics).
func WithRevokeOnAll ¶
func WithRevokeOnAll() Option
WithRevokeOnAll declares all 4 CredentialEvent values at once (ADR D3 fail-closed by default). Recommended path for composition roots — the typed enum + complete-set check make "forgot one event" unrepresentable.
type OrderingAuthzEpoch ¶
type OrderingAuthzEpoch struct{}
OrderingAuthzEpoch uses a per-user monotonic epoch column to invalidate stale tokens (OAuth Security BCP §4.13.1). In the S4d row-provenance model (ADR 202605101400 §A8; §A1 RETRACTED), the access JWT does NOT carry an authz_epoch claim. Instead, sessionvalidate.enforceSessionState compares the live users.authz_epoch against the session row's authz_epoch_at_issue column (restored by migration 026/027). Inequality (`!=`) is the comparison: fail-closed against both stale grants (row_epoch < user_epoch, after a credential event) and tampered grants (row_epoch > user_epoch). Session rows are created with authz_epoch_at_issue = users.authz_epoch at login time; refresh_tokens rows inherit the epoch from the parent session/token row on Rotate. See ADR 202605101400 §A8 for the full row-provenance design.
type OrderingModel ¶
type OrderingModel interface {
// contains filtered or unexported methods
}
OrderingModel is sealed: only types declared in this package may implement it. Callers select a concrete ordering primitive at composition root via WithOrdering. Future alternatives (advisory lock / row version) may be added as sibling types if a use case emerges.
type Protocol ¶
type Protocol struct {
// contains filtered or unexported fields
}
Protocol bundles the protocol decisions that govern a session subsystem.
Fields are required (NewProtocol fail-fasts on missing values) and are immutable after construction. Accessor methods return defensive copies where applicable.
func NewProtocol ¶
NewProtocol assembles a Protocol from the supplied options and fail-fasts on missing required fields. The returned *Protocol is safe for concurrent read-only use.
func (*Protocol) Fingerprint ¶
func (p *Protocol) Fingerprint() FingerprintMode
Fingerprint returns the configured fingerprint mode.
func (*Protocol) Ordering ¶
func (p *Protocol) Ordering() OrderingModel
Ordering returns the configured ordering model.
func (*Protocol) RevokeOn ¶
func (p *Protocol) RevokeOn() []CredentialEvent
RevokeOn returns a defensive copy of the configured credential events. Callers must not assume the returned slice retains its underlying array.
type Session ¶
type Session struct {
// ID is the opaque server-side session identifier. Storage backends pick
// the format (UUID, ULID, etc.); the protocol does not interpret it.
ID string
// SubjectID is the user identifier that owns this session. The protocol
// treats this as an opaque string; callers may use any non-empty format
// (UUID, ULID, etc.).
//
// Backends MAY enforce additional shape constraints. For example, the
// PG-backed store (adapters/postgres.PGSessionStore) requires SubjectID
// to be a UUID string for FK relationships to the users.id column.
// Mem store accepts any non-empty string.
SubjectID string
// JTI is a unique fingerprint stored on the session row; backends with
// FingerprintJTIRef require it non-empty on Create. It stores the access
// JWT's `jti` claim per RFC 9068 §2.2.4 — sessionmint.MintAccess generates
// a fresh UUIDv4 per token and returns it as Result.JTI so the caller
// (sessionlogin) can persist it on the session row at Create. Refresh
// keeps session.ID stable across rotations but each minted access token
// gets its own jti; the session row stores the *original* login-time jti
// as the fingerprint, not the latest rotation.
JTI string
// AuthzEpochAtIssue is the snapshot of users.authz_epoch at the moment
// this session was created. It is the credential provenance source of
// truth — sessionvalidate compares it against the current users.authz_epoch
// (NOT against the JWT claim, which can be re-minted from live user state
// during refresh). A zero value is invalid: Store.Create rejects it with
// ErrValidationFailed (storetest conformance T-S4D-1 enforces).
//
// ADR-credential §A8 (S4d) — supersedes §A1 (retracted): row-level pin is
// not a JWT claim mirror, it is the only server-side source that survives
// refresh rotation. Without it, a stale refresh upgrades to live epoch on
// next use (PR #490 review P1).
//
// ref: PostgreSQL row-level locking — login flow uses SELECT ... FOR UPDATE
// on users to serialize against credentialinvalidate.Invalidator.Apply,
// making the snapshot read+write atomic with respect to epoch bumps.
AuthzEpochAtIssue int64
// CreatedAt is the issue timestamp in UTC.
CreatedAt time.Time
// ExpiresAt is the GC eligibility timestamp in UTC — when the row may be
// physically deleted by a sweep (migration 018 idx_sessions_expires).
// It is NOT a validate gate: Store.Get returns a *ValidateView that does
// not expose this field, so validate paths cannot reach it. JWT exp claim
// guards access-token lifetime; RevokedAt guards revocation.
//
// ref: ory/fosite handler/oauth2/strategy_jwt.go ValidateAccessToken
// (JWT exp only); hashicorp/vault expiration.go (leaseEntry.ExpireTime
// physically isolated from token lookup path).
ExpiresAt time.Time
// RevokedAt is non-nil iff Revoke / RevokeForSubject has marked this row
// dead. Once set it must never be cleared (append-only revoke semantics
// — ADR-Session D3 fail-closed).
RevokedAt *time.Time
// TenantID is the tenant that owns this session. It is the carrier for
// the sessions.tenant_id column (migration 054) and is required at
// Create time — the PG composite FK (tenant_id, subject_id) → users
// proves that session.tenant_id is trustworthy for deriving RLS scope
// in refresh/validate paths. Empty TenantID is rejected by Store.Create.
TenantID tenant.TenantID
}
Session is the canonical server-side session record exchanged between session.Store implementations and consumers. Field shape is fixed by ADR-Session (`docs/architecture/202605101400-adr-credential-session-protocol.md`) D1/D2 — jti-only fingerprint reference, AuthzEpoch ordering snapshot, no access-token plaintext.
Session is a value record; behavior (revoked/expired predicates) is left to call sites until ≥ 3 distinct call sites emerge (then we can extract methods — go-standards.md repetition rule).
type Store ¶
type Store interface {
// Create persists s with the given tenant t. t must be a valid non-empty
// canonical UUID; empty TenantID returns ErrValidationFailed. t is stored
// on the sessions.tenant_id column (migration 054) and exposed through
// ValidateView.TenantID for downstream RLS scope derivation.
Create(ctx context.Context, t tenant.TenantID, s *Session) error
Get(ctx context.Context, id string) (*ValidateView, error)
Revoke(ctx context.Context, id string) error
RevokeForSubject(ctx context.Context, subjectID string, event CredentialEvent, tok credentialfence.FenceToken) error
// RepoReady is a differentiated readiness check for the sessions relation.
// See Store godoc for full semantics.
RepoReady(ctx context.Context) error
}
Store persists session records and exposes a differentiated repository readiness check. Implementations must obey the protocol decisions encoded in *Protocol — Create rejects records that violate FingerprintMode shape (e.g. empty JTI under FingerprintJTIRef), and RevokeForSubject revokes every active session for the subject regardless of which CredentialEvent triggered it (D3 fail-closed by default).
Method semantics (ADR-Session §4.2):
- Create: persist a new session. Nil session, empty Session.ID, empty Session.SubjectID, or zero Session.AuthzEpochAtIssue return ErrValidationFailed (S4d: epoch is required credential provenance — storetest conformance T-S4D-1 fixes the contract). Records violating the protocol-configured FingerprintMode (e.g. empty JTI under FingerprintJTIRef) return ErrValidationFailed. Duplicate Session.ID returns ErrSessionConflict; the protocol does not mandate uniqueness on (SubjectID, JTI) — that is a backend decision (PG schema in S3+S5).
- Get: fetch validate projection by Session.ID. Missing → ErrSessionNotFound; revoked sessions are still returned (caller checks RevokedAt). Session.ExpiresAt (GC eligibility) is intentionally not exposed — validate paths must not gate on it.
- Revoke: mark a single session dead. Idempotent: already-revoked or missing IDs are no-ops returning nil (防枚举 — must not leak existence). RevokedAt is set exactly once; subsequent Revoke calls do not re-stamp. Decorating implementations MAY narrow this with an ambient-transaction precondition: adapters/redis.CachingSessionStore registers a post-commit cache-eviction hook (#796) and therefore panics if Revoke runs outside a RunInTx (after-commit) scope; the bare PG / mem stores impose no such precondition. Callers obtaining a Store from composition must honor the strictest wrapped implementation's scope (sessionlogout's persistRevoke already wraps Revoke in RunInTx); bare callers — the storetest conformance suite — supply a unit-of-work scope themselves. Compile- enforcing this precondition (a typed tx-scoped revoke capability rather than a runtime panic) was evaluated as gh #1615's F3 and resolved won't-do (a same-package seal is unreachable — adapters/redis's read path needs a synchronous cache mutation on the same field Revoke uses, and Go has no per-method field scoping; a cross-package seal is disproportionate with no industry precedent). The "second caller outside a tx" risk is instead caught statically by archtest SESSION-REVOKE-CALLER-INTX-01, which allowlists session.Store.Revoke's production callers (each RunInTx-wrapped).
- RevokeForSubject: mark every active session for SubjectID dead. tok is a credentialfence.FenceToken — a sealed capability proof minted only by credentialinvalidate.Invalidator via credentialfence.Mint; passing nil panics via MustHave (B-class programmer error, surfaces as 500). Empty subjectID returns ErrValidationFailed; an event value not declared in the CredentialEvent enum returns ErrValidationFailed. With valid arguments, returns nil even when the subject has no sessions; pre- revoked sessions for the subject preserve their original RevokedAt timestamp (append-only revoke per ADR-Session D3).
- RepoReady: differentiated readiness check for the sessions relation. SQL-backed implementations issue a representative query against the sessions table (e.g. SELECT 1 FROM sessions WHERE false) so the check exercises a distinct failure domain from the pool-level postgres_ready probe — it surfaces schema/migration drift and table-level permission loss that a connection ping cannot detect. In-memory implementations return nil (always ready, MemStore convention). Satisfies kernel/healthz.RepoProber; registered via cellgen RegisterReadiness.
type ValidateView ¶
type ValidateView struct {
ID string
SubjectID string
RevokedAt *time.Time
// AuthzEpochAtIssue exposes Session.AuthzEpochAtIssue to the validate
// path. sessionvalidate compares this with the live users.authz_epoch;
// mismatch → 401 (ADR-credential §A8). The JWT's authz_epoch claim is
// removed in S4d — view.AuthzEpochAtIssue is the only credential
// provenance source-of-truth.
AuthzEpochAtIssue int64
// TenantID exposes Session.TenantID to the validate / refresh paths.
// sessionrefresh uses this as the scope for the accesscore RLS SET LOCAL
// (PR-3b): the composite FK (tenant_id, subject_id) → users in the DB
// guarantees this value is consistent with the user row. sessionvalidate
// uses it to call GetByIDInTenant on the scoped tx path.
TenantID tenant.TenantID
}
ValidateView is the narrow projection of a Session exposed by Store.Get. It carries exactly the fields validate paths (sessionvalidate, sessionrefresh, sessionlogout) need to make their decision: identity (ID, SubjectID) and revocation (RevokedAt). Session.ExpiresAt is intentionally absent — it is GC eligibility metadata, not a validate gate (see Session.ExpiresAt godoc).
This type-level partition mirrors hashicorp/vault's barrier-view isolation: token lookup paths physically cannot reach leaseEntry.ExpireTime, so the "validate by time comparison" anti-pattern is unrepresentable. Here, the equivalent guard is at the Go type level — sess.ExpiresAt is not a field on ValidateView, so re-introducing the double-gate fails to compile.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package sessiontest provides test helpers for code that depends on runtime/auth/session.
|
Package sessiontest provides test helpers for code that depends on runtime/auth/session. |
|
Package storetest provides a reusable Protocol-driven contract test suite for session.Store implementations.
|
Package storetest provides a reusable Protocol-driven contract test suite for session.Store implementations. |