auth

package
v0.1.35 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 66 Imported by: 0

Documentation

Index

Constants

View Source
const DelegatedProviderKeyLength = len(DelegatedProviderKeyPrefix) + sha256.Size*2

DelegatedProviderKeyLength is the fixed length of a delegated storage key: len("mcp:v1:") + 64 hex characters of an untruncated SHA-256 digest.

View Source
const DelegatedProviderKeyPrefix = "mcp:v1:"

DelegatedProviderKeyPrefix marks provider column values holding delegated (per-MCP-provider) tokens rather than workspace provider names.

View Source
const MCPLinkCSRFHeader = "X-Agently-Csrf"

MCPLinkCSRFHeader carries the per-session CSRF token required on the cookie-authenticated POST initiate and DELETE disconnect endpoints. Clients obtain the token from the status response.

Variables

View Source
var ErrOAuthStateInvalid = errors.New("oauth link state is invalid")

ErrOAuthStateInvalid is the single non-enumerable failure returned for every rejected state consume: absent, expired, replayed, cross-user and cross-session states are indistinguishable to callers. The classification is recorded in audit logs only.

Functions

func CanonicalWorkspaceProvider added in v0.1.29

func CanonicalWorkspaceProvider(cfg *Config, provider string) string

CanonicalWorkspaceProvider normalizes trusted workspace provider aliases ("", "jwt", "oauth", "default") to the configured workspace provider name. Delegated storage keys and unknown provider names pass through unchanged.

func DefineOAuthLinkStateComponents added in v0.1.29

func DefineOAuthLinkStateComponents(ctx context.Context, dao *datly.Service) error

DefineOAuthLinkStateComponents registers the read, create-or-get-pending, atomic consume and delete-expired linkstate components with datly.Service. Called during auth runtime initialization.

func DelegatedProviderStorageKey added in v0.1.29

func DelegatedProviderStorageKey(workspaceNamespace, providerRef string) string

DelegatedProviderStorageKey derives the fixed-length, globally unambiguous user_oauth_token.provider value for a delegated provider reference:

mcp:v1:<hex sha256(workspaceNamespace + NUL + providerRef)>

The digest is never truncated. workspaceNamespace is an immutable configured identifier; changing it requires an explicit token-key migration.

func EffectiveUserID

func EffectiveUserID(ctx context.Context) string

EffectiveUserID returns a stable user identifier from context. Delegates to the internal auth package.

func InjectTokens

func InjectTokens(ctx context.Context, tokens *scyauth.Token) context.Context

InjectTokens stores OAuth tokens in context so that MCPAuthToken and downstream MCP clients can forward the logged-in user's token. External auth middleware (outside this module) should call this after authenticating the user from a session or JWT.

func InjectUser

func InjectUser(ctx context.Context, subject string) context.Context

InjectUser stores user identity in context using the agently-core auth key. External middleware (outside this module) can call this to bridge their own auth context into the context key that EffectiveUserID reads.

func IsDelegatedProviderKey added in v0.1.29

func IsDelegatedProviderKey(provider string) bool

IsDelegatedProviderKey reports whether a provider column value is a delegated storage key. Delegated rows require exact lookup and per-provider broker routing; they must never be served by workspace fallback or sent to the workspace refresh broker.

func IsWorkspaceProviderAlias added in v0.1.29

func IsWorkspaceProviderAlias(cfg *Config, provider string) bool

IsWorkspaceProviderAlias reports whether provider names the workspace identity provider (directly or through a trusted legacy alias).

func MCPAuthToken

func MCPAuthToken(ctx context.Context, useIDToken bool) string

MCPAuthToken selects a single token string suitable for outbound MCP calls. Delegates to the internal auth package.

func NewCreatedByUserTokenProvider added in v0.1.7

func NewCreatedByUserTokenProvider(cfg *Config, dao *datly.Service) token.Provider

NewCreatedByUserTokenProvider returns a store-backed token provider suitable for scheduler created_by_user_id auth restoration. It only restores tokens already persisted in user_oauth_token; it does not enable any broader auth flow.

func NewTokenStoreAdapter

func NewTokenStoreAdapter(store TokenStore, users UserService) token.TokenStore

NewTokenStoreAdapter wraps a service/auth.TokenStore to satisfy token.TokenStore.

func NotifyMCPAuthChange added in v0.1.29

func NotifyMCPAuthChange(event MCPAuthChangeEvent)

NotifyMCPAuthChange publishes a credential change to every listener.

func OAuthScopesForHeadless added in v0.1.20

func OAuthScopesForHeadless(client *OAuthClient) []string

OAuthScopesForHeadless returns the grant a non-interactive user-owned runtime should request. A dedicated CLI scope wins; otherwise legacy browser-created schedules inherit the web scope, with mobile as the final compatibility fallback.

func Protect

func Protect(cfg *Config, sessions *Manager, opts ...ProtectOption) func(http.Handler) http.Handler

func ProtectWithTokenProvider

func ProtectWithTokenProvider(cfg *Config, sessions *Manager, tp token.Provider, opts ...ProtectOption) func(http.Handler) http.Handler

Protect returns middleware that extracts auth credentials from the request (Bearer token or session cookie) and populates the request context with authenticated user identity and tokens.

Requests to /v1/api/auth/* and OPTIONS are passed through without auth. When JWT auth is configured, Bearer tokens are cryptographically verified.

ProtectWithTokenProvider is like Protect but also stores session tokens in the given token.Provider so they are available for subsequent requests from that user.

func RegisterMCPAuthChangeListener added in v0.1.29

func RegisterMCPAuthChangeListener(listener func(MCPAuthChangeEvent))

RegisterMCPAuthChangeListener subscribes to delegated MCP credential changes. Wiring code (e.g. the executor builder) registers MCP manager pool eviction here.

func ValidateOAuthTokenScopes added in v0.1.22

func ValidateOAuthTokenScopes(expected []string, token *scyauth.Token) error

ValidateOAuthTokenScopes validates a newly obtained OAuth token against the exact scopes requested for that authorization flow.

func WithAuthExtensions added in v0.1.3

func WithAuthExtensions(base http.Handler, runtime *Runtime) http.Handler

func WithAuthProtection added in v0.1.3

func WithAuthProtection(base http.Handler, runtime *Runtime) http.Handler

Types

type CanonicalUserResolver added in v0.1.29

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

CanonicalUserResolver maps verified workspace identities to the canonical Agently users.id. It is shared by the session and bearer entry paths so both apply identical provider/subject checks; the bearer path must not implement a second, weaker subject-to-user mapping.

func NewCanonicalUserResolver added in v0.1.29

func NewCanonicalUserResolver(users UserService) *CanonicalUserResolver

NewCanonicalUserResolver creates a resolver over the given user service.

func (*CanonicalUserResolver) Invalidate added in v0.1.29

func (r *CanonicalUserResolver) Invalidate()

Invalidate drops all cached subject mappings. Call on provider reload and on local user-status changes (disable/delete).

func (*CanonicalUserResolver) ResolveCanonicalWorkspaceUser added in v0.1.29

func (r *CanonicalUserResolver) ResolveCanonicalWorkspaceUser(ctx context.Context, identity VerifiedWorkspaceIdentity) (string, error)

ResolveCanonicalWorkspaceUser returns the canonical users.id for a verified workspace identity. It accepts only already-verified identities, never creates or upserts a user, and fails closed (returns an error) when the canonical owner cannot be determined reliably.

type Config added in v0.1.3

type Config struct {
	Enabled                 bool     `yaml:"enabled" json:"enabled"`
	CookieName              string   `yaml:"cookieName" json:"cookieName"`
	SessionTTLHours         int      `yaml:"sessionTTLHours,omitempty" json:"sessionTTLHours,omitempty"`
	TokenRefreshLeadMinutes int      `yaml:"tokenRefreshLeadMinutes,omitempty" json:"tokenRefreshLeadMinutes,omitempty"`
	DefaultUsername         string   `yaml:"defaultUsername" json:"defaultUsername"`
	IpHashKey               string   `yaml:"ipHashKey" json:"ipHashKey"`
	TrustedProxies          []string `yaml:"trustedProxies" json:"trustedProxies"`
	RedirectPath            string   `yaml:"redirectPath" json:"redirectPath"`
	// WorkspaceNamespace is the immutable identifier mixed into delegated MCP
	// token storage keys. It is not a filesystem path or display name;
	// changing it requires an explicit token-key migration. Empty selects
	// "default".
	WorkspaceNamespace string `yaml:"workspaceNamespace,omitempty" json:"workspaceNamespace,omitempty"`
	// TokenEncryptionKey is the explicit (env-expandable) encryption key for
	// delegated MCP token storage. When empty, delegated storage falls back to
	// the legacy workspace OAuth client configURL-derived salt. It lets
	// JWT/local-auth workspaces (no OAuth client) use delegated MCP OAuth.
	TokenEncryptionKey string `yaml:"tokenEncryptionKey,omitempty" json:"tokenEncryptionKey,omitempty"`
	// StateEncryptionKey is the active AEAD key material for delegated MCP
	// OAuth callback state (env-expandable). When empty, the delegated token
	// encryption salt is used. StateEncryptionKeyPrevious keeps the retired
	// key decryptable for at least the state TTL plus clock skew after a
	// rotation; new state always seals with the active key.
	StateEncryptionKey         string `yaml:"stateEncryptionKey,omitempty" json:"stateEncryptionKey,omitempty"`
	StateEncryptionKeyPrevious string `yaml:"stateEncryptionKeyPrevious,omitempty" json:"stateEncryptionKeyPrevious,omitempty"`
	// MCPLinkStateTTLMinutes bounds the delegated MCP OAuth state lifetime;
	// values are clamped to the mandated 5–10 minute window (default 7).
	MCPLinkStateTTLMinutes int    `yaml:"mcpLinkStateTTLMinutes,omitempty" json:"mcpLinkStateTTLMinutes,omitempty"`
	OAuth                  *OAuth `yaml:"oauth" json:"oauth"`
	Local                  *Local `yaml:"local" json:"local"`
	JWT                    *JWT   `yaml:"jwt,omitempty" json:"jwt,omitempty"`
}

Config defines global authentication settings for public embedders.

func DecodeConfigFromRoot added in v0.1.8

func DecodeConfigFromRoot(root *wscfg.Root) (*Config, error)

DecodeConfigFromRoot decodes the `auth:` section from an already- loaded workspace Root. Returns (nil, nil) when the root is nil or the auth section is empty — callers treat that as "auth disabled". Env-template expansion and the "effectively empty" check live here so the two entry points (LoadConfig / DecodeConfigFromRoot) behave identically.

func LoadConfig added in v0.1.8

func LoadConfig(workspaceRoot string) (*Config, error)

LoadConfig reads `<workspaceRoot>/config.yaml` and decodes the `auth:` section into a *Config. Returns (nil, nil) when no auth section is present or all fields are zero.

Callers that have already loaded the workspace `Root` (e.g. the executor bootstrap that also reads `default:` / `mcpServer`) should prefer DecodeConfigFromRoot to avoid re-reading and re-parsing config.yaml.

func LoadWorkspaceConfig deprecated added in v0.1.3

func LoadWorkspaceConfig(workspaceRoot string) (*Config, error)

LoadWorkspaceConfig is a thin compatibility shim over LoadConfig.

Deprecated: use LoadConfig for new code, or DecodeConfigFromRoot when the workspace Root is already in hand. Kept so external callers that still reference the old name keep compiling; will be removed once the tree is fully migrated.

func (*Config) DelegatedTokenEncryptionSalt added in v0.1.29

func (c *Config) DelegatedTokenEncryptionSalt() string

DelegatedTokenEncryptionSalt returns the salt encrypting delegated MCP token rows: the explicit auth.tokenEncryptionKey when configured, otherwise the legacy workspace OAuth client configURL. Empty means delegated storage has no usable key; delegated configuration must then fail loudly rather than silently disable.

func (*Config) IsBearerAccepted added in v0.1.3

func (c *Config) IsBearerAccepted() bool

func (*Config) IsCookieAccepted added in v0.1.3

func (c *Config) IsCookieAccepted() bool

func (*Config) IsJWTAuth added in v0.1.3

func (c *Config) IsJWTAuth() bool

func (*Config) IsLocalAuth added in v0.1.3

func (c *Config) IsLocalAuth() bool

func (*Config) Validate added in v0.1.3

func (c *Config) Validate() error

type DatlyUserService added in v0.1.5

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

func NewDatlyUserService added in v0.1.5

func NewDatlyUserService(dao *datly.Service) *DatlyUserService

func (*DatlyUserService) GetByID added in v0.1.29

func (s *DatlyUserService) GetByID(ctx context.Context, id string) (*User, error)

GetByID resolves a canonical user by users.id, including its active status. It implements UserByIDLookup for delegated-credential active checks.

func (*DatlyUserService) GetBySubjectAndProvider added in v0.1.5

func (s *DatlyUserService) GetBySubjectAndProvider(ctx context.Context, subject, provider string) (*User, error)

func (*DatlyUserService) GetByUsername added in v0.1.5

func (s *DatlyUserService) GetByUsername(ctx context.Context, username string) (*User, error)

func (*DatlyUserService) UpdateHashIPByID added in v0.1.5

func (s *DatlyUserService) UpdateHashIPByID(ctx context.Context, id, hash string) error

func (*DatlyUserService) UpdatePreferences added in v0.1.5

func (s *DatlyUserService) UpdatePreferences(ctx context.Context, username string, patch *PreferencesPatch) error

func (*DatlyUserService) Upsert added in v0.1.5

func (s *DatlyUserService) Upsert(ctx context.Context, user *User) error

func (*DatlyUserService) UpsertWithProvider added in v0.1.5

func (s *DatlyUserService) UpsertWithProvider(ctx context.Context, username, displayName, email, provider, subject string) (string, error)

type DelegatedCredentialResolver added in v0.1.29

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

DelegatedCredentialResolver implements the viant/mcp CredentialResolver for Agently: canonical-user-keyed encrypted storage, validated workspace-token reuse, provider-exact refresh with distributed leases and CAS, and safe invalidation. Resolution returns outbound credentials only — it never installs values into the Agently authentication context.

func NewDelegatedCredentialResolver added in v0.1.29

func NewDelegatedCredentialResolver(cfg *Config, store DelegatedTokenStore, registry *providerregistry.Registry, namespace string) *DelegatedCredentialResolver

NewDelegatedCredentialResolver builds the resolver. namespace is the immutable workspace namespace used in storage-key derivation.

func (*DelegatedCredentialResolver) Invalidate added in v0.1.29

func (r *DelegatedCredentialResolver) Invalidate(ctx context.Context, requirement authcfg.Requirement) error

Invalidate implements config.CredentialResolver. It deletes only the exact canonical-user/provider row. Unknown providers or a missing canonical owner must never delete credentials.

func (*DelegatedCredentialResolver) MaxRefreshLead added in v0.1.29

func (r *DelegatedCredentialResolver) MaxRefreshLead(ctx context.Context) time.Duration

MaxRefreshLead implements DelegatedTokenRefresher.

func (*DelegatedCredentialResolver) Refresh added in v0.1.29

Refresh implements config.CredentialResolver. Per contract it bypasses any cached access token and mints a fresh credential while retaining the stored refresh credential.

func (*DelegatedCredentialResolver) RefreshStoredDelegatedToken added in v0.1.29

func (r *DelegatedCredentialResolver) RefreshStoredDelegatedToken(ctx context.Context, stored *OAuthToken) error

RefreshStoredDelegatedToken implements DelegatedTokenRefresher for the background watcher. Unknown providers, malformed metadata and disabled providers are skipped without modifying stored credentials; provider failures are isolated to the row.

func (*DelegatedCredentialResolver) Resolve added in v0.1.29

Resolve implements config.CredentialResolver.

type DelegatedMCPAuth added in v0.1.29

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

DelegatedMCPAuth bundles the workspace OAuth provider registry and the Agently credential resolver installed into viant/mcp for MCP definitions with auth.mode=oauth. Legacy MCP auth configurations are unaffected: the manager installs these only for delegated configs.

func NewDelegatedMCPAuth added in v0.1.29

func NewDelegatedMCPAuth(cfg *Config, dao *datly.Service) *DelegatedMCPAuth

NewDelegatedMCPAuth builds the delegated MCP auth stack over the encrypted user_oauth_token store. It returns nil only when the required persistence layer (datly DAO) is unavailable — delegated configs then fail loudly at client creation because no credential resolver is installed. When storage exists but no encryption key can be derived (neither auth.tokenEncryptionKey nor a workspace OAuth client configURL), the resolver is installed in a fail-loud state: every delegated resolution returns an actionable configuration error instead of silently disabling delegated auth.

func (*DelegatedMCPAuth) CleanupDelegatedCredentials added in v0.1.29

func (d *DelegatedMCPAuth) CleanupDelegatedCredentials(ctx context.Context, canonicalUserID string) error

CleanupDelegatedCredentials is the user-lifecycle hook for deactivation and deletion: it deletes every delegated (mcp:v1) token row belonging to the canonical user after attempting best-effort provider-side revocation. Providers without revocation support are audited and cleaned locally.

func (*DelegatedMCPAuth) ClearResolverCooldown added in v0.1.29

func (d *DelegatedMCPAuth) ClearResolverCooldown(canonicalUserID, storageKey string)

ClearResolverCooldown drops the refresh retry cooldown for one canonical user and delegated storage key on this instance. Used by cache-invalidation wiring so a re-link on another resolver instance takes effect immediately.

func (*DelegatedMCPAuth) ProviderRegistry added in v0.1.29

func (d *DelegatedMCPAuth) ProviderRegistry() *providerregistry.Registry

ProviderRegistry exposes the Agently registry surface (kill switches, fingerprint, refresh leads).

func (*DelegatedMCPAuth) Registry added in v0.1.29

Registry exposes the generic viant/mcp provider registry.

func (*DelegatedMCPAuth) Resolver added in v0.1.29

Resolver exposes the generic viant/mcp credential resolver.

func (*DelegatedMCPAuth) SetUserLookup added in v0.1.29

func (d *DelegatedMCPAuth) SetUserLookup(users interface{})

SetUserLookup installs the canonical by-ID active-status lookup: with it in place, disabled or deleted canonical users cannot resolve or refresh delegated credentials. Any UserService implementing UserByIDLookup applies.

func (*DelegatedMCPAuth) TokenRefresher added in v0.1.29

func (d *DelegatedMCPAuth) TokenRefresher() DelegatedTokenRefresher

TokenRefresher exposes the watcher-facing delegated refresh surface.

type DelegatedTokenLister added in v0.1.29

type DelegatedTokenLister interface {
	ListDelegated(ctx context.Context, userID string) ([]*OAuthToken, error)
}

DelegatedTokenLister is the optional listing surface used by the user-lifecycle cleanup hook to enumerate a canonical user's delegated rows.

type DelegatedTokenRefresher added in v0.1.29

type DelegatedTokenRefresher interface {
	// RefreshStoredDelegatedToken applies the row's provider-specific refresh
	// policy and, when due, refreshes through that provider's exact
	// client/token endpoint using the distributed lease and CAS write.
	RefreshStoredDelegatedToken(ctx context.Context, stored *OAuthToken) error
	// MaxRefreshLead returns the largest configured provider refresh lead used
	// as the watcher's broad scan horizon.
	MaxRefreshLead(ctx context.Context) time.Duration
}

DelegatedTokenRefresher lets the background watcher refresh delegated rows through their exact provider broker. Until installed, delegated rows are skipped without mutation.

type DelegatedTokenStore added in v0.1.29

type DelegatedTokenStore interface {
	GetExact(ctx context.Context, userID, provider string) (*OAuthToken, error)
	Put(ctx context.Context, token *OAuthToken) error
	Delete(ctx context.Context, userID, provider string) error
	TryAcquireRefreshLease(ctx context.Context, userID, provider, owner string, ttl time.Duration) (version int64, acquired bool, err error)
	ReleaseRefreshLease(ctx context.Context, userID, provider, owner string) error
	CASPut(ctx context.Context, token *OAuthToken, expectedVersion int64, owner string) (swapped bool, err error)
}

DelegatedTokenStore is the exact, canonical-user-keyed persistence surface used for delegated MCP credentials. It deliberately excludes the legacy fallback Get: delegated storage keys require exact matches and delegated owner resolution uses CanonicalUserID directly, never resolveOAuthTokenOwnerID.

type ExpiringTokenScanner added in v0.1.7

type ExpiringTokenScanner interface {
	// ScanExpiring returns all stored tokens whose expiry is before horizon
	// and that carry a refresh token. Only tokens that can actually be
	// refreshed need to be returned.
	ScanExpiring(ctx context.Context, horizon time.Time) ([]*OAuthToken, error)
}

ExpiringTokenScanner is an optional extension of TokenStore for implementations that can efficiently query the store for tokens expiring before a given horizon. It is used by the background refresh watcher to proactively refresh tokens for users who are idle (no active in-memory session) before they expire. TokenStore implementations that do not embed a queryable DB may omit this.

type Handler

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

Handler serves auth-related HTTP endpoints. It is designed to be mounted under /v1/api/auth/* on the application router.

func NewHandler

func NewHandler(cfg *Config, sessions *Manager, opts ...HandlerOption) *Handler

NewHandler creates an auth HTTP handler.

func (*Handler) Register

func (h *Handler) Register(mux *http.ServeMux)

Register mounts auth routes on the given mux.

func (*Handler) RegisterPreferences

func (h *Handler) RegisterPreferences(mux *http.ServeMux)

RegisterPreferences mounts preference endpoints on the given mux. These endpoints require the auth middleware to populate user context.

type HandlerOption

type HandlerOption func(*Handler)

HandlerOption customises the auth Handler.

func WithTokenProvider

func WithTokenProvider(tp token.Provider) HandlerOption

WithTokenProvider injects a shared token lifecycle manager.

func WithTokenStore

func WithTokenStore(ts TokenStore) HandlerOption

WithTokenStore injects an OAuth token store.

func WithUserService

func WithUserService(us UserService) HandlerOption

WithUserService injects a user service.

type JWT added in v0.1.3

type JWT struct {
	Enabled       bool     `yaml:"enabled" json:"enabled"`
	RSA           []string `yaml:"rsa,omitempty" json:"rsa,omitempty"`
	HMAC          string   `yaml:"hmac,omitempty" json:"hmac,omitempty"`
	CertURL       string   `yaml:"certURL,omitempty" json:"certURL,omitempty"`
	RSAPrivateKey string   `yaml:"rsaPrivateKey,omitempty" json:"rsaPrivateKey,omitempty"`
}

type JWTService

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

JWTService wraps scy's JWT signer and verifier for the agently-core auth layer.

func NewJWTService

func NewJWTService(cfg *JWT) *JWTService

NewJWTService creates a JWT service from the given config. Call Init() before use to load keys.

func NewJWTServiceFromConfigValues added in v0.1.1

func NewJWTServiceFromConfigValues(enabled bool, rsa []string, hmac, certURL, rsaPrivateKey string) *JWTService

NewJWTServiceFromConfigValues creates a JWT service from raw config values for callers outside agently-core that cannot import the internal auth package.

func (*JWTService) Init

func (j *JWTService) Init(ctx context.Context) error

Init loads keys from scy resources. Safe to call multiple times.

func (*JWTService) PublicKeys

func (j *JWTService) PublicKeys() (map[string]*rsa.PublicKey, error)

PublicKeys returns the loaded RSA public keys (for diagnostics/testing).

func (*JWTService) Sign

func (j *JWTService) Sign(ttl time.Duration, claims interface{}) (string, error)

Sign creates a signed JWT token with the given claims and TTL. Returns an error if no signer is configured.

func (*JWTService) Verify

func (j *JWTService) Verify(ctx context.Context, tokenString string) (*UserInfo, error)

Verify validates a JWT token string and returns the parsed claims. Returns an error if the token is invalid, expired, or signature verification fails.

type Local added in v0.1.3

type Local struct {
	Enabled bool `yaml:"enabled" json:"enabled"`
}

type MCPAuthChangeEvent added in v0.1.29

type MCPAuthChangeEvent struct {
	// Kind is one of "linked", "disconnected".
	Kind string
	// CanonicalUserID keys credential storage and resolver cooldowns.
	CanonicalUserID string
	// EffectiveUserID keys the MCP manager client pool.
	EffectiveUserID string
	ServerName      string
	ProviderRef     string
	StorageKey      string
}

MCPAuthChangeEvent describes a delegated MCP credential change (link, disconnect, invalid_grant). Subscribers evict caches only — the event never changes EffectiveUserID, sessions or the workspace provider.

type MCPAuthConfigProvider added in v0.1.29

type MCPAuthConfigProvider interface {
	Options(ctx context.Context, serverName string) (*mcpcfg.MCPClient, error)
}

MCPAuthConfigProvider yields MCP client configurations for the hosted link endpoints. The MCP manager's repository provider satisfies it; the runtime falls back to a workspace repository loader when none is installed.

type MCPAuthState added in v0.1.29

type MCPAuthState struct {
	CanonicalUserID string    `json:"canonicalUserId"`
	SessionIDHash   string    `json:"sessionIdHash"`
	ServerName      string    `json:"serverName"`
	ProviderRef     string    `json:"providerRef"`
	ClientRef       string    `json:"clientRef,omitempty"`
	Resource        string    `json:"resource,omitempty"`
	Scopes          []string  `json:"scopes,omitempty"`
	CodeVerifier    string    `json:"codeVerifier"`
	ReturnURL       string    `json:"returnURL,omitempty"`
	Nonce           string    `json:"nonce"`
	ExpiresAt       time.Time `json:"expiresAt"`
	// ConfigFingerprint records the non-secret provider-registry fingerprint
	// at initiation; the callback verifies the configuration has not changed
	// underneath the flow.
	ConfigFingerprint string `json:"configFingerprint,omitempty"`
	// RedirectURI pins the exact redirect used on the authorization request so
	// the token exchange sends the identical value.
	RedirectURI string `json:"redirectURI,omitempty"`
}

MCPAuthState is the encrypted callback state payload for delegated MCP OAuth. It round-trips through the browser only as AEAD ciphertext; the database stores only its hash. The PKCE verifier lives exclusively here.

type Manager

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

Manager manages user sessions with an in-memory cache and optional persistent store.

func NewManager

func NewManager(ttl time.Duration, store SessionStore) *Manager

NewManager creates a session manager with the given TTL. If store is nil, sessions are stored only in memory.

func (*Manager) ActiveSessions

func (m *Manager) ActiveSessions() []*Session

ActiveSessions returns a snapshot of all non-expired sessions in memory.

func (*Manager) Delete

func (m *Manager) Delete(ctx context.Context, id string)

Delete removes a session.

func (*Manager) Get

func (m *Manager) Get(ctx context.Context, id string) *Session

Get retrieves a session by ID. Returns nil if not found or expired.

func (*Manager) Put

func (m *Manager) Put(ctx context.Context, s *Session)

Put stores a session.

func (*Manager) PutAsync added in v0.1.8

func (m *Manager) PutAsync(ctx context.Context, s *Session)

PutAsync stores the session in memory immediately and persists it to the backing store out-of-band. Use this on latency-sensitive HTTP auth paths where the request should not block on durable session persistence.

type OAuth added in v0.1.3

type OAuth struct {
	Mode          string       `yaml:"mode" json:"mode"`
	Name          string       `yaml:"name" json:"name"`
	Label         string       `yaml:"label" json:"label"`
	UsePopupLogin bool         `yaml:"usePopupLogin,omitempty" json:"usePopupLogin,omitempty"`
	Client        *OAuthClient `yaml:"client" json:"client"`
}

type OAuthClient added in v0.1.3

type OAuthClient struct {
	ConfigURL      string   `yaml:"configURL" json:"configURL"`
	DiscoveryURL   string   `yaml:"discoveryURL" json:"discoveryURL"`
	JWKSURL        string   `yaml:"jwksURL" json:"jwksURL"`
	RedirectURI    string   `yaml:"redirectURI" json:"redirectURI"`
	RedirectURIs   []string `yaml:"redirectURIs" json:"redirectURIs"`
	ClientID       string   `yaml:"clientID" json:"clientID"`
	Scopes         []string `yaml:"scopes" json:"scopes"`
	WebUIScopes    []string `yaml:"webUIScopes,omitempty" json:"webUIScopes,omitempty"`
	MobileUIScopes []string `yaml:"mobileUIScopes,omitempty" json:"mobileUIScopes,omitempty"`
	CLIScopes      []string `yaml:"cliScopes,omitempty" json:"cliScopes,omitempty"`
	Issuer         string   `yaml:"issuer" json:"issuer"`
	Audiences      []string `yaml:"audiences" json:"audiences"`
}

type OAuthStateRecord added in v0.1.29

type OAuthStateRecord struct {
	StateHash       string
	FlowHash        string
	CanonicalUserID string
	SessionHash     string
	Provider        string
	ExpiresAt       time.Time
	ConsumedAt      *time.Time
	CreatedAt       time.Time
}

OAuthStateRecord is the non-secret oauth_link_state row. It never carries authorization codes, PKCE verifiers, client secrets or tokens; the encrypted state blob itself stays with the flow owner (browser round-trip) only.

func (*OAuthStateRecord) Pending added in v0.1.29

func (r *OAuthStateRecord) Pending(now time.Time) bool

Pending reports whether the record is unconsumed and unexpired at now.

type OAuthStateStore added in v0.1.29

type OAuthStateStore interface {
	// CreateOrGetPending returns the stored flow row and whether this call
	// created (or replaced a consumed/expired) row. The creator owns the flow
	// and receives the authorization URL; concurrent callers must poll.
	CreateOrGetPending(ctx context.Context, record *OAuthStateRecord) (stored *OAuthStateRecord, created bool, err error)
	// Consume atomically transitions the state to consumed. It fails with
	// ErrOAuthStateInvalid when the record is absent, expired, already
	// consumed, owned by another canonical user or bound to another session.
	Consume(ctx context.Context, stateHash, canonicalUserID, sessionHash string) error
	// DeleteExpired removes rows expired at or before the horizon and reports
	// the deleted count plus the oldest removed expiry for metrics.
	DeleteExpired(ctx context.Context, before time.Time) (deleted int64, oldestExpiresAt time.Time, err error)
}

OAuthStateStore is the distributed single-use OAuth state surface used by the MCP link endpoints. Implementations must provide cross-pod CreateOrGetPending deduplication and an atomic pending-to-consumed transition; encryption alone does not make state single-use.

type OAuthStateStoreDatly added in v0.1.29

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

OAuthStateStoreDatly implements OAuthStateStore exclusively through the registered Datly linkstate components: no raw database/sql access happens here or in any HTTP handler above it.

func NewOAuthStateStoreDatly added in v0.1.29

func NewOAuthStateStoreDatly(dao *datly.Service) *OAuthStateStoreDatly

NewOAuthStateStoreDatly creates the Datly-backed state store adapter.

func (*OAuthStateStoreDatly) Consume added in v0.1.29

func (s *OAuthStateStoreDatly) Consume(ctx context.Context, stateHash, canonicalUserID, sessionHash string) error

func (*OAuthStateStoreDatly) CreateOrGetPending added in v0.1.29

func (s *OAuthStateStoreDatly) CreateOrGetPending(ctx context.Context, record *OAuthStateRecord) (*OAuthStateRecord, bool, error)

func (*OAuthStateStoreDatly) DeleteExpired added in v0.1.29

func (s *OAuthStateStoreDatly) DeleteExpired(ctx context.Context, before time.Time) (int64, time.Time, error)

func (*OAuthStateStoreDatly) GetPending added in v0.1.29

func (s *OAuthStateStoreDatly) GetPending(ctx context.Context, flowHash string) (*OAuthStateRecord, error)

GetPending returns the unexpired pending record for a flow hash (status polling); nil when none exists.

type OAuthToken

type OAuthToken struct {
	Username     string    `json:"username"`
	Provider     string    `json:"provider"`
	AccessToken  string    `json:"accessToken"`
	IDToken      string    `json:"idToken,omitempty"`
	RefreshToken string    `json:"refreshToken,omitempty"`
	ExpiresAt    time.Time `json:"expiresAt,omitempty"`

	// Issuer is the normalized OAuth issuer the token was granted by.
	Issuer string `json:"issuer,omitempty"`
	// Resource is the protected resource (audience) the token targets.
	Resource string `json:"resource,omitempty"`
	// Scopes are the granted scopes (authoritative after refresh).
	Scopes []string `json:"scopes,omitempty"`
	// TokenType records accessToken versus idToken usage intent.
	TokenType string `json:"tokenType,omitempty"`
	// Subject is the provider subject extracted from the verified token; it is
	// credential metadata only and never becomes the effective user.
	Subject string `json:"subject,omitempty"`
	// ProviderRef/ClientRef record the workspace provider registry references
	// for delegated tokens (the row provider column stores a fixed-length
	// hashed storage key).
	ProviderRef string `json:"providerRef,omitempty"`
	ClientRef   string `json:"clientRef,omitempty"`
	// IDTokenExpiresAt is the verified ID-token exp. ExpiresAt remains the
	// access-token expiry for compatibility; tokenType=idToken consumers must
	// derive validity and refresh thresholds from this field instead.
	IDTokenExpiresAt time.Time `json:"idTokenExpiresAt,omitempty"`
	// IssuedAt records when the token set was obtained (login, exchange or
	// refresh); the refresh policy uses it to derive the original selected
	// token lifetime for the 20% clamp.
	IssuedAt time.Time `json:"issuedAt,omitempty"`
}

OAuthToken represents a stored OAuth token set for a user/provider pair. The metadata fields (Issuer, Resource, Scopes, TokenType, Subject, ProviderRef, ClientRef) are optional for backward compatibility with legacy workspace rows; delegated (per-provider MCP) tokens always populate them and every conversion/refresh path must preserve them.

func (*OAuthToken) HasDelegatedMetadata added in v0.1.29

func (t *OAuthToken) HasDelegatedMetadata() bool

HasDelegatedMetadata reports whether the token carries the delegated credential metadata that refresh/CAS paths must not drop.

func (*OAuthToken) MergeMetadataFrom added in v0.1.29

func (t *OAuthToken) MergeMetadataFrom(prior *OAuthToken)

MergeMetadataFrom copies missing metadata fields from prior. Populated fields on the receiver win (e.g. an authoritative refreshed scope set).

type PreferencesPatch

type PreferencesPatch struct {
	DisplayName        *string                           `json:"displayName,omitempty"`
	Timezone           *string                           `json:"timezone,omitempty"`
	DefaultAgentRef    *string                           `json:"defaultAgentRef,omitempty"`
	DefaultModelRef    *string                           `json:"defaultModelRef,omitempty"`
	DefaultEmbedderRef *string                           `json:"defaultEmbedderRef,omitempty"`
	AgentPrefs         map[string]map[string]interface{} `json:"agentPrefs,omitempty"`
}

PreferencesPatch describes a partial update to user preferences.

type ProtectOption

type ProtectOption func(*protectConfig)

ProtectOption customises the Protect middleware.

func WithJWTService

func WithJWTService(j *JWTService) ProtectOption

WithJWTService injects a pre-initialised JWTService for Bearer token verification.

type Runtime added in v0.1.3

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

func NewRuntime added in v0.1.3

func NewRuntime(ctx context.Context, workspaceRoot string, dao *datly.Service) (*Runtime, error)

func (*Runtime) JWTService added in v0.1.3

func (r *Runtime) JWTService() *JWTService

func (*Runtime) SetDelegatedTokenRefresher added in v0.1.29

func (r *Runtime) SetDelegatedTokenRefresher(refresher DelegatedTokenRefresher)

SetDelegatedTokenRefresher installs the delegated (per-provider) refresher used by the background watcher for rows stored under delegated provider keys. Until it is installed, delegated rows are skipped without mutation.

func (*Runtime) SetMCPAuthConfigProvider added in v0.1.29

func (r *Runtime) SetMCPAuthConfigProvider(provider MCPAuthConfigProvider)

SetMCPAuthConfigProvider installs a shared MCP configuration provider (e.g. the MCP manager's repository provider) for the hosted link endpoints.

type Session

type Session struct {
	ID       string         `json:"id"`
	UserID   string         `json:"userId,omitempty"`
	Username string         `json:"username"`
	Email    string         `json:"email,omitempty"`
	Subject  string         `json:"subject,omitempty"`
	Provider string         `json:"provider,omitempty"`
	Scopes   []string       `json:"scopes,omitempty"`
	Tokens   *scyauth.Token `json:"-"`
	// TransientRefreshRetryAt suppresses repeated refresh attempts/log spam
	// after a temporary token-endpoint failure. Runtime auth code persists the
	// cooldown through session metadata when a durable store is configured.
	TransientRefreshRetryAt time.Time `json:"-"`
	CreatedAt               time.Time `json:"createdAt"`
	ExpiresAt               time.Time `json:"expiresAt"`
}

Session represents an authenticated user session.

Identity model:

  • UserID = canonical agently users.id when available
  • Subject = raw oauth/jwt subject
  • Username = jwt.preferred_username or jwt.name — display name only
  • Email = jwt.email — display / contact only

Identity split:

  • sess.UserID is the canonical users.id UUID for internal persistence, token storage, token refresh, and session bookkeeping.
  • request EffectiveUserID is the provider subject/email/username used by ownership and visibility filters, including created_by_user_id.

Do not feed sess.EffectiveUserID() into request context unless created_by_user_id has been migrated to canonical users.id. Token persistence can use the canonical UserID, but request-scoped ownership must remain subject-compatible.

func (*Session) EffectiveUserID added in v0.1.7

func (s *Session) EffectiveUserID() string

EffectiveUserID returns the canonical session identity when available. This is intended for session/token persistence. For request-scoped ownership or visibility checks, use Subject/Email/Username directly; those filters still compare against subject-based created_by_user_id values.

func (*Session) IsExpired

func (s *Session) IsExpired() bool

IsExpired returns true when the session has passed its expiry time.

type SessionRecord

type SessionRecord struct {
	ID             string    `json:"id"`
	UserID         string    `json:"userId,omitempty"`
	Username       string    `json:"username"`
	Email          string    `json:"email,omitempty"`
	Subject        string    `json:"subject,omitempty"`
	Provider       string    `json:"provider,omitempty"`
	Scopes         []string  `json:"scopes,omitempty"`
	AccessToken    string    `json:"accessToken,omitempty"`
	IDToken        string    `json:"idToken,omitempty"`
	RefreshToken   string    `json:"refreshToken,omitempty"`
	TokenExpiresAt time.Time `json:"tokenExpiresAt,omitempty"`
	CreatedAt      time.Time `json:"createdAt"`
	ExpiresAt      time.Time `json:"expiresAt"`
}

SessionRecord is the persistent form of a session for external stores.

type SessionStore

type SessionStore interface {
	Get(ctx context.Context, id string) (*SessionRecord, error)
	Upsert(ctx context.Context, rec *SessionRecord) error
	Delete(ctx context.Context, id string) error
}

SessionStore is a pluggable backend for persistent session storage.

type SessionStoreDAO

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

SessionStoreDAO uses Datly to persist sessions.

func NewSessionStoreDAO

func NewSessionStoreDAO(dao *datly.Service) *SessionStoreDAO

NewSessionStoreDAO constructs a Datly-backed session store.

func (*SessionStoreDAO) Delete

func (s *SessionStoreDAO) Delete(ctx context.Context, id string) error

Delete removes a session by id.

func (*SessionStoreDAO) Get

Get loads a session by id.

func (*SessionStoreDAO) Upsert

func (s *SessionStoreDAO) Upsert(ctx context.Context, rec *SessionRecord) error

Upsert inserts or updates a session record.

type TokenStore

type TokenStore interface {
	Get(ctx context.Context, username, provider string) (*OAuthToken, error)
	Put(ctx context.Context, token *OAuthToken) error
	Delete(ctx context.Context, username, provider string) error

	// TryAcquireRefreshLease atomically attempts to acquire a distributed lease
	// for refreshing the token identified by (username, provider).
	TryAcquireRefreshLease(ctx context.Context, username, provider, owner string, ttl time.Duration) (version int64, acquired bool, err error)

	// ReleaseRefreshLease releases a previously acquired lease.
	ReleaseRefreshLease(ctx context.Context, username, provider, owner string) error

	// CASPut atomically updates the token only if the current version matches
	// expectedVersion and the lease is held by owner.
	CASPut(ctx context.Context, token *OAuthToken, expectedVersion int64, owner string) (swapped bool, err error)
}

TokenStore abstracts encrypted OAuth token persistence. Implementations may use scy-backed secrets, database storage, etc.

type TokenStoreDAO

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

TokenStoreDAO is a Datly-backed TokenStore with Blowfish encryption.

func NewTokenStoreDAO

func NewTokenStoreDAO(dao *datly.Service, salt string, opts ...TokenStoreOption) *TokenStoreDAO

NewTokenStoreDAO creates a Datly-backed token store.

func (*TokenStoreDAO) CASPut

func (s *TokenStoreDAO) CASPut(ctx context.Context, token *OAuthToken, expectedVersion int64, owner string) (bool, error)

CASPut atomically updates the token only if the current version matches expectedVersion and the lease is held by owner. On success, bumps version and clears the lease. Returns (true, nil) if the swap succeeded.

func (*TokenStoreDAO) Delete

func (s *TokenStoreDAO) Delete(ctx context.Context, username, provider string) error

Delete atomically clears a token while retaining its audit row.

func (*TokenStoreDAO) Get

func (s *TokenStoreDAO) Get(ctx context.Context, username, provider string) (*OAuthToken, error)

Get loads and decrypts a token from DB.

func (*TokenStoreDAO) GetExact added in v0.1.29

func (s *TokenStoreDAO) GetExact(ctx context.Context, username, provider string) (*OAuthToken, error)

GetExact loads and decrypts the token stored under exactly (username, provider). Unlike Get it never falls back to another provider row: a miss is a miss. Delegated MCP credential code must use this method exclusively.

func (*TokenStoreDAO) ListDelegated added in v0.1.29

func (s *TokenStoreDAO) ListDelegated(ctx context.Context, userID string) ([]*OAuthToken, error)

ListDelegated returns every delegated (mcp:v1) token row stored for a canonical user. It implements DelegatedTokenLister for the user-lifecycle cleanup hook; workspace rows are never included.

func (*TokenStoreDAO) Put

func (s *TokenStoreDAO) Put(ctx context.Context, token *OAuthToken) error

Put encrypts and saves a token via the Datly write handler.

func (*TokenStoreDAO) ReleaseRefreshLease

func (s *TokenStoreDAO) ReleaseRefreshLease(ctx context.Context, username, provider, owner string) error

ReleaseRefreshLease releases a previously acquired lease, resetting the row to idle. The owner check ensures we only release our own lease.

func (*TokenStoreDAO) ScanExpiring added in v0.1.7

func (s *TokenStoreDAO) ScanExpiring(ctx context.Context, horizon time.Time) ([]*OAuthToken, error)

ScanExpiring returns all stored tokens expiring before horizon that carry a refresh token. Called by the background watcher to refresh tokens for idle users who have no active in-memory session.

func (*TokenStoreDAO) TryAcquireRefreshLease

func (s *TokenStoreDAO) TryAcquireRefreshLease(ctx context.Context, username, provider, owner string, ttl time.Duration) (int64, bool, error)

TryAcquireRefreshLease atomically attempts to acquire a distributed lease for refreshing the token identified by (username, provider). The lease is granted only when the row is idle or has an expired lease. All timestamp comparisons use the DB server's CURRENT_TIMESTAMP to avoid clock-skew issues.

func (*TokenStoreDAO) ValidateProviderColumnWidth added in v0.1.29

func (s *TokenStoreDAO) ValidateProviderColumnWidth(ctx context.Context, width int) error

ValidateProviderColumnWidth verifies the live user_oauth_token.provider column can hold at least width characters (the fixed 71-character delegated storage key). MySQL silently truncating a delegated key is never accepted; dialects without declared column widths (sqlite) pass.

type TokenStoreOption added in v0.1.29

type TokenStoreOption func(*TokenStoreDAO)

TokenStoreOption configures a TokenStoreDAO.

func WithDelegatedSalt added in v0.1.29

func WithDelegatedSalt(salt string) TokenStoreOption

WithDelegatedSalt sets the encryption salt used for delegated (mcp:v1) token rows. Workspace rows keep using the base salt.

type User

type User struct {
	ID          string                 `json:"id,omitempty"`
	Username    string                 `json:"username"`
	Email       string                 `json:"email,omitempty"`
	DisplayName string                 `json:"displayName,omitempty"`
	Provider    string                 `json:"provider,omitempty"`
	Subject     string                 `json:"subject,omitempty"`
	Preferences map[string]interface{} `json:"preferences,omitempty"`
	// Disabled marks a deactivated canonical user: disabled users cannot use
	// stored delegated credentials and fail canonical resolution.
	Disabled bool `json:"disabled,omitempty"`
}

User represents a registered user.

type UserByIDLookup added in v0.1.29

type UserByIDLookup interface {
	GetByID(ctx context.Context, id string) (*User, error)
}

UserByIDLookup is the optional by-ID active-status lookup used by the delegated credential resolver, the background refresh watcher and the link endpoints: a disabled or deleted canonical user cannot use delegated tokens. It is a separate interface so existing UserService implementations remain compatible.

type UserInfo added in v0.1.3

type UserInfo struct {
	Subject string
	Email   string
}

UserInfo carries minimal identity extracted from auth context or JWT claims.

func RuntimeUserFromContext added in v0.1.3

func RuntimeUserFromContext(ctx context.Context) *UserInfo

type UserService

type UserService interface {
	GetByUsername(ctx context.Context, username string) (*User, error)
	GetBySubjectAndProvider(ctx context.Context, subject, provider string) (*User, error)
	Upsert(ctx context.Context, user *User) error
	UpsertWithProvider(ctx context.Context, username, displayName, email, provider, subject string) (string, error)
	UpdateHashIPByID(ctx context.Context, id, hash string) error
	UpdatePreferences(ctx context.Context, username string, patch *PreferencesPatch) error
}

UserService abstracts user CRUD. Implementations may use Datly, SQL, or an in-memory store.

type VerifiedWorkspaceIdentity added in v0.1.29

type VerifiedWorkspaceIdentity struct {
	Provider string
	Issuer   string
	Subject  string
	Email    string
}

VerifiedWorkspaceIdentity carries an already-verified workspace identity. Both session and bearer authentication paths must build this value only after token/session verification and pass it to the shared canonical resolver; the resolver never verifies credentials itself.

Directories

Path Synopsis
Package providerregistry implements the workspace OAuth provider registry.
Package providerregistry implements the workspace OAuth provider registry.

Jump to

Keyboard shortcuts

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