Documentation
¶
Overview ¶
Package session defines the session domain entity and its store interface.
Index ¶
- Constants
- type RevokedRefreshToken
- type Session
- func (s *Session) AbsoluteDeadline(lifetime time.Duration) time.Time
- func (s *Session) AuthzActors() principal.Chain
- func (s *Session) ClampToAbsoluteDeadline(lifetime time.Duration)
- func (s *Session) ImpersonatedBy() id.UserID
- func (s *Session) IsHumanPrincipal() bool
- func (s Session) MarshalJSON() ([]byte, error)
- func (s *Session) PastAbsoluteDeadline(lifetime time.Duration, now time.Time) bool
- func (s *Session) SetImpersonatedBy(adminID id.UserID)
- func (s *Session) Subject() principal.Ref
- func (s *Session) UnmarshalJSON(data []byte) error
- type Store
Constants ¶
const ( PrincipalKindUser principal.Kind = "user" PrincipalKindServiceAccount principal.Kind = "service_account" // PrincipalKindAgent marks a session issued to a delegated agent. Unlike // a service-account session, UserID stays populated with the delegating // human, so every consumer that resolves a session's user keeps working. PrincipalKindAgent principal.Kind = "agent" )
Principal kinds a session may carry. An empty PrincipalKind means PrincipalKindUser, for rows written before the column existed.
const ( RevokeReasonRotated = "rotated" RevokeReasonReplayDetected = "replay_detected" RevokeReasonLogout = "logout" RevokeReasonFamilyRevocation = "family_revocation" )
Revocation reason constants for RevokedRefreshToken.Reason.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type RevokedRefreshToken ¶
type RevokedRefreshToken struct {
TokenHash string // hex-encoded SHA-256 of the original refresh token
FamilyID id.SessionFamilyID // family the original token belonged to
RevokedAt time.Time
Reason string // "rotated" | "replay_detected" | "logout"
}
RevokedRefreshToken records a previously-rotated or replay-flagged refresh token. The raw token is NEVER stored — only its SHA-256 hash.
type Session ¶
type Session struct {
ID id.SessionID `json:"id"`
AppID id.AppID `json:"app_id"`
EnvID id.EnvironmentID `json:"env_id"`
UserID id.UserID `json:"user_id"`
OrgID id.OrgID `json:"org_id,omitempty"`
// FamilyID groups sessions descended from a single sign-in via
// successive refresh-token rotations. All sessions sharing a FamilyID
// can be revoked together when refresh-token replay is detected
// (RFC 6819 §5.2.2.3). Fresh sign-ins start a new family; rotations
// inherit the parent's FamilyID. Zero-value on legacy rows is allowed.
FamilyID id.SessionFamilyID `json:"family_id,omitempty"`
// Token and RefreshToken hold the plaintext credentials and are only
// ever populated on the way into the store or when the caller presented
// that exact plaintext to look the session up. A session read back by id
// carries empty plaintext: the store keeps only TokenHash and
// RefreshTokenHash, so a copy of the database yields no usable credential.
Token string `json:"-"`
RefreshToken string `json:"-"`
TokenHash string `json:"-"`
RefreshTokenHash string `json:"-"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
DeviceID id.DeviceID `json:"device_id,omitempty"`
LastActivityAt time.Time `json:"last_activity_at,omitempty"`
ExpiresAt time.Time `json:"expires_at"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Roles holds the role slugs the principal held when this session was
// issued, stamped once on the way to the store rather than resolved per
// request. Forge's auth extension reads them off the AuthContext to
// satisfy the role requirements a route declares, and the generated
// clients turn the same strings into their capability surface.
//
// Stamped means stale: a role granted or revoked after sign-in does not
// reach an existing session, so a revocation needs the session revoked
// too. That is the trade this design accepts in exchange for keeping
// authentication free of an RBAC lookup on every request. Anything that
// must be current at the instant it is checked belongs in a permission
// check against warden, not here.
Roles []string `json:"roles,omitempty"`
// Audience holds the resource identifiers this session's access token was
// granted for (RFC 8707). Empty means unrestricted, which is what every
// session issued before resource indicators existed carries, and what any
// client that sends no `resource` parameter still gets.
//
// This is the opaque-token half of the `aud` claim. A JWT carries its
// audience inside the token and is checked without a store read, while an
// opaque token is only a session lookup key, so the same value has to live
// here for introspection and for the middleware audience check to see it.
Audience []string `json:"aud,omitempty"`
// Scopes holds the OAuth scopes this session was issued with, stamped at
// issuance. Same trade as Roles: authoritative for what this token may
// do, stale with respect to anything granted afterwards.
//
// Empty does not mean "may do anything". A session minted by password
// sign-in carries no scopes, and the RFC 8693 token exchange grant treats
// that as "no subject-side ceiling" while still bounding the result by the
// delegation grant and the client's own registered scopes.
Scopes []string `json:"scopes,omitempty"`
// PrincipalKind identifies the type of principal that owns this session:
// principal.KindUser, principal.KindAgent, principal.KindWorkload, or
// principal.KindService. Empty string means KindUser, for backwards
// compatibility with sessions written before this field existed.
PrincipalKind principal.Kind `json:"principal_kind,omitempty"`
// ServiceAccountID is set when PrincipalKind is anything other than
// KindUser, i.e. for the three non-human kinds, which all share the
// service-account identity space. UserID is left as the zero value in
// that case.
ServiceAccountID id.ServiceAccountID `json:"service_account_id,omitempty"`
// AgentID is set when PrincipalKind is "agent". UserID remains the
// delegating human.
AgentID id.AgentID `json:"agent_id,omitempty"`
// GrantID names the AgentGrant that authorized this session, so revoking
// that grant can find and delete the sessions it issued.
GrantID id.AgentGrantID `json:"grant_id,omitempty"`
// Actors is the chain of principals acting on the subject's behalf,
// ordered nearest-caller-first. Empty on an ordinary session, where the
// subject is calling directly.
//
// This subsumes the old ImpersonatedBy field. An impersonation is a chain
// of one user actor with ActorGrant set to impersonation, which is why
// ImpersonatedBy is now derived rather than stored.
Actors principal.Chain `json:"actors,omitempty"`
// ActorGrant records which sort of grant put the actors on this session.
// Empty when Actors is empty.
ActorGrant principal.GrantKind `json:"actor_grant,omitempty"`
// DelegationID is the grant this session was minted against. Zero for an
// ordinary session.
DelegationID id.DelegationID `json:"delegation_id,omitempty"`
// DPoPJKT is the RFC 7638 thumbprint of the public key this session is
// bound to (RFC 9449). Empty means an ordinary unbound bearer session,
// which is what every session created before this field existed will be.
//
// Enforcement follows this value and not any configuration: once it is
// set, the session cannot be used without a matching proof, whatever the
// app or client mode says at the time of the request.
DPoPJKT string `json:"dpop_jkt,omitempty"`
// ClientID names the OAuth2 client a token was issued to. A session with
// a ClientID is the bearer of an OAuth2 access token: the auth middleware
// parks it until a route tagged with middleware.RequireScope admits it,
// only the token endpoint's refresh_token grant may rotate it, and the
// client that owns it is the only client that may revoke it.
ClientID string `json:"client_id,omitempty"`
}
Session represents an authenticated user session.
func (*Session) AbsoluteDeadline ¶
AbsoluteDeadline is the instant after which the session must not be honoured however active it has been: CreatedAt plus lifetime. A zero lifetime or an unset CreatedAt yields the zero time, meaning no deadline.
func (*Session) AuthzActors ¶
AuthzActors returns the actors Warden must independently authorize.
Empty for an impersonation, and that is the point. Impersonating somebody is precisely the request to evaluate as them, so the admin's own permissions are not intersected in. The gate for impersonation sits on Engine.Impersonate, which is where an admin is checked once, rather than on every subsequent permission check made while impersonating.
func (*Session) ClampToAbsoluteDeadline ¶
ClampToAbsoluteDeadline pulls ExpiresAt and RefreshTokenExpiresAt back to the absolute deadline when they were set past it.
func (*Session) ImpersonatedBy ¶
ImpersonatedBy returns the admin acting as this session's user, or the zero ID when nobody is.
Derived from Actors rather than stored. The grant kind is checked first: an agent acting for a user is also a session with two principals, and reporting that as impersonation would put an admin-takeover banner and an admin- severity audit record on ordinary delegated traffic.
func (*Session) IsHumanPrincipal ¶
IsHumanPrincipal reports whether a person owns this session.
func (Session) MarshalJSON ¶
MarshalJSON keeps impersonated_by on the wire now that it is no longer a struct field. Consumers outside this repository read that key, and the chain is an addition for them, not a replacement.
func (*Session) PastAbsoluteDeadline ¶
PastAbsoluteDeadline reports whether now is after AbsoluteDeadline.
func (*Session) SetImpersonatedBy ¶
SetImpersonatedBy marks this session as adminID acting as its user.
func (*Session) Subject ¶
Subject returns the principal this session is for.
An empty PrincipalKind means "user", which is what every row written before the field existed carries. Normalizing it on read would make a legacy row indistinguishable from one deliberately stamped, so the zero value is interpreted here and not rewritten in the store.
func (*Session) UnmarshalJSON ¶
UnmarshalJSON accepts either representation: a payload carrying only the legacy impersonated_by key rebuilds the chain from it, and one carrying actors is taken as written.
type Store ¶
type Store interface {
CreateSession(ctx context.Context, s *Session) error
GetSession(ctx context.Context, sessionID id.SessionID) (*Session, error)
GetSessionByToken(ctx context.Context, token string) (*Session, error)
GetSessionByRefreshToken(ctx context.Context, refreshToken string) (*Session, error)
UpdateSession(ctx context.Context, s *Session) error
// RotateSession atomically persists s only if the stored row's access token
// hash still equals expectedTokenHash (store.HashToken of the pre-rotation
// plaintext, which s.TokenHash carries after a lookup). It reports true when the row was updated and
// false when no row matched — meaning a concurrent refresh already rotated
// this session. This compare-and-swap serializes concurrent refresh-token
// rotations so only one caller can "win"; the others are refused rather than
// returning tokens that were never persisted (the refresh TOCTOU fix).
RotateSession(ctx context.Context, s *Session, expectedTokenHash string) (bool, error)
// TouchSession performs a lightweight update of last_activity_at, expires_at,
// and updated_at without rewriting the entire session row.
TouchSession(ctx context.Context, sessionID id.SessionID, lastActivityAt, expiresAt time.Time) error
DeleteSession(ctx context.Context, sessionID id.SessionID) error
DeleteUserSessions(ctx context.Context, userID id.UserID) error
// DeleteSessionsByGrant deletes every session issued under grantID. An
// agent session carries the delegating human's UserID (see Session.GrantID
// below), so revoking the grant that authorized it must also delete the
// session itself: otherwise it keeps authenticating as that human on any
// route not guarded by agentauth's own scope check until it separately
// expires.
DeleteSessionsByGrant(ctx context.Context, grantID id.AgentGrantID) error
ListUserSessions(ctx context.Context, userID id.UserID) ([]*Session, error)
// ListUserSessionsPage is the bounded twin for the request path.
ListUserSessionsPage(ctx context.Context, userID id.UserID, opts page.Opts) (page.Page[*Session], error)
// ListSessions returns the most recent sessions across all users, up to limit.
ListSessions(ctx context.Context, limit int) ([]*Session, error)
// IsRefreshTokenRevoked reports whether the given hash is in the revoked
// set. Used to detect replay on every refresh exchange.
IsRefreshTokenRevoked(ctx context.Context, tokenHash string) (bool, error)
// MarkRefreshTokenRevoked persists a hash as revoked. Called on every
// successful rotation (the OLD token's hash is recorded with
// reason="rotated") and on replay detection (reason="replay_detected").
// Idempotent — duplicate inserts succeed silently.
MarkRefreshTokenRevoked(ctx context.Context, tokenHash string, familyID id.SessionFamilyID, reason string) error
// GetRevokedRefreshTokenFamily looks up the family bound to a
// previously-rotated token. Used to identify which sessions to
// cascade-revoke when a replay is detected.
GetRevokedRefreshTokenFamily(ctx context.Context, tokenHash string) (id.SessionFamilyID, error)
// RevokeRefreshTokenFamily revokes every session sharing the given
// family. Called on replay detection so a leaked token doesn't keep
// working through siblings.
RevokeRefreshTokenFamily(ctx context.Context, familyID id.SessionFamilyID, reason string) error
// MarkRefreshTokenReplayed upgrades an already-revoked token's reason to
// "replay_detected" and reports whether this call performed that transition
// (i.e. the token was not already flagged as replayed). It is the
// idempotency guard for replay handling: the engine cascade-revokes the
// family and emits the security alert only when firstReplay is true, so a
// client stuck re-presenting a dead token yields exactly one alert instead
// of one per attempt. The token must already be in the revoked set.
MarkRefreshTokenReplayed(ctx context.Context, tokenHash string) (firstReplay bool, err error)
}
Store defines the persistence interface for session operations.