Documentation
¶
Index ¶
- Constants
- Variables
- func NormalizeUserCode(raw string) string
- type DeviceLogin
- type DeviceLoginState
- type DeviceLoginStore
- func (s *DeviceLoginStore) Approve(userCode string, approverUserID int, approve bool, now time.Time) (DeviceLogin, error)
- func (s *DeviceLoginStore) Create(login DeviceLogin, now time.Time) (DeviceLogin, error)
- func (s *DeviceLoginStore) Exchange(deviceCode string, now time.Time) (DeviceLogin, error)
- func (s *DeviceLoginStore) GetByUserCode(userCode string, now time.Time) (DeviceLogin, error)
- func (s *DeviceLoginStore) NewDeviceLogin(login DeviceLogin, now time.Time) (DeviceLogin, error)
- type LoginLimiter
- type OAuthAuthorizationRequest
- type OAuthAuthorizationRequestStore
- func (s *OAuthAuthorizationRequestStore) Approve(requestID string, userID int, approve bool, now time.Time) error
- func (s *OAuthAuthorizationRequestStore) Complete(requestID string, userID int, now time.Time) (OAuthAuthorizationRequest, error)
- func (s *OAuthAuthorizationRequestStore) Create(request OAuthAuthorizationRequest, now time.Time) (OAuthAuthorizationRequest, error)
- func (s *OAuthAuthorizationRequestStore) Exchange(code string, now time.Time) (OAuthAuthorizationRequest, error)
- func (s *OAuthAuthorizationRequestStore) Get(requestID string, userID int, now time.Time) (OAuthAuthorizationRequest, error)
- type OAuthAuthorizationState
- type RevocationCache
- type State
- type WindowLimiter
Constants ¶
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.
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.
const SSOStateTTL = 5 * time.Minute
SSOStateTTL is how long a one-time OAuth2 state nonce stays valid.
Variables ¶
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.
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.
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 ¶
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 ¶
func (s *OAuthAuthorizationRequestStore) Create(request OAuthAuthorizationRequest, now time.Time) (OAuthAuthorizationRequest, error)
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 ¶
func (s *OAuthAuthorizationRequestStore) Exchange(code string, now time.Time) (OAuthAuthorizationRequest, error)
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 ¶
func (s *OAuthAuthorizationRequestStore) Get(requestID string, userID int, now time.Time) (OAuthAuthorizationRequest, error)
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.
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 (*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.