auth

package
v0.116.3 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package auth provides a lightweight, role-based access control layer for kojo's HTTP API. Its purpose is "spoiler prevention" — keeping an agent from incidentally reading other agents' Persona / configuration when it curls the API on its own. It is NOT a security boundary against a malicious agent: an agent runs as the same OS user as kojo itself and can read other agents' files directly. See README for the threat model.

Index

Constants

View Source
const HeaderNoIdempotencyCache = "X-Kojo-No-Idempotency-Cache"

HeaderNoIdempotencyCache is the response header any middleware sets to tell the idempotency wrapper NOT to save the captured response. Used by AgentFencing for its 409 wrong_holder response: the underlying lock state is transient, so caching the refusal would shadow a future successful retry.

Variables

View Source
var ErrNodeKeyResolverNotReady = errors.New("auth: tsnet node key resolver not ready")

ErrNodeKeyResolverNotReady is returned by a TailnetIdentityConfig.Resolver to signal "the tsnet/tailscaled handle is not wired yet" — distinct from a real WhoIs failure or a genuinely unknown caller. The middleware uses it to refuse Hub-mode Owner-fallback during the brief window between server start and resolver wiring; a real `lc.WhoIs` error (tailscaled blip) or empty `w.Node` (tsnet view stale) still falls back to Owner so the operator's Dashboard keeps working through transient outages.

Callers that build the resolver closure should return ("", ErrNodeKeyResolverNotReady) when the underlying handle is nil instead of returning ("", nil) — the latter is reserved for "WhoIs returned no node" and is honored by Hub fallback.

View Source
var ErrTokenRawUnavailable = errors.New("auth: agent token raw value not available; re-issue required")

ErrTokenRawUnavailable is returned by AgentToken when the agent has a stored token hash but the raw value is no longer available in memory (post-migration boot). Callers either accept this (the agent already has the token cached elsewhere) or re-issue via ReissueAgentToken.

Functions

func AgentFencingMiddleware added in v0.101.0

func AgentFencingMiddleware(st AgentFencingStore, selfPeerID string, logger *slog.Logger) func(http.Handler) http.Handler

AgentFencingMiddleware refuses mutating requests from RoleAgent / RolePrivAgent / RolePeer principals when agent_locks.holder_peer does NOT match the local peer. Closes the §3.7 device-switch invariant the previous slice left open: once the orchestrator transfers the lock to a target peer, the source peer's agent runtime — and any stale Hub→source proxy write — MUST stop writing to the agent's tables.

Scope:

  • RoleAgent / RolePrivAgent gated as before.
  • RolePeer also gated: a Hub→peer agent proxy that lands on a host that no longer holds the lock must 409 rather than mutate state out from under the real holder. The fence is the load-bearing safeguard here because EnforceMiddleware admits /api/v1/agents/* for trusted RolePeer.
  • RoleOwner / RoleGuest pass through (owner is admin; guest doesn't write to agent state).
  • Read methods (GET / HEAD / OPTIONS) pass through. Lock holders rotate via complete; readers should still observe transient state without 409s.
  • The agent's own /handoff/switch route is exempted because it IS the call that moves the lock away — refusing it would create a chicken-and-egg deadlock.
  • Routes outside /api/v1/agents/{agentID}/ pass through. The gate is per-agent; cross-agent routes (groupdms, etc.) have their own membership checks.

Failure modes:

  • lock row missing (ErrNotFound): the agent has no claimed lock yet (first boot, or recently released). v1 single-Hub trusts that AgentLockGuard will Acquire shortly; we pass through rather than block a fresh agent. A future multi- peer cluster might tighten this to "no lock = refuse".
  • store read error: 503 with a generic message; the agent runtime should retry rather than barge through an unknown state.
  • holder mismatch: 409 wrong_holder so the agent runtime knows to stop driving writes and (typically) shut itself down — the lock has moved.

func AllowNonOwner

func AllowNonOwner(p Principal, method, path string) bool

AllowNonOwner gates non-Owner principals against a small whitelist of API routes. Unrecognised paths are denied with 403, and recognised paths may further be denied based on Principal/method/target.

Wired via EnforceMiddleware on BOTH listeners:

  • public (tsnet) listener — TailnetIdentityMiddleware stamps RoleOwner for every tailnet caller (Hub mode), including callers whose WhoIs is transiently unresolved, or RolePeer / Guest (peer mode). Owner short-circuits at the top, so this gate effectively runs only for the §3.7 RolePeer inter-peer surface and Guest fallthroughs.
  • agent-facing (auth-required) loopback listener — AuthMiddleware resolves Bearer/X-Kojo-Token to Owner/Agent/PrivAgent, and this gate enforces per-route policy for non-Owner roles.

The intent is "default deny". Routes are grouped into three buckets:

  1. Public reads — info, directory, agent list/get, avatar.
  2. Self-scoped reads/writes — files, messages, tasks, credentials, memory, slackbot config, notify sources, group memberships, avatar upload, persona / metadata patch, MCP, PreCompact hook. Permitted only when the path's {id} matches the principal.
  3. Privileged-cross-agent — delete / reset / checkin / unarchive / reset-session. Permitted for self by Agent, or for any target by PrivAgent.

Owner-only routes (sessions, git, files browser, embedding, push, custom-models, group DM mutate-as-owner, fork, /privilege, generate-*) fall through and 403. Note: POST /api/v1/groupdms (group creation) is exposed to Agent / PrivAgent below — the handler then enforces the caller-in-memberIds invariant.

func AuthMiddleware

func AuthMiddleware(resolver *Resolver) func(http.Handler) http.Handler

AuthMiddleware resolves Authorization: Bearer / X-Kojo-Token to a Principal and passes it through ctx. It does NOT enforce per-route policy — that is the handler's responsibility (or a separate gate).

Skips the Bearer resolution when an earlier middleware (e.g. TailnetIdentityMiddleware) already attached a non-Guest principal so a paired peer's Tailnet-identified RolePeer doesn't get downgraded to Guest by the absence of a Bearer.

func EnforceMiddleware

func EnforceMiddleware(next http.Handler) http.Handler

EnforceMiddleware gates /api/v1/* requests by Principal/method/path using AllowNonOwner. It runs after AuthMiddleware (which set the Principal in ctx). Static files and non-API paths pass through.

func OwnerOnlyMiddleware

func OwnerOnlyMiddleware(h http.Handler) http.Handler

OwnerOnlyMiddleware tags every request as the Owner UNLESS an earlier middleware (e.g. TailnetIdentityMiddleware) already attached a non-Guest principal. The exception keeps the "Tailscale reach == Owner" UX for the kojo user from clobbering a paired peer's Tailnet-identified RolePeer on the same listener.

Used on the public (Tailscale) listener that the kojo user accesses from their phone — the user's UX is preserved (no token required) for everything except inter-peer requests that arrive pre-stamped.

func SplitAgentIDPath added in v0.101.0

func SplitAgentIDPath(path string) (id, sub string, ok bool)

SplitAgentIDPath parses /api/v1/agents/{id}{sub} into (id, sub, true) where sub starts with "/" or is empty. Returns ok=false if the path does not match.

func TailnetIdentityMiddleware added in v0.101.0

func TailnetIdentityMiddleware(cfg TailnetIdentityConfig) func(http.Handler) http.Handler

TailnetIdentityMiddleware authenticates inbound HTTP requests against the Tailscale identity of the calling node.

Chain order: this middleware runs FIRST (before any per-role gate) so a downstream OwnerOnly or Enforce check sees the Principal already stamped.

Resolution:

  1. Unsafe → stamp RoleOwner (Hub) or RolePeer (--peer) and call next. tsnet is not consulted.
  2. Resolver(remoteAddr) → NodeKey. If the resolver errors OR returns an empty NodeKey (tsnet's view of the tailnet hasn't refreshed for this caller yet — typical for a just-joined node or one whose key was recently rotated), the request: - on Hub mode (PromoteUnknownTailnetToOwner=true) → Owner, UNLESS the error wraps ErrNodeKeyResolverNotReady. The tsnet listener is only reachable over Tailscale, so the listener boundary itself is the trust gate; a transient WhoIs blip must not 403 the operator's UI. The ErrNodeKeyResolverNotReady exception covers the startup window where SetNodeKeyResolver has not run yet — an unidentified caller there has no claim to Owner. - on Peer mode (false) → falls through as Guest so a downstream middleware (the AuthMiddleware on the auth-required listener) can resolve a Bearer instead.
  3. NodeKey == SelfNodeKey → RoleOwner.
  4. PromoteUnknownTailnetToOwner true (Hub public listener) → RoleOwner. peer_registry is NOT consulted for the role; it is touched async as a liveness side-effect when the NodeKey matches a registered peer.
  5. PromoteUnknownTailnetToOwner false (peer-mode public listener) → consult peer_registry. Hit ⇒ RolePeer + sync TouchPeer. Miss / DB error ⇒ Guest (unapproved tailnet callers stay Guest; the policy layer 403s privileged surface).

func WithPrincipal

func WithPrincipal(ctx context.Context, p Principal) context.Context

WithPrincipal attaches a Principal to ctx.

Types

type AgentFencingStore added in v0.101.0

type AgentFencingStore interface {
	GetAgentLock(ctx context.Context, agentID string) (*store.AgentLockRecord, error)
}

AgentFencingStore is the minimal contract AgentFencingMiddleware needs from the DB layer. Lets the test fixtures pass a fake without standing up a full *store.Store.

type Principal

type Principal struct {
	Role    Role
	AgentID string // populated for RoleAgent / RolePrivAgent
	PeerID  string // populated for RolePeer (device_id from peer_registry); also stamped on RoleOwner when the Hub-public TailnetIdentityMiddleware's WhoIs lookup matches a paired peer, so events handlers can identify which paired-peer connection they're on without re-querying the registry
}

Principal identifies the actor behind a request.

func FromContext

func FromContext(ctx context.Context) Principal

FromContext retrieves the Principal stashed in ctx by middleware. Defaults to RoleGuest when no principal is set.

func (Principal) CanDeleteOrReset

func (p Principal) CanDeleteOrReset(targetID string) bool

CanDeleteOrReset returns true for delete/reset/unarchive/checkin/ reset-session ops. Owner: any. PrivAgent: any. Agent: self only. Peer: admitted — Hub proxy validated the original caller.

func (Principal) CanForkOrCreate

func (p Principal) CanForkOrCreate() bool

CanForkOrCreate returns true only for the Owner. Forking copies persona/memory and would leak the source agent's full state, so it is kept Owner-only even for privileged agents. Bare creation is also Owner-only.

func (Principal) CanMutateSelf

func (p Principal) CanMutateSelf(targetID string) bool

CanMutateSelf returns true if the principal may issue self-scoped mutations (PATCH, reset, etc.) against targetID. Peers pass through because the Hub's Enforce layer already authorised the original request before the proxy signed and forwarded it.

func (Principal) CanReadFull

func (p Principal) CanReadFull(targetID string) bool

CanReadFull returns true if the principal can read the full record (Persona, Token-bearing fields, etc.) for the given target agent ID. Owners can read any. Agents can only read their own. Peers are admitted because the Hub's proxy already validated the original caller's identity before forwarding.

func (Principal) CanRestartServer added in v0.111.0

func (p Principal) CanRestartServer() bool

CanRestartServer returns true for principals allowed to trigger a daemon self-restart (POST /api/v1/system/restart): the Owner and privileged agents. Regular agents and peers are refused — a restart quiesces every agent on this host, not just the caller.

func (Principal) CanSetPrivileged

func (p Principal) CanSetPrivileged() bool

CanSetPrivileged returns true only for the Owner. A privileged-agent must never be able to grant or revoke privilege.

func (Principal) IsAgent

func (p Principal) IsAgent() bool

IsAgent returns true if the principal is bound to a specific agent (regular or privileged).

func (Principal) IsOwner

func (p Principal) IsOwner() bool

IsOwner returns true if the principal is the kojo user.

func (Principal) IsPeer added in v0.101.0

func (p Principal) IsPeer() bool

IsPeer reports whether the principal was authenticated via Tailnet identity (WhoIs over tsnet → peer_registry hit). RolePeer is scoped to inter-peer endpoints (cross-subscribe status feed, blob handoff fetch, device-switch orchestration) AND to proxied agent requests (§3.7 remoteAgentProxy: Hub forwards browser/ agent requests to the holder peer over tsnet). Handler-level guard methods (CanReadFull, CanMutateSelf, CanDeleteOrReset) admit IsPeer because the Hub already ran Enforce before proxying — re-blocking at the handler would 403 every proxied request.

type Resolver

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

Resolver maps a Bearer token to a Principal.

func NewResolver

func NewResolver(tokens *TokenStore, isPrivileged func(string) bool) *Resolver

NewResolver builds a Resolver from a TokenStore and a privilege predicate (agent.Manager.IsPrivileged).

func (*Resolver) Resolve

func (r *Resolver) Resolve(token string) Principal

Resolve maps a Bearer token to a Principal. Empty/unknown tokens resolve to RoleGuest.

type Role

type Role int

Role identifies the actor behind an HTTP request after middleware resolution.

const (
	// RoleGuest is the default role for unauthenticated requests on the
	// auth-required listener. Guests can only read directory entries.
	RoleGuest Role = iota
	// RoleAgent is a regular agent-bound principal. It can fully read its
	// own data and is limited to DirectoryEntry on other agents.
	RoleAgent
	// RolePrivAgent extends RoleAgent with the ability to delete/reset
	// other agents (but not fork or read their full data).
	RolePrivAgent
	// RolePeer authenticates a request that arrived from a
	// registered remote peer over tsnet (docs §3.10 / §3.7). The
	// principal's PeerID names the device_id from peer_registry;
	// the scope is the inter-peer surface plus proxied
	// agent/session routes — every other gate returns false. Set
	// by TailnetIdentityMiddleware on ServeAuthTsnet (peer-mode
	// primary listener) and on the Hub's public listener via
	// WhoIs → peer_registry; the legacy Ed25519-signed Bearer
	// stamping path is retired. --unsafe collapses the WhoIs
	// check and stamps RolePeer unconditionally for LAN/docker/CI.
	RolePeer
	// RoleOwner is the kojo user. It has full access to everything.
	RoleOwner
)

type TailnetIdentityConfig added in v0.101.0

type TailnetIdentityConfig struct {
	Resolver func(ctx context.Context, remoteAddr string) (string, error)
	Store    *store.Store
	// SelfNodeKeyFunc returns the local kojo's own Tailscale
	// NodeKey at request time. Reads through a sync.RWMutex on the
	// Server so cmd/kojo can rewire the value after tsnet binds.
	// May be nil — equivalent to a function that always returns
	// "" (no self-Owner promotion).
	SelfNodeKeyFunc func() string
	// PromoteUnknownTailnetToOwner, when true, stamps RoleOwner on
	// every tailnet caller that reaches the listener — both
	// WhoIs-resolved callers and callers whose WhoIs is transiently
	// unresolved (resolver error, empty Node, tsnet view lag). The
	// listener boundary itself (tsnet.ListenTLS) is the trust gate,
	// so a transient WhoIs blip never demotes the operator. The one
	// exception is ErrNodeKeyResolverNotReady — startup before
	// SetNodeKeyResolver wires the closure — which stays Guest so
	// "no resolver by design" cannot turn into Owner-by-default.
	// peer_registry is NOT consulted on the Hub's public listener:
	// that listener is
	// exclusively for the operator's UI — paired peer devices
	// opening the Hub UI in a browser also arrive here, and there
	// is no UX-meaningful distinction between "operator on the Hub
	// host" and "operator on a paired tailnet device."
	//
	// The peer-mode daemon leaves this FALSE so it can still
	// classify a tailnet caller as RolePeer via peer_registry for
	// the §3.7 device-switch surface (Server.ServeAuthTsnet wires
	// this middleware ahead of AuthMiddleware on the peer's
	// primary tsnet listener). A stray tailnet node that hasn't
	// been Approved stays Guest and falls through to the Bearer
	// gate.
	PromoteUnknownTailnetToOwner bool
	// Unsafe collapses the entire WhoIs path. Every caller is
	// stamped RoleOwner (UnsafeAsHub=true) or RolePeer (false).
	Unsafe       bool
	UnsafeAsHub  bool // true on Hub binary (RoleOwner), false on --peer (RolePeer)
	ResolveDelay time.Duration
	Logger       *slog.Logger
	// SelfDeviceID, when non-empty, demotes a peer_registry hit
	// whose device_id matches this value back to Guest. The
	// self-row carries the local NodeKey so other peers can dial
	// us; without this check, a request looped back through tsnet
	// (e.g. operator hits the peer's tailnet IP from the same
	// host) would resolve to the self-row and be stamped RolePeer,
	// silently bypassing the Bearer gate downstream. Empty
	// disables the check (Hub public listener doesn't need it
	// since PromoteUnknownTailnetToOwner short-circuits ahead of
	// the lookup).
	SelfDeviceID string
}

TailnetIdentityConfig configures TailnetIdentityMiddleware.

Resolver maps an incoming request's RemoteAddr to the calling Tailscale node's stable NodeKey (`nodekey:...`). nil disables tsnet identity resolution — the middleware then admits every caller as Guest unless `Unsafe` is true.

Store is the peer_registry handle the middleware queries to translate a NodeKey into a Principal.PeerID.

SelfNodeKey, when non-empty, marks the local kojo's own Tailscale node. A request arriving from that key is the operator hitting the daemon from the same host they run kojo on — it gets promoted to RoleOwner to preserve the "Tailscale reach == Owner" UX on the public listener.

Unsafe collapses every check: the middleware stamps RolePeer (peer-mode) or RoleOwner (hub-mode) onto every request without consulting tsnet or the store. Intended for `--unsafe` LAN / docker / CI deployments where the operator opts into trusting the listener boundary.

type TokenStore

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

TokenStore persists owner / agent token HASHES and keeps an in-memory hash → ID index for O(1) reverse lookup.

Phase 2c-2 slice 17: hashes now live in the kv table at (namespace="auth", key="owner.token" | "agent_tokens/<id>", scope=global, type=string). The legacy on-disk layout is retained as a fallback for v1 installs that booted under pre-cutover binaries:

<base>/owner.token            — owner secret hash (legacy)
<base>/agent_tokens/<id>      — per-agent secret hash (legacy)

On first boot after the cutover, NewTokenStore mirrors any surviving disk file into kv (IfMatchAny so a peer-replicated row wins on collision) and best-effort unlinks the disk file. Steady-state reads come from kv; the disk fallback path runs only when kv has no row AND the legacy file is present.

On-disk / on-the-wire value format (unchanged across the cutover so a rollback could still parse the value):

"sha256:<64-hex-chars>"

Legacy (pre-Phase-5) files held the raw token verbatim. On first boot the store recognizes the legacy form (64 hex chars, no prefix), rewrites it as the hashed form (now in kv, not on disk), and keeps the raw value in memory for THIS boot only so the existing console URL print (cmd/kojo) and agent-side env injection still work without a forced re-issue. From the next boot on, the raw token is unrecoverable from kv — only the operator's cached value (browser localStorage, agent env) keeps working. To force-re-issue a token, delete the kv row (or use RemoveAgentToken) and restart.

The store does NOT depend on the agent package; agent.Manager is the caller that drives EnsureAgentToken / RemoveAgentToken on agent lifecycle events.

func NewTokenStore

func NewTokenStore(base string, kv *store.Store, overrideOwner string) (*TokenStore, error)

NewTokenStore initializes a store rooted at base. The kv handle (Phase 2c-2 slice 17) is the canonical backing; the disk layout at base is retained as a legacy fallback for v1 installs that booted under pre-cutover binaries. Pass nil for kv in tests that only exercise the disk path.

The owner token is created on first use if absent unless overrideOwner is non-empty (in which case overrideOwner is treated as the canonical owner token and is *not* persisted — neither to kv nor to disk).

func (*TokenStore) AdoptAgentTokenFromPeer added in v0.101.0

func (s *TokenStore) AdoptAgentTokenFromPeer(agentID, rawToken string) error

AdoptAgentTokenFromPeer installs a raw token sent by another peer during a §3.7 device-switch agent-sync. The hash is computed locally, persisted to kv, and added to the in-memory verifier so the post-handoff PTY (whose $KOJO_AGENT_TOKEN was stamped by the source peer) authenticates here.

Semantics:

  • Idempotent: a sync that delivers the same raw token we already hold is a no-op (no kv write, in-memory state unchanged).
  • Source-wins: a sync that delivers a DIFFERENT raw token overwrites the local hash. Any prior raw token this peer had cached for the agent is dropped — the orchestrator intends the new token to be the only one in flight.
  • The raw is added to rawByID so a subsequent PTY spawn can read it back via AgentToken (no re-issue needed).

Threadsafe: takes the write lock for the entire call.

func (*TokenStore) AgentToken

func (s *TokenStore) AgentToken(agentID string) (string, error)

AgentToken returns the raw token for the given agent ID if one is available in memory (this boot generated or migrated it). Otherwise the store does not know the raw value and returns ("", error) so a caller can decide whether to re-issue. To force a re-issue, call ReissueAgentToken — it atomically swaps the kv hash and in-memory verifier under one lock without exposing a verifier gap, unlike the older RemoveAgentToken+AgentToken pattern.

func (*TokenStore) AutoRepairAgentToken added in v0.111.0

func (s *TokenStore) AutoRepairAgentToken(agentID string) (bool, error)

AutoRepairAgentToken is the §3.7 device-switch finalize helper for targets that received NO raw token in the sync payload (the source only held the kv hash after a restart). It ensures this peer ends up with a usable raw $KOJO_AGENT_TOKEN:

  • raw already available (or freshly generated for an agent with no prior hash): nothing to repair, returns (false, nil).
  • hash-only (ErrTokenRawUnavailable): re-issues via ReissueAgentToken — kv hash CAS-swapped, in-memory verifier + raw updated — and returns (true, nil).
  • anything else: (false, err).

This replaces the old "manual re-issue required" operator step.

func (*TokenStore) EnsureAgentToken

func (s *TokenStore) EnsureAgentToken(agentID string) error

EnsureAgentToken is a convenience wrapper used at agent-create time. Unlike AgentToken, it tolerates ErrTokenRawUnavailable: when a token already exists on disk we don't need to know its raw value to guarantee "the agent has a token".

func (*TokenStore) LookupAgent

func (s *TokenStore) LookupAgent(token string) (string, bool)

LookupAgent returns the agent ID associated with the given raw token, if any. Hash is computed and matched in constant time against every stored agent hash — this is O(N) but agent count is small.

func (*TokenStore) OwnerHash added in v0.101.0

func (s *TokenStore) OwnerHash() string

OwnerHash returns the SHA-256 hash of the owner token (hex). Always populated.

func (*TokenStore) OwnerToken

func (s *TokenStore) OwnerToken() string

OwnerToken returns the raw owner token if available on this boot, otherwise the empty string. Available scenarios:

  • Fresh install (file just created)
  • Legacy migration boot (file rewritten from raw to hash)
  • KOJO_OWNER_TOKEN override

On any subsequent boot from a hash-only file, OwnerToken returns "". Callers that just want to verify a presented token MUST use VerifyOwner instead.

func (*TokenStore) ReissueAgentToken added in v0.101.0

func (s *TokenStore) ReissueAgentToken(agentID string) (string, error)

ReissueAgentToken atomically rotates the agent's token: CAS-replaces the existing kv row with a fresh hash, then swaps in-memory entries. Holds the write lock for the entire operation so concurrent callers for the same agent serialize and the second caller observes the freshly-issued raw value (rather than racing a second rotation that would invalidate the first caller's raw before it could be injected into a PTY env).

The kv row's hash is compared against this peer's local idIndex before CAS — if they disagree, another peer rotated and we adopt their hash instead of overwriting it. ETag-only CAS isn't enough: a successful peer rotation produces a fresh ETag, our GetKV reads that fresh ETag, and an unguarded saveAgentTokenKV would CAS- succeed and silently overwrite the peer's newly-issued hash — breaking the peer's PTY whose $KOJO_AGENT_TOKEN was just minted.

Returns the raw token kojo can inject into $KOJO_AGENT_TOKEN, or an error if persisting the new hash fails. In peer mode this causes the verifier hash in kv to change — other peers' in-memory verifiers do NOT hot-reload, so tokens issued here are valid on THIS peer until the others restart and re-read kv. Acceptable for the post-migration single-host case (the only place the current callback drives this); cluster-wide raw-token sync is a separate design problem.

func (*TokenStore) RemoveAgentToken

func (s *TokenStore) RemoveAgentToken(agentID string)

RemoveAgentToken deletes the kv row, in-memory entries, and any surviving legacy disk file for an agent. Safe to call on an unknown ID. Invalid IDs are ignored.

func (*TokenStore) VerifyOwner added in v0.101.0

func (s *TokenStore) VerifyOwner(presented string) bool

VerifyOwner returns true if presented matches the stored owner token hash. Constant-time comparison prevents timing-leak side channels.

Jump to

Keyboard shortcuts

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