Documentation
¶
Overview ¶
Package session manages sign-in sessions: issue (cookie-backed opaque tokens), validate (sliding expiry), list, and revoke. The sign-in endpoint lives here; credential checking is delegated to a Verifier (the password feature) via consumer-side wiring.
Index ¶
- Constants
- Variables
- func RequestIP(r *http.Request) string
- type AccessClaims
- type AccessTokenSigner
- type AccessTokenSignerOption
- type AuthTokenID
- type Meta
- type PostgresStore
- func (s *PostgresStore) Create(ctx context.Context, se *Session) error
- func (s *PostgresStore) ListActiveForUser(ctx context.Context, userID user.UserID) ([]Session, error)
- func (s *PostgresStore) RevokeAllForUser(ctx context.Context, userID user.UserID, exceptID string) error
- func (s *PostgresStore) RevokeByTokenHash(ctx context.Context, tokenHash string) error
- func (s *PostgresStore) RevokeForUser(ctx context.Context, userID user.UserID, sessionID string) error
- func (s *PostgresStore) Rotate(ctx context.Context, id string, tokenHash string, expiresAt time.Time) error
- func (s *PostgresStore) Touch(ctx context.Context, id string, expiresAt time.Time) error
- func (s *PostgresStore) ValidByID(ctx context.Context, id string) (Session, user.User, error)
- func (s *PostgresStore) ValidByTokenHash(ctx context.Context, tokenHash string) (Session, user.User, error)
- type RefreshTokenID
- type Service
- func (s *Service) APIRoutes(r chi.Router, _ identity.RouteGroups)
- func (s *Service) IssueAccess(ctx context.Context, p kernel.Principal) (string, time.Time, error)
- func (s *Service) IssueForUser(ctx context.Context, userID user.UserID, provider string, meta Meta) (string, Session, error)
- func (s *Service) ListForUser(ctx context.Context, userID user.UserID) ([]Session, error)
- func (s *Service) Name() string
- func (s *Service) RPCService() (string, http.Handler)
- func (s *Service) Resolve(ctx context.Context, token string) (user.User, Session, error)
- func (s *Service) ResolveAccess(ctx context.Context, token string) (kernel.Principal, error)
- func (s *Service) ResolveSession(ctx context.Context, token string) (kernel.Principal, error)
- func (s *Service) RevokeAllForUser(ctx context.Context, userID user.UserID, keepID string) error
- func (s *Service) RevokeCurrent(ctx context.Context, token string) error
- func (s *Service) RevokeForUser(ctx context.Context, userID user.UserID, sessionID string) error
- func (s *Service) Rotate(ctx context.Context, se Session) (string, Session, error)
- func (s *Service) SignIn(ctx context.Context, identityText, secret string, meta Meta) (string, user.User, Session, error)
- func (s *Service) SignInWithPending(ctx context.Context, identityText, secret string, meta Meta) (SignInWithPendingResult, error)
- type ServiceOption
- func WithAccessTokens(signer *AccessTokenSigner) ServiceOption
- func WithClock(now func() time.Time) ServiceOption
- func WithLifetime(d time.Duration) ServiceOption
- func WithMFAPort(port identity.MFAPendingIssuer) ServiceOption
- func WithRecorder(recorder identity.Recorder) ServiceOption
- func WithShortLifetime(d time.Duration) ServiceOption
- type Session
- type SessionID
- type SignInWithPendingResult
- type Store
- type Verifier
Constants ¶
const ( // DefaultAccessTokenTTL bounds the bearer token's usefulness // after exfiltration when no lifetime is configured; refresh // stays cookie-only. DefaultAccessTokenTTL = 10 * time.Minute // AccessTokenIssuer and AccessTokenAudience pin the internal // token space; OIDC tokens carry the public issuer URL and a // client id instead. AccessTokenIssuer = "tango:internal" AccessTokenAudience = "tango:rpc" // AccessTokenCookieName mirrors the access token in an HttpOnly // cookie scoped to the auth bridge paths, so the worker can // bootstrap without a refresh rotation right after sign-in. AccessTokenCookieName = "tango_access" // AccessTokenPath scopes the access cookie to the bridge. AccessTokenPath = "/api/auth" )
The internal access token is a short-lived RS256 JWT the auth worker holds in memory and sends as `Authorization: Bearer` on RPC calls. Issuer and audience are pinned away from the OIDC values so an internal token can never be exchanged for a relying-party token or vice versa.
Variables ¶
var ErrInvalidCredentials = errors.New("session: invalid credentials")
ErrInvalidCredentials mirrors the password feature error so the handler maps both to 401 without importing it.
var ( // ErrNotFound is returned when no session matches, including // expired and revoked ones (they must be indistinguishable). ErrNotFound = errors.New("session: not found") )
Errors surfaced by stores and the service.
Functions ¶
Types ¶
type AccessClaims ¶ added in v0.0.12
type AccessClaims struct {
// SessionID is the owning session TypeID — the link that keeps
// RPC revocation exact even inside the token TTL.
SessionID string `json:"sid"`
Admin bool `json:"admin"`
}
AccessClaims are the private claims of an access token; the standard subject carries the user TypeID.
type AccessTokenSigner ¶ added in v0.0.12
type AccessTokenSigner struct {
// contains filtered or unexported fields
}
AccessTokenSigner mints and verifies internal access tokens with the shared signing key provider.
func NewAccessTokenSigner ¶ added in v0.0.12
func NewAccessTokenSigner(provider jwtutils.KeyProvider, opts ...AccessTokenSignerOption) *AccessTokenSigner
NewAccessTokenSigner builds the access-token service on top of a cached key provider.
func (*AccessTokenSigner) Issue ¶ added in v0.0.12
func (a *AccessTokenSigner) Issue(ctx context.Context, p kernel.Principal) (string, time.Time, error)
Issue signs a fresh access token for the principal and returns it with its expiry.
func (*AccessTokenSigner) Verify ¶ added in v0.0.12
func (a *AccessTokenSigner) Verify(ctx context.Context, token string) (jwtutils.Verified[AccessClaims], error)
Verify checks signature, issuer, audience, and expiry.
type AccessTokenSignerOption ¶ added in v0.0.12
type AccessTokenSignerOption func(*AccessTokenSigner)
AccessTokenSignerOption configures the signer.
func WithAccessTokenTTL ¶ added in v0.0.12
func WithAccessTokenTTL(ttl time.Duration) AccessTokenSignerOption
WithAccessTokenTTL sets the bearer token lifetime; a non-positive value keeps the default.
type AuthTokenID ¶
Typed IDs for the session tables: UUIDv7 suffix, snake_case prefix matching the singular table name.
type Meta ¶
type Meta struct {
UserAgent string
DeviceName string
IPAddress string
// Remember selects the long session lifetime; the zero value asks
// for the short one.
Remember bool
}
Meta carries request context captured at sign-in.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore persists sessions in public.sessions; the token hash is the lookup key, the TypeID string is the public identifier.
func NewPostgresStore ¶
func NewPostgresStore(exec datastore.Executor) *PostgresStore
NewPostgresStore builds the production session store.
func (*PostgresStore) Create ¶
func (s *PostgresStore) Create(ctx context.Context, se *Session) error
func (*PostgresStore) ListActiveForUser ¶
func (s *PostgresStore) ListActiveForUser(ctx context.Context, userID user.UserID) ([]Session, error)
ListActiveForUser returns live sessions, newest first.
func (*PostgresStore) RevokeAllForUser ¶
func (s *PostgresStore) RevokeAllForUser(ctx context.Context, userID user.UserID, exceptID string) error
RevokeAllForUser revokes every live session, optionally sparing one (the caller's current session).
func (*PostgresStore) RevokeByTokenHash ¶
func (s *PostgresStore) RevokeByTokenHash(ctx context.Context, tokenHash string) error
RevokeByTokenHash revokes the session matching a token hash.
func (*PostgresStore) RevokeForUser ¶
func (s *PostgresStore) RevokeForUser(ctx context.Context, userID user.UserID, sessionID string) error
RevokeForUser revokes one session owned by the user; unknown or foreign session IDs surface ErrNotFound.
func (*PostgresStore) Rotate ¶ added in v0.0.12
func (s *PostgresStore) Rotate(ctx context.Context, id string, tokenHash string, expiresAt time.Time) error
Rotate replaces the session's refresh token hash and restarts its sliding expiry; the previous token stops resolving.
func (*PostgresStore) Touch ¶
Touch extends a live session's expiry (sliding window) and marks the refresh time.
func (*PostgresStore) ValidByID ¶ added in v0.0.12
ValidByID resolves one live session with its user by ID; expired and revoked rows are invisible (indistinguishable from missing).
func (*PostgresStore) ValidByTokenHash ¶
func (s *PostgresStore) ValidByTokenHash(ctx context.Context, tokenHash string) (Session, user.User, error)
ValidByTokenHash resolves one live session with its user; expired and revoked rows are invisible (indistinguishable from missing).
type RefreshTokenID ¶
Typed IDs for the session tables: UUIDv7 suffix, snake_case prefix matching the singular table name.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service owns the sign-in flow: verify credentials, issue opaque tokens, resolve them back to principals, and manage revocation.
func NewService ¶
func NewService(store Store, verifier Verifier, users user.Store, recorder identity.Recorder, opts ...ServiceOption) *Service
NewService builds the session feature.
func (*Service) APIRoutes ¶
func (s *Service) APIRoutes(r chi.Router, _ identity.RouteGroups)
APIRoutes mounts the retained auth endpoints inside the shared /api group: the stateless token refresh. Sign-in, sign-out, and session reads serve ConnectRPC exclusively — see handler_rpc.go.
func (*Service) IssueAccess ¶ added in v0.0.12
IssueAccess mints the RPC bearer JWT for a live session.
func (*Service) IssueForUser ¶
func (s *Service) IssueForUser(ctx context.Context, userID user.UserID, provider string, meta Meta) (string, Session, error)
IssueForUser mints a session for an already-authenticated identity — the alternative sign-in providers (passkeys, device login, one-time access) call this after verifying their own ceremony. A confirmed second factor fails these flows closed: the pending bridge belongs to the password sign-in path only, so no provider can bypass MFA.
func (*Service) ListForUser ¶
ListForUser returns the user's live sessions, newest first.
func (*Service) RPCService ¶ added in v0.0.12
RPCService returns the Connect registration for the auth surface: the procedure prefix and the handler. SignIn is public; SignOut and GetSession resolve the RPC bearer token and are guarded by the interceptor, because a service-wide middleware would lock out the anonymous entry point.
func (*Service) Resolve ¶
Resolve maps a cookie token back to its principal and live session, sliding the expiry forward when past half-life.
func (*Service) ResolveAccess ¶ added in v0.0.12
ResolveAccess verifies an RPC bearer access token: signature, issuer, audience, and expiry first, then the owning session — so a revoked or expired session cannot ride out the token TTL.
func (*Service) ResolveSession ¶
ResolveSession implements kernel.Authenticator: cookie token in, transport principal out.
func (*Service) RevokeAllForUser ¶
RevokeAllForUser revokes every session, optionally sparing one (the current session during a password change).
func (*Service) RevokeCurrent ¶
RevokeCurrent signs out the caller's session by token.
func (*Service) RevokeForUser ¶
RevokeForUser revokes one session owned by the user; unknown or foreign session IDs surface ErrNotFound.
func (*Service) Rotate ¶ added in v0.0.12
Rotate issues the next refresh token for a live session: the new token replaces the hash and restarts the sliding expiry, so the previous token stops resolving immediately.
func (*Service) SignIn ¶
func (s *Service) SignIn(ctx context.Context, identityText, secret string, meta Meta) (string, user.User, Session, error)
SignIn verifies the credentials and issues a full session; callers that compose with the second factor use SignInWithPending.
func (*Service) SignInWithPending ¶
func (s *Service) SignInWithPending(ctx context.Context, identityText, secret string, meta Meta) (SignInWithPendingResult, error)
SignInWithPending verifies the credentials and lands in a full session — or in the pending-auth bridge when a confirmed second factor requires it. The pending token goes to the pending cookie, never to the session cookie.
type ServiceOption ¶
type ServiceOption func(*Service)
ServiceOption configures the session service.
func WithAccessTokens ¶ added in v0.0.12
func WithAccessTokens(signer *AccessTokenSigner) ServiceOption
WithAccessTokens wires the internal access-token signer; required for the RPC bearer bridge and the Connect handlers.
func WithClock ¶
func WithClock(now func() time.Time) ServiceOption
WithClock overrides the service clock (tests).
func WithLifetime ¶
func WithLifetime(d time.Duration) ServiceOption
WithLifetime sets the session lifetime and the sliding-refresh threshold (half the lifetime).
func WithMFAPort ¶
func WithMFAPort(port identity.MFAPendingIssuer) ServiceOption
WithMFAPort wires the second-factor port; a confirmed enrollment makes password sign-in (and every other non-MFA provider) land in a pending authentication instead of a full session.
func WithRecorder ¶
func WithRecorder(recorder identity.Recorder) ServiceOption
WithRecorder overrides the audit recorder (tests).
func WithShortLifetime ¶ added in v0.0.12
func WithShortLifetime(d time.Duration) ServiceOption
WithShortLifetime sets the lifetime of a session issued without "remember me". It must not exceed the lifetime set by WithLifetime.
type Session ¶
type Session struct {
ID string
UserID user.UserID
Provider string
TokenHash string
UserAgent *string
DeviceName *string
IPAddress *string
CreatedAt time.Time
ExpiresAt time.Time
RefreshedAt *time.Time
RevokedAt *time.Time
// Remember records the duration requested at sign-in: a session
// issued with remember=false keeps the short lifetime across
// sliding refreshes and rotations.
Remember bool
}
Session is one active sign-in. The ID is public (listed, revoked by users); the token never leaves the service — only its SHA-256 hash is stored, and only the cookie carries it.
type SessionID ¶
Typed IDs for the session tables: UUIDv7 suffix, snake_case prefix matching the singular table name.
type SignInWithPendingResult ¶
type SignInWithPendingResult struct {
Pending bool
Token string // pending token when Pending, else the session token
User user.User
Session Session
}
SignIn verifies the identity + secret pair and issues a session token. The token goes to the client once (cookie); only its hash is stored. SignInWithPendingResult carries either a full session or the pending-auth bridge that only MFA verification can upgrade.
type Store ¶
type Store interface {
Create(ctx context.Context, s *Session) error
ValidByTokenHash(ctx context.Context, tokenHash string) (Session, user.User, error)
// ValidByID resolves one live session with its user by session
// ID — the access-token check anchors on it.
ValidByID(ctx context.Context, id string) (Session, user.User, error)
Touch(ctx context.Context, id string, expiresAt time.Time) error
// Rotate replaces the session's refresh token hash and restarts
// its sliding expiry; the previous token stops resolving.
Rotate(ctx context.Context, id string, tokenHash string, expiresAt time.Time) error
RevokeByTokenHash(ctx context.Context, tokenHash string) error
RevokeForUser(ctx context.Context, userID user.UserID, sessionID string) error
RevokeAllForUser(ctx context.Context, userID user.UserID, exceptID string) error
ListActiveForUser(ctx context.Context, userID user.UserID) ([]Session, error)
}
Store persists sessions. Validity (revoked/expired) is enforced by every read.