state

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 13, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// DeviceLoginTTL is how long a device login stays answerable.
	DeviceLoginTTL = 10 * time.Minute
	// DeviceLoginMinPollInterval is the shortest interval between two polls of
	// the same request. RFC 8628 answers a faster poll with "slow_down".
	DeviceLoginMinPollInterval = 1 * time.Second
	// DeviceLoginRecommendedPollInterval is the interval the server asks
	// clients to poll at. It is longer than the enforced minimum on purpose.
	DeviceLoginRecommendedPollInterval = 3 * time.Second
)

Device login bounds. A request authorizes exactly one token, so the window is deliberately short and its interval is enforced.

View Source
const (
	// OAuthAuthorizationRequestTTL is how long a pending request stays
	// answerable.
	OAuthAuthorizationRequestTTL = 10 * time.Minute
	// OAuthAuthorizationCodeTTL is how long an issued authorization code can be
	// exchanged for a token.
	OAuthAuthorizationCodeTTL = 60 * time.Second
)

OAuth authorization bounds. A request authorizes exactly one token, so both the consent interaction and the code it yields are deliberately short-lived.

View Source
const SSOStateTTL = 5 * time.Minute

SSOStateTTL is how long a one-time OAuth2 state nonce stays valid.

Variables

View Source
var (
	// ErrDeviceLoginNotFound means the code is unknown, already consumed, or
	// evicted.
	ErrDeviceLoginNotFound = errors.New("device login not found")
	// ErrDeviceLoginCodeTaken means one of the generated codes is already in
	// use; the caller must generate another one.
	ErrDeviceLoginCodeTaken = errors.New("device login code already in use")
	// ErrDeviceLoginExpired means the request is past its expiry.
	ErrDeviceLoginExpired = errors.New("device login expired")
	// ErrDeviceLoginNotPending means the request was already approved or denied.
	ErrDeviceLoginNotPending = errors.New("device login is not pending")
	// ErrDeviceLoginSlowDown means the request was polled again too soon.
	ErrDeviceLoginSlowDown = errors.New("device login polled too soon")
)

Device login lifecycle errors.

View Source
var (
	// ErrOAuthRequestNotFound means the request id is unknown, already consumed,
	// or evicted.
	ErrOAuthRequestNotFound = errors.New("oauth authorization request not found")
	// ErrOAuthRequestExpired means the request is past its pending lifetime.
	ErrOAuthRequestExpired = errors.New("oauth authorization request expired")
	// ErrOAuthRequestNotPending means the request was already approved or denied.
	ErrOAuthRequestNotPending = errors.New("oauth authorization request is not pending")
	// ErrOAuthRequestNotApproved means the request is not, or is no longer, in a
	// state that can be completed into a code.
	ErrOAuthRequestNotApproved = errors.New("oauth authorization request is not approved")
	// ErrOAuthRequestWrongUser means the request belongs to another user.
	ErrOAuthRequestWrongUser = errors.New("oauth authorization request belongs to another user")
	// ErrOAuthCodeNotFound means the authorization code is unknown, already
	// exchanged, or evicted.
	ErrOAuthCodeNotFound = errors.New("oauth authorization code not found")
	// ErrOAuthCodeExpired means the authorization code is past its lifetime.
	ErrOAuthCodeExpired = errors.New("oauth authorization code expired")
)

OAuth authorization lifecycle errors.

View Source
var ErrInstanceConnectionLimit = errors.New("instance connection limit reached")

ErrInstanceConnectionLimit reports that an instance is already using all of its outstanding connection slots; the caller should retry later.

Functions

func NormalizeUserCode

func NormalizeUserCode(raw string) string

NormalizeUserCode accepts a code as a human would type it: any case, with or without the dash, and the characters Crockford decodes to a digit ("O" to "0", "I" and "L" to "1"). It returns the canonical XXXX-XXXX form, or "" when the input cannot be a code.

Types

type DeviceLogin

type DeviceLogin struct {
	DeviceCode string
	UserCode   string
	State      DeviceLoginState

	ClientName       string
	ClientVersion    string
	RequestIP        string
	RequestUserAgent string

	CreateTime time.Time
	ExpireTime time.Time

	// ApprovedByUserID is the approver, set once the request is approved.
	ApprovedByUserID int
	// contains filtered or unexported fields
}

DeviceLogin is one in-flight device authorization request. It is reachable by both its secret device code and its human-readable user code.

type DeviceLoginState

type DeviceLoginState int

DeviceLoginState is the lifecycle of a device authorization request.

const (
	// DeviceLoginPending waits for a signed-in user to approve or deny it.
	DeviceLoginPending DeviceLoginState = iota
	// DeviceLoginApproved is approved and has not been exchanged yet.
	DeviceLoginApproved
	// DeviceLoginDenied was refused by the user.
	DeviceLoginDenied
	// DeviceLoginExpired was never approved, or was approved but not exchanged
	// within the TTL.
	DeviceLoginExpired
)

type DeviceLoginStore

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

DeviceLoginStore holds in-flight device authorization requests. Like every other cache in this package it is process-local, so a deployment with several replicas must pin a client's requests to a single replica (or front them with sticky routing).

func (*DeviceLoginStore) Approve

func (s *DeviceLoginStore) Approve(userCode string, approverUserID int, approve bool, now time.Time) (DeviceLogin, error)

Approve records the user's decision. Only a pending request can be decided, and the approver is bound to it so the token can be issued for them.

func (*DeviceLoginStore) Create

func (s *DeviceLoginStore) Create(login DeviceLogin, now time.Time) (DeviceLogin, error)

Create stores a new pending request. The caller generates both codes; a collision is reported so it can generate another pair. The returned value is the stored request, with its state, times and poll clock normalised.

func (*DeviceLoginStore) Exchange

func (s *DeviceLoginStore) Exchange(deviceCode string, now time.Time) (DeviceLogin, error)

Exchange polls a request by its device code. An approved request is consumed here: the caller holds the only copy from that moment on, so the token can be issued exactly once. A denied request is consumed the same way after its state has been reported once.

func (*DeviceLoginStore) GetByUserCode

func (s *DeviceLoginStore) GetByUserCode(userCode string, now time.Time) (DeviceLogin, error)

GetByUserCode returns the request a user code refers to, so the confirmation page can show what is about to be approved. An expired request is reported as expired rather than missing.

func (*DeviceLoginStore) NewDeviceLogin

func (s *DeviceLoginStore) NewDeviceLogin(login DeviceLogin, now time.Time) (DeviceLogin, error)

NewDeviceLogin allocates fresh codes for a request and stores it. Codes are generated here, next to the uniqueness check they have to satisfy, and a collision simply draws another pair.

type LoginLimiter

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

LoginLimiter is an in-memory fixed-window counter for failed logins: an account's or a source's window opens with its first failure and lasts loginAttemptWindow. It is deliberately process-local: a deployment with several replicas should put a shared limiter in front of the API.

func (*LoginLimiter) Blocked

func (l *LoginLimiter) Blocked(email, source string, now time.Time) bool

Blocked reports whether the account or the source has exhausted its window and the request must be refused before the password is even checked.

func (*LoginLimiter) RecordFailure

func (l *LoginLimiter) RecordFailure(email, source string, now time.Time)

RecordFailure counts one failed attempt against both the account and the source, starting a new window when the old one has expired.

func (*LoginLimiter) ResetAccount

func (l *LoginLimiter) ResetAccount(email string)

ResetAccount clears the account's failures after a successful login. The source counter is deliberately not cleared: a caller who knows one password must not be able to buy a fresh spraying budget from a shared address.

type OAuthAuthorizationRequest

type OAuthAuthorizationRequest struct {
	// RequestID is opaque and assigned by the store; it is the only handle the
	// consent page and the audit log ever see.
	RequestID string
	ClientID  string
	// ClientName is display only and not verified.
	ClientName          string
	RedirectURI         string
	Resource            string
	Scopes              []string
	CodeChallenge       string
	CodeChallengeMethod string
	// ClientState is the client's opaque `state`, echoed back when the flow
	// completes.
	ClientState string
	// UserID is the signed-in user the request belongs to; no method will
	// resolve the request for anybody else.
	UserID           int
	ApprovedByUserID int
	State            OAuthAuthorizationState
	// Code is minted by Complete and consumed by Exchange.
	Code           string
	RequestIP      string
	CreateTime     time.Time
	ExpireTime     time.Time
	CodeExpireTime time.Time
}

OAuthAuthorizationRequest is one authorization request and, once Complete has run, the single-use code minted from it.

type OAuthAuthorizationRequestStore

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

OAuthAuthorizationRequestStore holds in-flight authorization requests and the authorization codes minted from them. Like every other cache in this package it is process-local, so a deployment with several replicas must pin a browser's authorization flow to a single replica (or front them with sticky routing).

func NewOAuthAuthorizationRequestStore

func NewOAuthAuthorizationRequestStore() *OAuthAuthorizationRequestStore

func (*OAuthAuthorizationRequestStore) Approve

func (s *OAuthAuthorizationRequestStore) Approve(requestID string, userID int, approve bool, now time.Time) error

Approve records the user's decision. Only a pending request can be decided, and a denial leaves ApprovedByUserID unset because nobody approved it.

func (*OAuthAuthorizationRequestStore) Complete

func (s *OAuthAuthorizationRequestStore) Complete(requestID string, userID int, now time.Time) (OAuthAuthorizationRequest, error)

Complete mints the authorization code for an approved request. The code is minted once: a retry is refused rather than handed a second code.

func (*OAuthAuthorizationRequestStore) Create

Create stores a new pending request. The id, the state and the code are the store's to assign, so a caller can neither seed an approved request nor name a code. A generated id that collides with a live request is redrawn, so the returned id is always fresh.

func (*OAuthAuthorizationRequestStore) Exchange

Exchange consumes an authorization code. The record is deleted here, so a code resolves exactly once and a replay reports it as unknown.

func (*OAuthAuthorizationRequestStore) Get

Get returns a pending request so the consent page can show what is about to be approved. A request is visible only to the user it was created for.

type OAuthAuthorizationState

type OAuthAuthorizationState string

OAuthAuthorizationState is where a pending request stands.

const (
	// OAuthAuthorizationPending waits for the signed-in user to approve or deny
	// it.
	OAuthAuthorizationPending OAuthAuthorizationState = "PENDING"
	// OAuthAuthorizationApproved can be completed into an authorization code.
	OAuthAuthorizationApproved OAuthAuthorizationState = "APPROVED"
	// OAuthAuthorizationDenied was refused by the user.
	OAuthAuthorizationDenied OAuthAuthorizationState = "DENIED"
)

type RevocationCache

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

RevocationCache remembers whether a token id has been revoked.

func (*RevocationCache) Lookup

func (c *RevocationCache) Lookup(tokenID string, now time.Time) (revoked, fresh bool)

Lookup returns the cached decision for a token id and whether it is still fresh. A stale entry is treated as unknown so the caller reads the table.

func (*RevocationCache) Remember

func (c *RevocationCache) Remember(tokenID string, revoked bool, now time.Time)

Remember records a decision read from the persistent table. It never downgrades a revoked decision already in the cache: a table read that started before a concurrent logout can finish after it, and overwriting the entry would serve the revoked token until the next read.

func (*RevocationCache) Revoke

func (c *RevocationCache) Revoke(tokenID string, now time.Time)

Revoke records a revocation this process just performed, so the token is refused immediately instead of after the next table read.

type State

type State struct {
	// TokenRevocationCache caches lookups in the persistent revoked_token table,
	// which is the authority; see RevocationCache.
	TokenRevocationCache *RevocationCache
	// InstanceOutstandingConnections counts the open connections per instance.
	InstanceOutstandingConnections *resourceLimiter
	// LoginLimiter throttles failed password logins per account and per source.
	LoginLimiter *LoginLimiter
	// LoginRequestLimiter bounds anonymous Login requests per source and
	// globally, before either the account limiter or bcrypt runs.
	LoginRequestLimiter *WindowLimiter
	// CreateUserRequestLimiter bounds anonymous CreateUser requests per source
	// and globally; each one checks email existence and hashes a password.
	CreateUserRequestLimiter *WindowLimiter
	// LogoutRequestLimiter bounds anonymous Logout requests per source and
	// globally; each one writes a ledger row even when the revocation it asks
	// for is a repeated no-op.
	LogoutRequestLimiter *WindowLimiter
	// PrincipalRequestLimiter bounds the ledger-writing calls of a signed-in
	// caller, keyed by principal and procedure; the ledger never shrinks, so its
	// growth is bounded for an authenticated caller too. See
	// PrincipalThrottleInterceptor.
	PrincipalRequestLimiter *WindowLimiter
	// SSOStateCache holds one-time OAuth2 state nonces issued by
	// CreateSSOState, mapped to their issue time.
	SSOStateCache *lru.Cache[string, time.Time]
	// SSOStateRequestLimiter bounds anonymous CreateSSOState requests per source
	// and globally, before the handler adds a nonce to SSOStateCache.
	SSOStateRequestLimiter *WindowLimiter
	// DeviceLoginStore holds the in-flight device authorization requests.
	DeviceLoginStore *DeviceLoginStore
	// DeviceLoginLimiter throttles CreateDeviceLogin per source address.
	DeviceLoginLimiter *WindowLimiter
	// DeviceLoginLookupLimiter throttles GetDeviceLogin and ApproveDeviceLogin
	// per caller, so holding an account does not buy unlimited guesses at
	// someone else's user code.
	DeviceLoginLookupLimiter *WindowLimiter
	// OAuthAuthorizationRequestStore holds the pending OAuth 2.1 authorization
	// requests and the single-use authorization codes minted from them.
	OAuthAuthorizationRequestStore *OAuthAuthorizationRequestStore
	// MCPCallLimiter throttles MCP tool calls per principal. The endpoint is a
	// remote entry point a model drives, and every call reads the registry and
	// writes a ledger row.
	MCPCallLimiter *WindowLimiter
	// MCPSourceCallLimiter bounds requests to the MCP endpoint per source
	// address, counted before the bearer check, so an unauthenticated probe is
	// bounded even though it never becomes a tool call.
	MCPSourceCallLimiter *WindowLimiter
}

func New

func New() (*State, error)

func (*State) AcquireInstanceConnection

func (s *State) AcquireInstanceConnection(instanceID string, maximumConnections int) (func(), error)

AcquireInstanceConnection reserves one of the instance's outstanding connection slots and returns the release function. A maximumConnections <= 0 means no limit. Every path that opens a driver for an instance — the periodic sync, an API-triggered sync and the connection test — goes through it, so a slow or hostile target cannot stack connections past the instance's ceiling.

type WindowLimiter

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

WindowLimiter is an in-memory fixed-window counter: a key's window opens with its first allowed request and lasts throttleWindow, so a full quota can land at the end of one window and another at the start of the next. A request is counted only when it is allowed, so hammering a refused key cannot keep pushing its window forward.

func (*WindowLimiter) Allow

func (l *WindowLimiter) Allow(source string, now time.Time) bool

Allow counts one request from source and reports whether it may proceed.

Jump to

Keyboard shortcuts

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