Documentation
¶
Overview ¶
Package sdk is a self-contained client for resource servers (and MCP servers) that accept Legant delegation tokens. It verifies a composite sub/act token against the issuer's published keys and authorizes a request's scope and constraints — entirely offline, with no callback to Legant and no dependency on Legant's internal packages. Its only dependency is golang-jwt.
Typical use at a resource server:
keys, _ := sdk.FetchJWKS(ctx, "https://legant.example/.well-known/jwks.json")
v := sdk.NewVerifier("https://legant.example", "https://my-api.example/", keys)
claims, err := v.Verify(bearerToken)
if err != nil { /* 401 */ }
if err := claims.Authorize(sdk.Action{Scope: "expenses:submit", Amount: 120, Category: "travel"}); err != nil {
/* 403 */
}
// claims.Provenance() == "user:alice -> agent:assistant"
Index ¶
- Variables
- func Authenticate(v *Verifier) func(http.Handler) http.Handler
- func FetchJWKS(ctx context.Context, jwksURL string) (map[string]*rsa.PublicKey, error)
- func MCPToolName(body []byte) (string, error)
- func ParseJWKS(data []byte) (map[string]*rsa.PublicKey, error)
- func RequireAction(action func(*http.Request) Action) func(http.Handler) http.Handler
- func RequireScope(scope string) func(http.Handler) http.Handler
- type Action
- type Actor
- type Claims
- type Constraints
- type Option
- type RateLimit
- type RevocationFeed
- type TimeWindow
- type Verifier
Constants ¶
This section is empty.
Variables ¶
var ErrRevoked = errors.New("token revoked")
ErrRevoked is returned by Verify when the token's id is present in the configured revocation feed.
Functions ¶
func Authenticate ¶
Authenticate verifies the request's Bearer token with v and, on success, stores the Claims in the request context for downstream handlers (read them with ClaimsFrom). On any verification failure it writes 401 with an RFC 6750 WWW-Authenticate challenge and does NOT call the next handler. It does not authorize a specific action — compose RequireScope / RequireAction for that, or call Claims.Authorize inside the handler.
Revocation behavior follows how v was built: WithRevocationFeed makes a revoked token 401 here; WithFeedFailClosed couples availability to the feed. Without a feed, revocation is bounded by the token's TTL. Choose deliberately.
func FetchJWKS ¶
FetchJWKS fetches and parses an issuer's JSON Web Key Set into a kid->key map. The jwksURL is expected to be the resource server's own trusted issuer (a configured constant), not a value derived from request input — this is a startup/refresh call, not a per-request fetch, so it deliberately does not impose SSRF allow-listing. Pass a URL you control.
func MCPToolName ¶
MCPToolName extracts the tool name from a JSON-RPC MCP "tools/call" request body, for resource servers that ARE an MCP server (rather than sitting behind the gateway). Pair it with Claims.Authorize to gate a tool before dispatch:
name, _ := sdk.MCPToolName(body)
if err := claims.Authorize(sdk.Action{Scope: toolScopes[name], Tool: name}); err != nil { ... }
func RequireAction ¶
RequireAction derives an Action from each request and authorizes it against the verified token, writing 403 on denial. Mount it AFTER Authenticate.
r.With(sdk.Authenticate(v), sdk.RequireAction(func(r *http.Request) sdk.Action {
return sdk.Action{Scope: "warehouse:query", Resource: r.URL.Query().Get("schema")}
})).Get("/query", handler)
func RequireScope ¶
RequireScope authorizes that the request's token carries scope. Mount it AFTER Authenticate. For per-request constraints (amount/resource/tool/time), use RequireAction instead, or call Claims.Authorize in the handler.
Types ¶
type Action ¶
type Action struct {
Scope string
Amount float64
Category string
Tool string
Resource string
At time.Time // instant of the action; zero means "now" (time-window check)
}
Action describes the concrete operation being attempted. Zero-value fields are "not applicable" and skip the corresponding constraint check.
type Actor ¶
Actor models the RFC 8693 "act" claim: the most recent actor on top, earlier actors nested inside.
type Claims ¶
type Claims struct {
jwt.RegisteredClaims
Scope string `json:"scope"`
Act *Actor `json:"act,omitempty"`
Constraints *Constraints `json:"cnst,omitempty"`
}
Claims is the verified body of a delegation token.
func ClaimsFrom ¶
ClaimsFrom returns the verified Claims stored by Authenticate, if any.
func MustClaims ¶
MustClaims returns the verified Claims or panics — use only inside handlers mounted behind Authenticate (where the claims are guaranteed present).
func (*Claims) Authorize ¶
Authorize enforces that the token carries the required scope and that the action satisfies every constraint.
func (*Claims) Provenance ¶
Provenance renders the delegation path, e.g. "user:alice -> agent:assistant -> agent:ocr".
type Constraints ¶
type Constraints struct {
MaxAmount *float64 `json:"max_amount,omitempty"`
Categories []string `json:"categories,omitempty"`
Tools []string `json:"tools,omitempty"`
Resources []string `json:"resources,omitempty"`
TimeWindow *TimeWindow `json:"time_window,omitempty"`
// Rate is informational at the resource server: a rolling-hour cap needs shared
// state and is enforced by Legant at token-exchange time, not offline here.
Rate *RateLimit `json:"rate,omitempty"`
}
Constraints are the fine-grained limits carried in the token's "cnst" claim.
type Option ¶
type Option func(*Verifier)
Option configures an optional Verifier behavior.
func WithFeedFailClosed ¶
WithFeedFailClosed makes Verify REJECT tokens when the revocation feed is older than maxStaleness (high-assurance: couples availability to the feed). The default is fail-open-to-TTL: a stale/unreachable feed reverts to TTL-bounded revocation rather than rejecting valid tokens.
func WithRevocationFeed ¶
func WithRevocationFeed(f *RevocationFeed) Option
WithRevocationFeed makes the verifier reject tokens present in the revocation feed (Tier B). The feed is pulled out of band (no per-request callback); a token revoked at the issuer is rejected here within the feed's refresh window. Without this option the verifier behaves exactly as before (revocation bounded by the token's short TTL — Tier C).
type RateLimit ¶
type RateLimit struct {
MaxPerHour int `json:"max_per_hour"`
}
RateLimit caps how often a delegation may be exercised per rolling hour.
type RevocationFeed ¶
type RevocationFeed struct {
// contains filtered or unexported fields
}
RevocationFeed is an offline, pull-based view of revoked tokens. The resource server fetches a signed feed from the issuer on a timer and checks token ids against an in-memory set — with NO per-request callback. A token revoked at the issuer takes effect here within the feed's refresh interval (and never later than the token's own short expiry, which is the always-present backstop).
The feed is signed with the SAME key as the issuer's JWKS, so it adds no new trust root. A stale or missing feed can only ever MISS a revocation, never invent one; and a regressing version is rejected as a rollback/replay.
func FetchRevocationFeed ¶
func FetchRevocationFeed(ctx context.Context, feedURL, issuer string, keysByKID map[string]*rsa.PublicKey) (*RevocationFeed, error)
FetchRevocationFeed fetches and verifies the issuer's revocation feed once. keysByKID is the same JWKS key map the Verifier uses, so the feed's signature is checked under the established trust root.
func ParseRevocationFeed ¶ added in v0.1.1
func ParseRevocationFeed(feedJWT []byte, issuer string, keysByKID map[string]*rsa.PublicKey) (*RevocationFeed, error)
ParseRevocationFeed verifies a signed revocation feed read from bytes (for example a local feed.jwt file written by `legant apply` / `legant revoke`), with no HTTP. It is the offline counterpart of FetchRevocationFeed: same signature and version checks, no network. To pick up later revocations, parse the file again. keysByKID is the same JWKS key map the Verifier uses.
func (*RevocationFeed) IsRevoked ¶
func (f *RevocationFeed) IsRevoked(jti string) bool
IsRevoked reports whether a token id is in the latest feed snapshot.
func (*RevocationFeed) Refresh ¶
func (f *RevocationFeed) Refresh(ctx context.Context) error
Refresh fetches the feed and applies it (verify signature, enforce a monotonic version, atomically swap the in-memory set).
func (*RevocationFeed) Staleness ¶
func (f *RevocationFeed) Staleness() time.Duration
Staleness is how long since the feed was last successfully refreshed.
func (*RevocationFeed) StartPolling ¶
func (f *RevocationFeed) StartPolling(ctx context.Context, interval time.Duration, onError func(error))
StartPolling refreshes the feed on the given interval until ctx is cancelled. Refresh errors are non-fatal (the previous snapshot is retained); pass a logger-wrapped onError if you want visibility.
type TimeWindow ¶
type TimeWindow struct {
Weekdays []int `json:"weekdays,omitempty"`
StartMin int `json:"start_min"`
EndMin int `json:"end_min"`
TZ string `json:"tz,omitempty"`
}
TimeWindow restricts when the authority may be used: an optional weekday allow-list (0=Sunday … 6=Saturday) and an inclusive minute-of-day range, evaluated in TZ (IANA name; empty = UTC). Enforced offline.
type Verifier ¶
type Verifier struct {
// contains filtered or unexported fields
}
Verifier verifies delegation tokens against a fixed issuer and audience.
func NewVerifier ¶
func NewVerifier(issuer, audience string, keysByKID map[string]*rsa.PublicKey, opts ...Option) *Verifier
NewVerifier builds a verifier from public keys indexed by kid (e.g. from FetchJWKS). The audience must be this resource server's own identifier. It is compared against the token's aud after RFC 8707 canonicalization, so it is insensitive to host case, a default port (:443/:80), and a trailing slash — "https://api.example", "https://API.example:443", and "https://api.example/" all match the canonical "https://api.example/" the issuer mints.