personserver

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package personserver implements the Person Server role of AAuth (draft-hardt-oauth-aauth-protocol-11 §7): the person token endpoint, the auth token endpoint, deferred (pending) requests with clarification chat, and the metadata document and JWKS a PS publishes.

The package is library-first and storage-agnostic. A Server is an http.Handler configured with:

  • a Store holding agent-person bindings, directed identifiers, token records for revocation, missions, and pending requests (NewMemoryStore is a reference implementation for tests);
  • a Decider that makes every consent decision. The library never decides policy: a decision allows, denies, or defers, and a deferred request is resolved later by the hosting application through Server.Approve, Server.Deny, Server.Fail, or Server.Ask — from its own approval UI, a push notification, or an administrator queue.

The PS authenticates every agent request by its HTTP message signature and agent token (§11.3), derives a pairwise directed identifier per resource with a keyed HMAC (DirectedSubject), binds each agent to exactly one person (§13.14), records the tokens it issues for revocation (§11.12.4), and verifies resource tokens together with the presented token they name (§6.7.2), call-chaining upstream tokens (§9.4.5), and sub-agent tokens (§10.2).

One origin can host a PS, an agent provider, and an access server: each role has its own key, metadata document, and JWKS, and Server.Register adds the PS routes (under /ps/ by default) to a shared mux.

A minimal deployment:

ps, err := personserver.New(personserver.Config{
	Issuer:         "https://ps.example",
	Key:            psKey,
	SubjectKey:     subjectKey, // 32+ random bytes, kept stable
	Store:          store,
	InteractionURL: "https://ps.example/consent",
	Decider: personserver.DeciderFunc(func(ctx context.Context, r *personserver.TokenRequest) (personserver.Decision, error) {
		if r.Person == "" {
			return personserver.DeferInteraction(), nil // enroll a new agent
		}
		return personserver.Allow(personserver.Grant{}), nil
	}),
})
http.Handle("/", ps)

The consent page reads ?code=, calls Server.ConsumeCode, authenticates the person (§13.13), and calls Server.Approve with the person's identifier, which binds the agent.

Example (CollapsedDeployment)

Example_collapsedDeployment hosts an agent provider, a person server, and an access server on one origin (draft -11 §4.3, §9.3.3). Storage and policy are the hosting application's: the in-memory stores and limiters here are for a single instance only, and the Decider and Authorizer stand in for real consent and access policy.

package main

import (
	"context"
	"crypto/rand"
	"log"
	"net/http"
	"time"

	aauth "github.com/aauth-dev/aauth-go"
	"github.com/aauth-dev/aauth-go/accessserver"
	"github.com/aauth-dev/aauth-go/agentprovider"
	"github.com/aauth-dev/aauth-go/personserver"
	"github.com/aauth-dev/aauth-go/ratelimit"
)

func main() {
	const issuer = "https://example.com" // shared by the collocated roles
	ctx := context.Background()

	// Each role has its own signing key, metadata document, and JWKS.
	apKey, err := aauth.GenerateKey(aauth.AlgEd25519)
	if err != nil {
		log.Fatal(err)
	}
	psKey, err := aauth.GenerateKey(aauth.AlgEd25519)
	if err != nil {
		log.Fatal(err)
	}
	asKey, err := aauth.GenerateKey(aauth.AlgES256)
	if err != nil {
		log.Fatal(err)
	}
	subjectKey := make([]byte, 32) // keep stable: it derives directed sub values
	if _, err := rand.Read(subjectKey); err != nil {
		log.Fatal(err)
	}

	ap, err := agentprovider.New(agentprovider.Config{
		Issuer:  issuer,
		Key:     apKey,
		Limiter: ratelimit.Per(10, time.Minute),
	})
	if err != nil {
		log.Fatal(err)
	}

	as, err := accessserver.New(accessserver.Config{
		Issuer: issuer,
		Key:    asKey,
		Store:  accessserver.NewMemoryStore(), // implement accessserver.Store over your database
		Authorizer: accessserver.AuthorizerFunc(func(ctx context.Context, r *accessserver.AuthorizationRequest) (accessserver.Decision, error) {
			return accessserver.Allow(""), nil // the resource token's scope
		}),
		Limiter: ratelimit.Per(5, time.Second),
	})
	if err != nil {
		log.Fatal(err)
	}

	var ps *personserver.Server
	ps, err = personserver.New(personserver.Config{
		Issuer:         issuer,
		Key:            psKey,
		SubjectKey:     subjectKey,
		Store:          personserver.NewMemoryStore(), // implement personserver.Store over your database
		InteractionURL: issuer + "/consent",
		Decider: personserver.DeciderFunc(func(ctx context.Context, r *personserver.TokenRequest) (personserver.Decision, error) {
			if r.Person == "" {
				return personserver.DeferInteraction(), nil // a new agent: the person signs in at /consent
			}
			return personserver.Allow(personserver.Grant{}), nil
		}),
		// Resources governed by the collocated access server are federated
		// in process rather than over HTTP.
		Federator:    as.Local(issuer),
		CollocatedAS: func(resource string) bool { return resource == "https://ledger.example" },
		Limiter:      ratelimit.Per(5, time.Second),
	})
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	ap.Register(mux)
	ps.Register(mux)
	as.Register(mux)
	mux.HandleFunc("/consent", func(w http.ResponseWriter, r *http.Request) {
		p, err := ps.ConsumeCode(r.Context(), r.URL.Query().Get("code"))
		if err != nil {
			http.Error(w, "unknown code", http.StatusBadRequest)
			return
		}
		// Authenticate the person with the application's own login, then
		// approve; approval binds the agent to that person.
		if err := ps.Approve(r.Context(), p.ID, personserver.Grant{Person: "alice"}); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	// The application issues agent tokens for keys it has authorized, for
	// example a key bound to one of its work sessions.
	agentKey, err := aauth.GenerateKey(aauth.AlgEd25519)
	if err != nil {
		log.Fatal(err)
	}
	jwk, err := aauth.NewJWK(agentKey.Public())
	if err != nil {
		log.Fatal(err)
	}
	if _, _, err := ap.IssueAgentToken(ctx, agentprovider.AgentTokenParams{Name: "session-7", Key: jwk, PS: issuer}); err != nil {
		log.Fatal(err)
	}

	srv := &http.Server{Addr: ":8443", Handler: mux, ReadHeaderTimeout: 10 * time.Second}
	_ = srv // srv.ListenAndServeTLS(certFile, keyFile)
}

Index

Examples

Constants

View Source
const (
	MissionActive     = "active"
	MissionTerminated = "terminated"
)

Mission states (draft -11 §8.6).

View Source
const (
	LogTokenRequest  = "token_request"
	LogUpdate        = "update"
	LogCompletion    = "completion"
	LogPermission    = "permission"
	LogAudit         = "audit"
	LogInteraction   = "interaction"
	LogClarification = "clarification"
	LogTermination   = "termination"
)

Mission log entry kinds (draft -11 §8.3).

View Source
const LogApproval = "approval"

LogApproval is the mission log kind of a mission's approval.

Variables

View Source
var (
	// ErrResolved means the pending request is no longer open.
	ErrResolved = errors.New("personserver: pending request already resolved")
	// ErrClarificationLimit means the request reached the maximum number
	// of clarification rounds (§7.5.3).
	ErrClarificationLimit = errors.New("personserver: clarification round limit reached")
	// ErrInvalidCode means an interaction code is unknown, malformed, or
	// already consumed (§11.6.3.1); the interaction page answers it with
	// invalid_code.
	ErrInvalidCode = errors.New("personserver: invalid_code")
	// ErrQuestionPending means a clarification is already awaiting the
	// agent's answer.
	ErrQuestionPending = errors.New("personserver: a clarification is already pending")
	// ErrFederating means the PS has already consented and the request
	// waits on the access server (§9.1.2); it resolves when the AS
	// answers, not by Approve.
	ErrFederating = errors.New("personserver: the request is waiting on the access server")
)

Errors returned by the pending-request API.

View Source
var (
	// ErrNotFound means no record exists for the key.
	ErrNotFound = errors.New("personserver: not found")
	// ErrConflict means an optimistic update lost a race: the record's
	// Version changed since it was read.
	ErrConflict = errors.New("personserver: version conflict")
	// ErrBindingConflict means the agent is already bound to a different
	// person (draft -11 §13.14: once established, the PS MUST NOT allow a
	// different person to claim the same agent).
	ErrBindingConflict = errors.New("personserver: agent is bound to another person")
	// ErrExists means a record with the key already exists.
	ErrExists = errors.New("personserver: already exists")
	// ErrRevoked means the person token at the root of a grant has been
	// revoked, so an auth token issued under it was not recorded.
	ErrRevoked = errors.New("personserver: person token revoked")
	// ErrMissionTerminated means the mission a token was issued under has
	// been terminated, so the token was not recorded.
	ErrMissionTerminated = errors.New("personserver: mission terminated")
	// ErrAgentRevoked means the agent token (or sub-agent token) a person
	// token was requested with has been revoked, so the person token was not
	// recorded.
	ErrAgentRevoked = errors.New("personserver: agent token revoked")
	// ErrBindingRevoked means the agent's binding no longer names the
	// person a person token was issued for, so the token was not recorded.
	ErrBindingRevoked = errors.New("personserver: agent binding revoked")
)

Store errors. Implementations return these (or wrap them) so the server can tell an absent record from a failure to reach storage.

Functions

func DirectedSubject

func DirectedSubject(key []byte, person, resource string) string

DirectedSubject derives the person's directed identifier at resource (draft -11 §14.1, §10.1.1.2): a keyed HMAC-SHA-256 over the resource and the person, base64url-encoded without padding. Each resource sees a different value for the same person, the value is the same in every person token and auth token for that resource, and it does not vary with the agent or its key. key is the PS's secret derivation key; anyone without it cannot correlate values across resources.

func ResourceInteractionURL

func ResourceInteractionURL(p *Pending, callback string) (string, error)

ResourceInteractionURL is where the interaction page sends the person for the resource's own interaction (draft -11 §7.2.3): {interaction.url}?code={interaction.code}&callback={callback}, where callback is a PS-generated, per-flow URL. The interaction URL must be https (it is checked again here); apply egress admission before redirecting.

Types

type AgentRef

type AgentRef struct {
	Issuer  string `json:"iss"`
	Subject string `json:"sub"`
}

AgentRef identifies an agent the way a PS recognizes it: by its agent token's (iss, sub) (draft -11 §13.14).

func (AgentRef) IsZero

func (a AgentRef) IsZero() bool

IsZero reports whether a names no agent.

func (AgentRef) String

func (a AgentRef) String() string

String renders the pair as "sub (iss)".

type AgentTokenRecord

type AgentTokenRecord struct {
	Issuer  string    `json:"iss"`
	JTI     string    `json:"jti"`
	Subject string    `json:"sub"`
	Exp     time.Time `json:"exp"`
}

AgentTokenRecord is an agent token the PS accepted (draft -11 §11.12.4: the PS records each agent token's (iss, jti) and the sub it carried until its exp plus clock skew), so an agent provider's revocation cascades by agent identity.

type AuthTokenRecord

type AuthTokenRecord struct {
	Issuer   string    `json:"iss"` // the PS, or the AS it federated with
	JTI      string    `json:"jti"`
	Resource string    `json:"aud"`
	Exp      time.Time `json:"exp"`
	// PresentedJTI is the jti of the token the request presented, and
	// (PersonIssuer, PersonJTI) the person token at the root of the grant.
	PresentedJTI string   `json:"presented_jti"`
	PersonIssuer string   `json:"person_iss"`
	PersonJTI    string   `json:"person_jti"`
	Agent        AgentRef `json:"agent"`
	Subagent     AgentRef `json:"subagent,omitempty"`
	Person       string   `json:"person"`
	Subject      string   `json:"sub"`
	MissionS256  string   `json:"mission_s256,omitempty"`
}

AuthTokenRecord is an auth token the PS issued (three-party) or obtained by federation (four-party) (draft -11 §11.12.4).

type BindingStore

type BindingStore interface {
	// BoundPerson returns the person agent is bound to, or ErrNotFound.
	BoundPerson(ctx context.Context, agent AgentRef) (string, error)
	// Bind binds agent to person. Binding an agent to the person it is
	// already bound to succeeds; binding it to another person MUST fail
	// with ErrBindingConflict.
	Bind(ctx context.Context, agent AgentRef, person string) error
	// Unbind removes agent's binding (revoking it); a later Bind may bind
	// the agent to a different person. Unbinding an unbound agent succeeds.
	Unbind(ctx context.Context, agent AgentRef) error
}

BindingStore holds the agent-person binding (draft -11 §13.14): each agent, identified by (iss, sub), is associated with exactly one person. Person identifiers are the hosting application's own and opaque to the library.

type ClaimsProvider

type ClaimsProvider func(ctx context.Context, person string, required []string) (map[string]any, error)

ClaimsProvider supplies identity claims an access server requires about the person (draft -11 §9.2). It is never asked for sub. Nil answers with no claims; the AS ignores what it does not get.

type Config

type Config struct {
	// Issuer is the PS's server identifier (§11.1.1): the iss of the
	// tokens it issues and the base of its endpoint URLs.
	Issuer string
	// Key signs person and auth tokens and server-signed requests; KeyID
	// is its kid in the PS's JWKS (default: the key's RFC 7638
	// thumbprint). When one origin hosts several roles, give each its own
	// key: each role publishes its own metadata and JWKS (§9.3.3).
	Key   crypto.Signer
	KeyID string
	// PublishedKeys are extra keys to publish in the JWKS, for rotation.
	PublishedKeys []aauth.JWK
	// SubjectKey keys the derivation of directed identifiers (see
	// [DirectedSubject]); at least 32 bytes, stable for the PS's lifetime.
	SubjectKey []byte
	// Store persists PS state. Required.
	Store Store
	// Decider decides person and auth token requests. Required.
	Decider Decider
	// MissionApprover enables the mission endpoint (§8).
	MissionApprover MissionApprover
	// PermissionDecider enables the permission endpoint (§7.7).
	PermissionDecider PermissionDecider
	// InteractionRelay enables the interaction endpoint (§7.6).
	InteractionRelay InteractionRelay
	// Audit enables the audit endpoint (§7.8). Records go to the mission
	// log; AuditSink, when set, also receives them.
	Audit     bool
	AuditSink func(ctx context.Context, agent AgentRef, r aauth.AuditRequest)
	// Federator enables four-party access (§9.1): resource tokens
	// addressed to an access server are federated through it. Without
	// one, such resource tokens are refused. ClaimsProvider answers the
	// identity claims an access server requires (§9.2).
	Federator      Federator
	ClaimsProvider ClaimsProvider
	// CollocatedAS, for PS-AS collapse on one origin (§4.3, §9.3.3),
	// reports whether the access server sharing this PS's issuer governs
	// resource. Resource tokens addressed to the shared issuer are then
	// federated through Federator (typically accessserver.Server.Local)
	// instead of handled three-party, and the agent receives the AS's auth
	// token (dwk aauth-access.json). Roles that share an origin share an
	// identifier, so aud alone cannot tell them apart.
	CollocatedAS func(resource string) bool
	// InteractionURL is the PS's user-facing interaction page (https, no
	// query or fragment), sent as the url of requirement=interaction. The
	// page reads ?code= and calls [Server.ConsumeCode]. Required for
	// requirement=interaction decisions.
	InteractionURL string
	// Paths overrides endpoint paths; zero fields take DefaultPaths.
	Paths Paths
	// Metadata supplies the display members of the metadata document
	// (name, description, logos, ...); issuer, jwks_uri, and the endpoints
	// are filled in.
	Metadata aauth.ServerMetadata
	// ScopesSupported, ClaimsSupported, and MissionControlEndpoint are
	// published in the metadata document when set.
	ScopesSupported        []string
	ClaimsSupported        []string
	MissionControlEndpoint string
	// AgentResolver resolves agent token keys; nil discovers them at
	// {iss}/.well-known/aauth-agent.json through HTTPClient.
	AgentResolver aauth.KeyResolver
	// TokenResolver resolves resource token keys and access-server auth
	// token keys; nil discovers them through HTTPClient. Tokens this PS
	// issued are always verified against its own keys.
	TokenResolver aauth.KeyResolver
	// ServerResolver resolves the keys of servers signing under the
	// jwks_uri scheme (revocations); nil discovers them through HTTPClient.
	ServerResolver aauth.KeyResolver
	// AcceptRevocation, when set, decides whether a verified agent
	// provider or resource may revoke at this PS; refused callers get
	// unsupported_iss (§11.12.3). Nil accepts every verified caller.
	AcceptRevocation func(ctx context.Context, caller aauth.ServerCaller) bool
	// Revoker delivers the PS's own revocations downstream; nil uses an
	// HTTPRevoker signing as the PS.
	Revoker Revoker
	// HTTPClient makes outbound requests (discovery, revocation); nil uses
	// aauth.DiscoveryClient, which reaches only public https destinations.
	// Supply a client to reach others, such as local servers in
	// development.
	HTTPClient *http.Client
	// SignatureWindow is the HTTP signature validity window (default
	// aauth.DefaultSignatureWindow).
	SignatureWindow time.Duration
	// PendingTTL bounds how long a deferred request stays open (default
	// ten minutes); MaxWait caps how long a request is held for Prefer:
	// wait (default 30 seconds).
	PendingTTL time.Duration
	MaxWait    time.Duration
	// MaxClarificationRounds bounds clarification questions per request
	// (§7.5.3; default five).
	MaxClarificationRounds int
	// PersonTokenTTL and AuthTokenTTL are the default token lifetimes
	// (at most one hour; default one hour).
	PersonTokenTTL time.Duration
	AuthTokenTTL   time.Duration
	// Limiter, when set, rate-limits the actions the protocol defines a
	// refusal for: interaction code attempts per client ("code:"), polls
	// per pending request ("poll:", answered slow_down), revocations per
	// issuer ("revoke:", answered rate_limited), and new resources per
	// agent at the person token endpoint ("resources:"). See the
	// ratelimit package for an in-memory implementation.
	Limiter aauth.Limiter
	// Notify, when set, is called after a pending request is created or
	// changes (a clarification answer, an updated request, a resolution),
	// so the hosting application can update its approval UI.
	Notify func(ctx context.Context, p *Pending)
	// Logger receives security events (failed mission lookups, rejected
	// requests); nil discards them.
	Logger *slog.Logger
	// Now returns the current time; nil means time.Now.
	Now func() time.Time
	// InsecureSkipIdentifierCheck accepts identifiers that are not server
	// identifiers (http, ports), for development and tests. It also
	// relaxes the agent-token profile check. Never set it in production.
	InsecureSkipIdentifierCheck bool
}

Config configures a Server. Issuer, Key, SubjectKey, Store, and Decider are required.

type Decider

type Decider interface {
	Decide(ctx context.Context, r *TokenRequest) (Decision, error)
}

Decider decides person token and auth token requests: whether this agent may act at the resource as the person, and with what scope (draft -11 §7.1, §7.2, §7.4). A Deferred decision becomes a pending request.

type DeciderFunc

type DeciderFunc func(ctx context.Context, r *TokenRequest) (Decision, error)

DeciderFunc adapts a function to Decider.

func (DeciderFunc) Decide

func (f DeciderFunc) Decide(ctx context.Context, r *TokenRequest) (Decision, error)

Decide calls f.

type Decision

type Decision struct {
	Outcome Outcome
	// Grant qualifies an Allowed decision.
	Grant Grant
	// Code and Reason qualify a Denied decision. Code defaults to the
	// endpoint's denial: denied (§11.9.4) at the token endpoints, a 200
	// {"permission": "denied"} at the permission endpoint, and
	// interaction_unavailable at the interaction endpoint. Reason is a
	// human-readable detail (Markdown for a permission denial).
	Code   string
	Reason string
	// Requirement qualifies a Deferred decision: aauth.RequirementInteraction
	// (the person is directed to InteractionURL with a code), or
	// aauth.RequirementApproval (the PS reaches the person itself), or
	// aauth.RequirementClarification with Question. Empty means approval.
	Requirement string
	// Question is the clarification put to the agent (§7.5.1).
	Question *Question
	// RetryAfter is the polling interval to suggest, in seconds.
	RetryAfter int
}

Decision is a policy verdict on one request.

func Allow

func Allow(g Grant) Decision

Allow is an Allowed decision with g.

func DeferApproval

func DeferApproval() Decision

DeferApproval defers with requirement=approval: the PS obtains the decision itself, by push, email, or an administrator (§11.6.4).

func DeferClarification

func DeferClarification(question string, options ...string) Decision

DeferClarification defers by asking the agent a question (§7.5.1).

func DeferInteraction

func DeferInteraction() Decision

DeferInteraction defers with requirement=interaction: the agent directs the person to the PS's interaction page with a code (§11.6.3).

func Deny

func Deny(reason string) Decision

Deny is a Denied decision with the endpoint's default code.

type Exchange

type Exchange struct {
	Time          time.Time `json:"time"`
	Question      string    `json:"question"`
	Answer        string    `json:"answer,omitempty"`
	Updated       bool      `json:"updated,omitempty"` // an updated_request replaced the request
	Justification string    `json:"justification,omitempty"`
	// FromAS marks a question an access server asked (four-party).
	FromAS bool `json:"from_as,omitempty"`
}

Exchange is one round of clarification chat (draft -11 §7.5): the question put to the agent and the agent's answer, or the updated request it sent instead. Answers are agent-asserted (§7.4) and untrusted: sanitize before display (§13.3).

type FederationRequest

type FederationRequest struct {
	AccessServer   string `json:"-"`
	ResourceToken  string `json:"resource_token"`
	AgentToken     string `json:"agent_token"`
	PresentedToken string `json:"presented_token"`
	SubagentToken  string `json:"subagent_token,omitempty"`
	UpstreamToken  string `json:"upstream_token,omitempty"`
}

FederationRequest is the body of the PS-to-AS token request (draft -11 §9.1.1), addressed to AccessServer.

type FederationResponse

type FederationResponse struct {
	// AuthToken and ExpiresIn are set on a direct grant (200).
	AuthToken string
	ExpiresIn int64
	// PendingURL is set on a deferred response (202): the AS's pending
	// URL (absolute), with what it requires.
	PendingURL     string
	Requirement    aauth.Requirement
	Status         string
	Question       *Question
	RequiredClaims []string
	RetryAfter     int
	// ErrorStatus and ErrorCode are set on a terminal AS error, which the
	// PS relays to the agent in its own problem body (§9.1.3).
	ErrorStatus int
	ErrorCode   string
	ErrorDetail string
}

FederationResponse is the access server's answer (draft -11 §9.1.2): an auth token, a pending request, or a terminal error to relay.

type FederationState

type FederationState struct {
	AccessServer string            `json:"access_server"`
	PendingURL   string            `json:"pending_url"`
	Requirement  aauth.Requirement `json:"requirement,omitzero"`
	Status       string            `json:"status,omitempty"`
	Question     *Question         `json:"question,omitempty"`
	RetryAfter   int               `json:"retry_after,omitempty"`
	// GrantedScope is the scope the PS consented to; the AS's token may
	// not be broader.
	GrantedScope string `json:"granted_scope,omitempty"`
}

FederationState is an access server's pending request the PS is following on the agent's behalf (draft -11 §9.1.2).

type Federator

type Federator interface {
	// Federate sends the token request to the access server.
	Federate(ctx context.Context, r *FederationRequest) (*FederationResponse, error)
	// Poll fetches the current state of the AS's pending request.
	Poll(ctx context.Context, pendingURL string) (*FederationResponse, error)
	// Answer POSTs body to the AS's pending URL: the claims it required
	// (§9.2), or a clarification response or updated request (§7.5.2).
	Answer(ctx context.Context, pendingURL string, body any) error
}

Federator makes PS-to-AS token requests (draft -11 §9.1.1) and follows the AS's pending URL. An error means the PS could not obtain a usable response — connection failure, timeout, a malformed response — and is answered as_unreachable (§11.9.3).

type Grant

type Grant struct {
	// Person binds an unbound agent to this person (person and auth
	// tokens, missions; §13.14). It MUST equal an existing binding.
	Person string
	// Tenant is the person's organization, carried in person tokens.
	Tenant string
	// Scope is the scope an auth token grants; empty grants the resource
	// token's scope. It may not be broader than what was requested.
	Scope string
	// TTL caps the lifetime of the issued token (at most one hour).
	TTL time.Duration
	// Mission shapes an approved mission (mission proposals).
	Mission *MissionGrant
	// Answer is the person's answer to an interaction question.
	Answer string
}

Grant is what an Allowed decision, or an approval of a pending request, grants. Each field applies to the request kinds named.

type HTTPRevoker

type HTTPRevoker struct {
	Client     aauth.RevocationClient
	HTTPClient *http.Client
}

HTTPRevoker is the default Revoker: endpoint discovery plus an aauth.RevocationClient.

func (HTTPRevoker) Revoke

func (h HTTPRevoker) Revoke(ctx context.Context, recipient, dwk string, req aauth.RevocationRequest) (*aauth.RevocationResponse, error)

Revoke implements Revoker.

type InteractionRelay

type InteractionRelay interface {
	Relay(ctx context.Context, r *InteractionRequest) (Decision, error)
}

InteractionRelay delivers interaction requests to the person. For a question, Allowed answers with Grant.Answer at once and Deferred waits for Server.Approve with the answer. For an interaction or payment relay, Deferred means the PS is relaying it: call Server.MarkInteracting when the person engages and Server.Approve when the PS has done all it can. Denied answers interaction_unavailable (424, the agent directs the person itself) unless Code is user_unreachable.

type InteractionRelayFunc

type InteractionRelayFunc func(ctx context.Context, r *InteractionRequest) (Decision, error)

InteractionRelayFunc adapts a function to InteractionRelay.

func (InteractionRelayFunc) Relay

Relay calls f.

type InteractionRequest

type InteractionRequest struct {
	Agent   *aauth.AgentClaims
	Person  string
	Request *aauth.InteractionRequest
	Mission *MissionRecord
}

InteractionRequest is a request to reach the person through the PS (draft -11 §7.6): relay an interaction or payment URL, or ask a question.

type Kind

type Kind string

Kind is the kind of request a pending request holds.

const (
	KindPersonToken       Kind = "person_token"       // §7.1
	KindAuthToken         Kind = "auth_token"         // §7.2
	KindMissionProposal   Kind = "mission_proposal"   // §8.1
	KindMissionUpdate     Kind = "mission_update"     // §8.4
	KindMissionCompletion Kind = "mission_completion" // §8.5
	KindPermission        Kind = "permission"         // §7.7
	KindInteraction       Kind = "interaction"        // §7.6
)

Pending request kinds.

type MemoryStore

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

MemoryStore is an in-memory Store for tests and examples. It keeps everything in maps guarded by one mutex, copies records in and out (as a database would), and never prunes. Do not use it in production: state is lost on restart and is not shared between instances.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) AgentToken

func (m *MemoryStore) AgentToken(_ context.Context, iss, jti string) (*AgentTokenRecord, error)

AgentToken implements TokenStore.

func (*MemoryStore) AppendMissionLog

func (m *MemoryStore) AppendMissionLog(_ context.Context, s256 string, e MissionLogEntry) error

AppendMissionLog implements MissionStore.

func (*MemoryStore) AuthToken

func (m *MemoryStore) AuthToken(_ context.Context, iss, jti string) (*AuthTokenRecord, error)

AuthToken implements TokenStore.

func (*MemoryStore) AuthTokensForMission

func (m *MemoryStore) AuthTokensForMission(_ context.Context, s256 string) ([]AuthTokenRecord, error)

AuthTokensForMission implements TokenStore.

func (*MemoryStore) AuthTokensForPersonToken

func (m *MemoryStore) AuthTokensForPersonToken(_ context.Context, jti string) ([]AuthTokenRecord, error)

AuthTokensForPersonToken implements TokenStore.

func (*MemoryStore) Bind

func (m *MemoryStore) Bind(_ context.Context, agent AgentRef, person string) error

Bind implements BindingStore.

func (*MemoryStore) BoundPerson

func (m *MemoryStore) BoundPerson(_ context.Context, agent AgentRef) (string, error)

BoundPerson implements BindingStore.

func (*MemoryStore) CreateMission

func (m *MemoryStore) CreateMission(_ context.Context, r *MissionRecord) error

CreateMission implements MissionStore.

func (*MemoryStore) CreatePending

func (m *MemoryStore) CreatePending(_ context.Context, p *Pending) error

CreatePending implements PendingStore.

func (*MemoryStore) IsRevoked

func (m *MemoryStore) IsRevoked(_ context.Context, iss, jti string) (bool, error)

IsRevoked implements TokenStore.

func (*MemoryStore) MarkPresented

func (m *MemoryStore) MarkPresented(_ context.Context, jti, as string) error

MarkPresented implements TokenStore.

func (*MemoryStore) Mission

func (m *MemoryStore) Mission(_ context.Context, s256 string) (*MissionRecord, error)

Mission implements MissionStore.

func (*MemoryStore) MissionLog

func (m *MemoryStore) MissionLog(_ context.Context, s256 string) ([]MissionLogEntry, error)

MissionLog implements MissionStore.

func (*MemoryStore) Pending

func (m *MemoryStore) Pending(_ context.Context, id string) (*Pending, error)

Pending implements PendingStore.

func (*MemoryStore) PendingByCode

func (m *MemoryStore) PendingByCode(_ context.Context, code string) (*Pending, error)

PendingByCode implements PendingStore.

func (*MemoryStore) PendingForMission

func (m *MemoryStore) PendingForMission(_ context.Context, s256 string) ([]*Pending, error)

PendingForMission implements PendingStore.

func (*MemoryStore) PendingForResourceToken

func (m *MemoryStore) PendingForResourceToken(_ context.Context, iss, jti string) ([]*Pending, error)

PendingForResourceToken implements PendingStore.

func (*MemoryStore) PersonToken

func (m *MemoryStore) PersonToken(_ context.Context, jti string) (*PersonTokenRecord, error)

PersonToken implements TokenStore.

func (*MemoryStore) PersonTokensForSubject

func (m *MemoryStore) PersonTokensForSubject(_ context.Context, resource, sub string) ([]PersonTokenRecord, error)

PersonTokensForSubject implements TokenStore.

func (*MemoryStore) PersonTokensFromUpstream

func (m *MemoryStore) PersonTokensFromUpstream(_ context.Context, iss, jti string) ([]PersonTokenRecord, error)

PersonTokensFromUpstream implements TokenStore.

func (*MemoryStore) RecordAgentToken

func (m *MemoryStore) RecordAgentToken(_ context.Context, r AgentTokenRecord) error

RecordAgentToken implements TokenStore.

func (*MemoryStore) RecordAuthToken

func (m *MemoryStore) RecordAuthToken(_ context.Context, r AuthTokenRecord) error

RecordAuthToken implements TokenStore.

func (*MemoryStore) RecordPersonToken

func (m *MemoryStore) RecordPersonToken(_ context.Context, r PersonTokenRecord) error

RecordPersonToken implements TokenStore.

func (*MemoryStore) RecordSubject

func (m *MemoryStore) RecordSubject(_ context.Context, resource, sub, person string) error

RecordSubject implements SubjectStore.

func (*MemoryStore) ResolvePendingIfMissionActive

func (m *MemoryStore) ResolvePendingIfMissionActive(_ context.Context, p *Pending) error

ResolvePendingIfMissionActive implements PendingStore.

func (*MemoryStore) Revoke

func (m *MemoryStore) Revoke(_ context.Context, iss, jti string, exp time.Time) error

Revoke implements TokenStore.

func (*MemoryStore) SubjectPerson

func (m *MemoryStore) SubjectPerson(_ context.Context, resource, sub string) (string, error)

SubjectPerson implements SubjectStore.

func (*MemoryStore) TerminateMission

func (m *MemoryStore) TerminateMission(_ context.Context, s256, reason string) error

TerminateMission implements MissionStore.

func (*MemoryStore) TokensForAgent

func (m *MemoryStore) TokensForAgent(_ context.Context, agent AgentRef) ([]PersonTokenRecord, []AuthTokenRecord, error)

TokensForAgent implements TokenStore.

func (*MemoryStore) Unbind

func (m *MemoryStore) Unbind(_ context.Context, agent AgentRef) error

Unbind implements BindingStore.

func (*MemoryStore) UpdatePending

func (m *MemoryStore) UpdatePending(_ context.Context, p *Pending) error

UpdatePending implements PendingStore.

type MissionApprover

type MissionApprover interface {
	DecideMission(ctx context.Context, r *MissionRequest) (Decision, error)
}

MissionApprover decides mission proposals, updates, and completions. An Allowed proposal is shaped by Grant.Mission; an Allowed update is accepted into the log; an Allowed completion terminates the mission as completed. Follow-up questions are put with Server.Ask.

type MissionApproverFunc

type MissionApproverFunc func(ctx context.Context, r *MissionRequest) (Decision, error)

MissionApproverFunc adapts a function to MissionApprover.

func (MissionApproverFunc) DecideMission

func (f MissionApproverFunc) DecideMission(ctx context.Context, r *MissionRequest) (Decision, error)

DecideMission calls f.

type MissionGrant

type MissionGrant struct {
	// Description is the approved scope; empty keeps the proposal's.
	Description string
	// Tools are the approved_tools; nil keeps the proposal's, an empty
	// non-nil slice approves none.
	Tools []aauth.MissionTool
	// Resources are the approved_resources, for each of which the PS
	// issues a person token; nil keeps the proposal's.
	Resources []string
	// ExpiresAt, when set, is the mission's expires_at.
	ExpiresAt time.Time
	// Capabilities are those the PS can provide on the person's behalf now
	// (not part of the blob).
	Capabilities []string
	// Extra adds members to the blob; they become part of its identity.
	Extra map[string]any
}

MissionGrant shapes the mission blob the PS approves (draft -11 §8.2). The approved mission MAY differ from the proposal.

type MissionLogEntry

type MissionLogEntry struct {
	Time  time.Time `json:"time"`
	Kind  string    `json:"kind"`
	Agent AgentRef  `json:"agent,omitzero"`
	// Body is the entry's content: the request or the decision, as JSON.
	// For an accepted update it is the exact update bytes S256 covers.
	Body json.RawMessage `json:"body,omitempty"`
	// S256 is set on accepted updates (§8.4).
	S256 string `json:"s256,omitempty"`
}

MissionLogEntry is one entry of a mission's log (draft -11 §8.3): an ordered record of the agent's actions and the supervision decisions.

type MissionRecord

type MissionRecord struct {
	S256  string   `json:"s256"`
	Blob  []byte   `json:"blob"`
	Owner AgentRef `json:"owner"` // the agent that proposed it
	// Person is the person who approved it.
	Person string `json:"person"`
	// Status is MissionActive or MissionTerminated; a terminated mission
	// never returns to active.
	Status string `json:"status"`
	// TerminationReason records why it terminated (§8.6), outside the
	// immutable blob.
	TerminationReason string    `json:"termination_reason,omitempty"`
	ApprovedAt        time.Time `json:"approved_at"`
	// ExpiresAt is the blob's expires_at, zero when it has none.
	ExpiresAt time.Time `json:"expires_at,omitempty"`
	// Capabilities the PS reported at approval (§8.2).
	Capabilities []string `json:"capabilities,omitempty"`
}

MissionRecord is an approved mission (draft -11 §8.2). Blob is the exact bytes S256 covers; the PS serves those same bytes wherever it later exposes the mission.

type MissionRequest

type MissionRequest struct {
	// Kind is KindMissionProposal, KindMissionUpdate, or
	// KindMissionCompletion.
	Kind Kind
	// Agent proposed or owns the mission; Person is its bound person,
	// empty for an unbound agent's proposal (the decision then names one).
	Agent  *aauth.AgentClaims
	Person string
	// Proposal is the agent-asserted proposal (proposals only).
	Proposal *aauth.MissionProposal
	// Mission and Action are the mission and the update or completion
	// (updates and completions only). Read the mission's log with
	// [Server.MissionLog] to judge them in context.
	Mission *MissionRecord
	Action  *aauth.MissionAction
}

MissionRequest is a mission operation awaiting a decision (draft -11 §8): a proposal (§8.1), an update (§8.4), or a completion (§8.5).

type MissionStore

type MissionStore interface {
	// CreateMission stores a newly approved mission; ErrExists if S256 is
	// already taken.
	CreateMission(ctx context.Context, m *MissionRecord) error
	// Mission returns the mission s256, or ErrNotFound. The lookup SHOULD
	// cost the same whether or not the mission exists: the server compares
	// ownership after it in constant time so that a missing mission and
	// another agent's mission are answered alike (§8.7).
	Mission(ctx context.Context, s256 string) (*MissionRecord, error)
	// TerminateMission marks the mission terminated with reason. A
	// terminated mission stays terminated (its first reason is kept).
	TerminateMission(ctx context.Context, s256, reason string) error
	// AppendMissionLog appends e to the mission's log.
	AppendMissionLog(ctx context.Context, s256 string, e MissionLogEntry) error
	// MissionLog returns the log in order.
	MissionLog(ctx context.Context, s256 string) ([]MissionLogEntry, error)
}

MissionStore persists missions and their logs (draft -11 §8).

type Outcome

type Outcome int

Outcome is the verdict of a Decision.

const (
	Denied   Outcome = iota // refuse the request
	Allowed                 // grant it now, as the Grant says
	Deferred                // answer 202 and decide later
)

Outcomes. The zero value denies, so a forgotten field fails closed.

type Paths

type Paths struct {
	JWKS        string
	PersonToken string
	AuthToken   string
	Mission     string
	Permission  string
	Audit       string
	Interaction string
	Revocation  string
	// Pending is the prefix of pending URLs; a pending request's URL is
	// Pending + id.
	Pending string
}

Paths are the URL paths the PS serves, relative to its issuer. The metadata document is always at /.well-known/aauth-person.json. The defaults live under /ps/ so a PS can share an origin with an agent provider and an access server (role collocation, §4.3).

func DefaultPaths

func DefaultPaths() Paths

DefaultPaths returns the default endpoint paths.

type Pending

type Pending struct {
	ID    string `json:"id"`
	Kind  Kind   `json:"kind"`
	State State  `json:"state"`
	// Agent is the agent that made the request; only it may poll.
	// AgentJTI is the agent token it presented; Subagent the sub-agent a
	// parent requested for.
	Agent    AgentRef `json:"agent"`
	AgentJTI string   `json:"agent_jti,omitempty"`
	Subagent AgentRef `json:"subagent,omitzero"`
	// Person is the person the request is for, when known.
	Person      string `json:"person,omitempty"`
	MissionS256 string `json:"mission_s256,omitempty"`

	// What is asked, for display. Resource, Scope, and AccessServer are
	// resource-asserted (from the resource token); Hints, Proposal,
	// MissionAction, Permission, and Interaction are agent-asserted (§7.4).
	Resource      string                    `json:"resource,omitempty"`
	Scope         string                    `json:"scope,omitempty"`
	AccessServer  string                    `json:"access_server,omitempty"`
	Hints         aauth.TokenRequestHints   `json:"hints,omitzero"`
	Proposal      *aauth.MissionProposal    `json:"proposal,omitempty"`
	MissionAction *aauth.MissionAction      `json:"mission_action,omitempty"`
	Permission    *aauth.PermissionRequest  `json:"permission,omitempty"`
	Interaction   *aauth.InteractionRequest `json:"interaction,omitempty"`

	// Requirement is the AAuth-Requirement of the 202 (interaction,
	// approval), and InteractionURL and Code its parameters. CodeCanonical
	// is the canonical code PendingByCode looks up; CodeConsumed is set
	// once the person arrived with it (single use, §11.6.3.1).
	Requirement    string `json:"requirement,omitempty"`
	InteractionURL string `json:"interaction_url,omitempty"`
	Code           string `json:"code,omitempty"`
	CodeCanonical  string `json:"code_canonical,omitempty"`
	CodeConsumed   bool   `json:"code_consumed,omitempty"`
	RetryAfter     int    `json:"retry_after,omitempty"`

	// Question is the clarification awaiting the agent's answer, asked at
	// QuestionAt; Rounds counts the questions asked (§7.5.3).
	Question       *Question  `json:"question,omitempty"`
	QuestionAt     time.Time  `json:"question_at,omitzero"`
	QuestionFromAS bool       `json:"question_from_as,omitempty"`
	Rounds         int        `json:"rounds,omitempty"`
	Transcript     []Exchange `json:"transcript,omitempty"`

	// ResourceInteraction is the resource's own interaction (§7.2.3),
	// which the person completes before the PS's consent;
	// ResourceInteractionDone is set when its callback succeeded.
	ResourceInteraction     *aauth.ResourceInteraction `json:"resource_interaction,omitempty"`
	ResourceInteractionDone bool                       `json:"resource_interaction_done,omitempty"`

	// Federation is the access server's pending request, in four-party.
	Federation *FederationState `json:"federation,omitempty"`

	// The tokens the request depends on, for revocation (§11.12.4).
	ResourceTokenIssuer string `json:"resource_token_iss,omitempty"`
	ResourceTokenJTI    string `json:"resource_token_jti,omitempty"`
	UpstreamIssuer      string `json:"upstream_iss,omitempty"`
	UpstreamJTI         string `json:"upstream_jti,omitempty"`

	// Request is the PS's snapshot of the original request.
	Request json.RawMessage `json:"request,omitempty"`

	CreatedAt  time.Time `json:"created_at"`
	ReceivedAt time.Time `json:"received_at"`
	ExpiresAt  time.Time `json:"expires_at"`
	// RelayDeadline, for an interaction relay with max_wait, is when the
	// relay resolves even if the person has not engaged (§7.6.2).
	RelayDeadline time.Time `json:"relay_deadline,omitzero"`

	// Result is the terminal response, once resolved.
	Result *Result `json:"result,omitempty"`
	// Version supports optimistic updates (see PendingStore).
	Version int64 `json:"version"`
}

Pending is a deferred request (draft -11 §11.8). The exported fields are what an approval UI needs to show the person; Request is the PS's opaque snapshot of the original request.

func (*Pending) Open

func (p *Pending) Open() bool

Open reports whether the request is still awaiting resolution.

type PendingStore

type PendingStore interface {
	// CreatePending stores a new pending request (Version 1).
	CreatePending(ctx context.Context, p *Pending) error
	// Pending returns the pending request id, or ErrNotFound.
	Pending(ctx context.Context, id string) (*Pending, error)
	// PendingByCode returns the pending request whose canonical
	// interaction code is code, or ErrNotFound.
	PendingByCode(ctx context.Context, code string) (*Pending, error)
	// UpdatePending replaces p if its Version matches (ErrConflict
	// otherwise) and increments p.Version.
	UpdatePending(ctx context.Context, p *Pending) error
	// ResolvePendingIfMissionActive is UpdatePending for a request that
	// belongs to a mission (p.MissionS256), done only if that mission is
	// still active: otherwise it returns ErrMissionTerminated and changes
	// nothing. The mission check and the update MUST be one atomic step with
	// respect to TerminateMission, so that a request is never resolved with a
	// grant after its mission ended.
	ResolvePendingIfMissionActive(ctx context.Context, p *Pending) error
	// PendingForResourceToken returns the unresolved pending requests
	// started for the resource token (iss, jti) (§11.12.4).
	PendingForResourceToken(ctx context.Context, iss, jti string) ([]*Pending, error)
	// PendingForMission returns the unresolved pending requests that
	// belong to the mission s256 (Pending.MissionS256).
	PendingForMission(ctx context.Context, s256 string) ([]*Pending, error)
}

PendingStore persists deferred requests (draft -11 §11.8). Updates are optimistic: UpdatePending succeeds only when the stored Version equals the given record's Version, and then increments it.

type PermissionDecider

type PermissionDecider interface {
	DecidePermission(ctx context.Context, r *PermissionRequest) (Decision, error)
}

PermissionDecider decides permission requests. Allowed answers granted; Denied answers {"permission": "denied"} with Reason (a 200, §7.7.2).

type PermissionDeciderFunc

type PermissionDeciderFunc func(ctx context.Context, r *PermissionRequest) (Decision, error)

PermissionDeciderFunc adapts a function to PermissionDecider.

func (PermissionDeciderFunc) DecidePermission

func (f PermissionDeciderFunc) DecidePermission(ctx context.Context, r *PermissionRequest) (Decision, error)

DecidePermission calls f.

type PermissionRequest

type PermissionRequest struct {
	Agent   *aauth.AgentClaims
	Person  string
	Request *aauth.PermissionRequest
	// Mission is the mission the request belongs to, if any; its
	// approved_tools need no per-call permission.
	Mission *MissionRecord
}

PermissionRequest is a permission request awaiting a decision (draft -11 §7.7): an action no remote resource governs.

type PersonTokenRecord

type PersonTokenRecord struct {
	JTI      string    `json:"jti"`
	Resource string    `json:"aud"` // the token's aud
	Exp      time.Time `json:"exp"`
	Agent    AgentRef  `json:"agent"`              // the agent that requested it
	Subagent AgentRef  `json:"subagent,omitempty"` // the sub-agent whose key it binds, if any
	// AgentJTI and SubagentJTI are the jti of the agent token that signed
	// the request and of the subagent_token parameter, if any; the issuers
	// are Agent.Issuer and Subagent.Issuer. They let RecordPersonToken
	// refuse a token requested with a credential revoked meanwhile.
	AgentJTI    string `json:"agent_jti,omitempty"`
	SubagentJTI string `json:"subagent_jti,omitempty"`
	Person      string `json:"person"`
	Subject     string `json:"sub"`
	// MissionS256 is the mission the token was issued under.
	MissionS256 string `json:"mission_s256,omitempty"`
	// UpstreamIssuer and UpstreamJTI name the upstream token of a call
	// chaining request (§10.1.1), so revoking it walks the chain.
	UpstreamIssuer string `json:"upstream_iss,omitempty"`
	UpstreamJTI    string `json:"upstream_jti,omitempty"`
	// PresentedTo lists the access servers the PS presented the token to
	// (§9.1.1); it is revoked there too.
	PresentedTo []string `json:"presented_to,omitempty"`
}

PersonTokenRecord is a person token the PS issued (draft -11 §7.1 retention, §11.12.4).

type Question

type Question struct {
	Text    string   `json:"clarification"`
	Options []string `json:"options,omitempty"`
	// Timeout is how long the recipient has to answer, in seconds; zero
	// means no deadline beyond the pending request's own.
	Timeout int `json:"timeout,omitempty"`
}

Question is a clarification question (draft -11 §7.5.1).

type RateLimitError

type RateLimitError struct{ RetryAfter time.Duration }

RateLimitError is a refusal by Config.Limiter, with how long to wait.

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

Error implements error.

type Result

type Result struct {
	Status      int    `json:"status"`
	ContentType string `json:"content_type,omitempty"`
	Body        []byte `json:"body,omitempty"`
}

Result is a terminal response stored for a pending request: what the pending URL answers once the request is resolved.

type Revoker

type Revoker interface {
	Revoke(ctx context.Context, recipient, dwk string, req aauth.RevocationRequest) (*aauth.RevocationResponse, error)
}

Revoker delivers a revocation to another server (draft -11 §11.12.1). The default discovers recipient's revocation_endpoint from its metadata document dwk and signs as the PS under the jwks_uri scheme.

type Server

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

Server is a Person Server (draft -11 §7). It is an http.Handler serving the PS endpoints, its metadata document, and its JWKS; Server.Register adds the same routes to a shared mux when one origin hosts several roles.

func New

func New(cfg Config) (*Server, error)

New validates cfg and returns a Server.

func (*Server) Approve

func (s *Server) Approve(ctx context.Context, id string, g Grant) error

Approve resolves pending id as approved with g: the PS completes the request — issues the token, records the mission, grants the permission — and the agent's next poll receives the result. A completion that fails (a token expired while the person decided, a mission was terminated) resolves the request with that error instead. In four-party, approval of an auth token request starts federation and the request stays open until the access server answers.

func (*Server) Ask

func (s *Server) Ask(ctx context.Context, id string, q Question) error

Ask puts a clarification question to the agent (draft -11 §7.5): the next poll answers 202 with requirement=clarification, and the agent's answer arrives in the request's Transcript. At most MaxClarificationRounds questions are asked per request.

func (*Server) CompleteResourceInteraction

func (s *Server) CompleteResourceInteraction(ctx context.Context, id, callbackError string) error

CompleteResourceInteraction records the outcome of a resource's own interaction (draft -11 §7.2.3), reached by redirecting the person to ResourceInteractionURL. callbackError is the error parameter of the callback, empty on success; an error abandons the request with the mapped polling error (§7.3.1).

func (*Server) ConsumeCode

func (s *Server) ConsumeCode(ctx context.Context, code string) (*Pending, error)

ConsumeCode is ConsumeCodeFor with no client key: attempts are limited as one pool.

func (*Server) ConsumeCodeFor

func (s *Server) ConsumeCodeFor(ctx context.Context, code, client string) (*Pending, error)

ConsumeCodeFor is called by the interaction page when the person arrives with ?code= (draft -11 §11.6.3.1). It canonicalizes the code (case, hyphens, Crockford aliases), finds the pending request, consumes the code (single use), and marks the request interacting. An unknown, malformed, or already consumed code is ErrInvalidCode; an expired request is ErrResolved. client identifies who is entering codes (the page's session, or its address): with Config.Limiter set, attempts are limited per client — key "code:<client>" — and a limited attempt is *RateLimitError, checked before the code is looked up. The code only correlates the browser with the request: the page MUST authenticate the person before acting on their decision (§13.13).

func (*Server) Deny

func (s *Server) Deny(ctx context.Context, id, reason string) error

Deny resolves pending id as denied by the person or approver: denied (403) at the pending URL, or {"permission": "denied"} for a permission request (§7.7.2). reason is shown to the agent.

func (*Server) Fail

func (s *Server) Fail(ctx context.Context, id, code, detail string) error

Fail resolves pending id with a polling error (draft -11 §11.9.4): aauth.PollErrAbandoned when the person opened the interaction but did not complete it, aauth.PollErrExpired, or aauth.ErrCodeServerError.

func (*Server) Issuer

func (s *Server) Issuer() string

Issuer returns the PS's issuer.

func (*Server) JWKS

func (s *Server) JWKS() aauth.JWKS

JWKS returns the key set the PS publishes.

func (*Server) MarkInteracting

func (s *Server) MarkInteracting(ctx context.Context, id string) error

MarkInteracting records that the person has engaged with the request through a channel the PS controls (a notification they opened), so polls report status interacting.

func (*Server) Metadata

func (s *Server) Metadata() aauth.PersonServerMetadata

Metadata returns the PS's metadata document (§11.2.2).

func (*Server) Mission

func (s *Server) Mission(ctx context.Context, s256 string) (*MissionRecord, error)

Mission returns the mission s256 as the PS approved it: Blob is the exact bytes its identifier covers (§8.2.1). For the mission control plane and audit views; not exposed to agents.

func (*Server) MissionLog

func (s *Server) MissionLog(ctx context.Context, s256 string) ([]MissionLogEntry, error)

MissionLog returns the mission's log (§8.3). A party auditing a mission reads the blob and its accepted updates together.

func (*Server) PendingRequest

func (s *Server) PendingRequest(ctx context.Context, id string) (*Pending, error)

PendingRequest returns pending request id, for an approval UI.

func (*Server) Register

func (s *Server) Register(mux *http.ServeMux)

Register adds the PS routes to mux: the metadata document, the JWKS, the endpoints, and the pending URLs. Optional endpoints are registered only when configured.

func (*Server) RevokeAgent

func (s *Server) RevokeAgent(ctx context.Context, agent AgentRef) ([]aauth.RevocationOutcome, error)

RevokeAgent revokes every person token and auth token the PS issued to agent — as the requesting agent or as the sub-agent they bind — and terminates what it federated (§11.12.4). It is the PS side of an agent provider's revocation, and what a person removing an agent triggers.

func (*Server) RevokeAuthToken

func (s *Server) RevokeAuthToken(ctx context.Context, jti string) ([]aauth.RevocationOutcome, error)

RevokeAuthToken revokes a three-party auth token the PS issued, at its resource (§11.12.2).

func (*Server) RevokeBinding

func (s *Server) RevokeBinding(ctx context.Context, agent AgentRef) ([]aauth.RevocationOutcome, error)

RevokeBinding ends the agent's association with its person (§13.14) — the agent can obtain no new tokens until a new binding is established — and revokes the tokens issued to it.

func (*Server) RevokePersonToken

func (s *Server) RevokePersonToken(ctx context.Context, jti string) ([]aauth.RevocationOutcome, error)

RevokePersonToken revokes a person token the PS issued, with its cascade (draft -11 §11.12.4), and returns the outcome at each recipient. An outcome with an error may be retried by revoking again.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves the PS routes.

func (*Server) Signer

func (s *Server) Signer() aauth.ServerSigner

Signer returns the server signer the PS uses for requests it makes in its own right (revocations, federation; §11.3.2).

func (*Server) TerminateMission

func (s *Server) TerminateMission(ctx context.Context, s256, reason string) ([]aauth.RevocationOutcome, error)

TerminateMission terminates a mission (draft -11 §8.6) — the control plane operation for the person, the owning agent's operator, or an administrator — with reason (aauth.TerminationRevoked, aauth.TerminationSuperseded, aauth.TerminationAdministrative, ...). Later requests naming it are refused with mission_terminated, and the auth tokens issued under it are revoked (§11.12.4): three-party tokens at their resource, federated ones through their person token at the access server.

type State

type State string

State is the lifecycle state of a pending request.

const (
	StatePending     State = "pending"     // waiting for a decision
	StateInteracting State = "interacting" // the person has arrived at the interaction page
	StateDone        State = "done"        // resolved; Result holds the terminal response
	StateCancelled   State = "cancelled"   // withdrawn by the agent (§7.5.2.3)
)

Pending request states. A request is open while pending or interacting; done and cancelled are terminal.

type Store

Store is everything the PS persists. A hosting application implements it over its own database; see NewMemoryStore for the reference semantics.

type SubjectStore

type SubjectStore interface {
	// RecordSubject records that sub is person's directed identifier at
	// resource. Recording the same triple again succeeds.
	RecordSubject(ctx context.Context, resource, sub, person string) error
	// SubjectPerson returns the person whose directed identifier at
	// resource is sub, or ErrNotFound.
	SubjectPerson(ctx context.Context, resource, sub string) (string, error)
}

SubjectStore records the directed identifiers the PS minted (draft -11 §7.1, §14.1), so it can resolve the person behind an upstream token's (aud, sub) in call chaining.

type TokenRequest

type TokenRequest struct {
	// Kind is KindPersonToken or KindAuthToken.
	Kind Kind
	// Agent is the agent that signed the request; Subagent, when set, the
	// sub-agent the parent is requesting for (§10.2.3).
	Agent    *aauth.AgentClaims
	Subagent *aauth.AgentClaims
	// Person is the person the token would be issued for: the agent's
	// bound person, or the person behind the upstream token in call
	// chaining. Empty when the agent is not yet bound: the decision then
	// names the person in its Grant (typically after an interaction).
	Person string
	// Resource is the resource the token is for.
	Resource string
	// ResourceToken is the verified resource token (auth tokens only), and
	// Scope the scope it requests. AccessServer is its aud when the
	// resource has its own AS (four-party), else empty.
	ResourceToken *aauth.ResourceClaims
	Scope         string
	AccessServer  string
	// Mission is the mission the request is made under, if any.
	Mission *MissionRecord
	// Chained reports a call chaining request (an upstream_token was
	// verified, §10.1.1); Upstream is that token.
	Chained  bool
	Upstream aauth.PresentedToken
	// Capabilities are the agent's capabilities for this request, from the
	// request or, within a mission, from its approval (§7.1).
	Capabilities []string
	// Hints are the agent-asserted optional parameters (justification,
	// platform, device, ...). Present them as the agent's claims (§7.4).
	Hints aauth.TokenRequestHints
}

TokenRequest is a person token or auth token request awaiting a consent decision (draft -11 §7.1, §7.2). Everything in it has been verified, except the agent-asserted Hints.

type TokenStore

type TokenStore interface {
	RecordAgentToken(ctx context.Context, r AgentTokenRecord) error
	// AgentToken returns the accepted agent token (iss, jti), or
	// ErrNotFound.
	AgentToken(ctx context.Context, iss, jti string) (*AgentTokenRecord, error)

	// RecordPersonToken records a person token the PS issued, unless
	// issuing it is no longer allowed: for a token not issued on an
	// upstream token, the agent r.Agent is no longer bound to r.Person
	// (ErrBindingRevoked); for one issued on an upstream token, that token
	// (r.UpstreamIssuer, r.UpstreamJTI) has been revoked (ErrRevoked); in
	// either case the agent token (Agent.Issuer, r.AgentJTI) or sub-agent
	// token (Subagent.Issuer, r.SubagentJTI) it was requested with has been
	// revoked (ErrAgentRevoked), or its mission r.MissionS256 has been
	// terminated (ErrMissionTerminated). Then it records nothing. As with
	// RecordAuthToken, the checks and the insert MUST be atomic with
	// respect to Unbind, Revoke and TerminateMission, so that a revocation
	// stored before the insert is seen here and one stored after it finds
	// r through TokensForAgent, PersonTokensFromUpstream or the mission's
	// tokens. Otherwise an issuance racing a revocation escapes the
	// cascade (§11.12.4).
	RecordPersonToken(ctx context.Context, r PersonTokenRecord) error
	// PersonToken returns the issued person token jti, or ErrNotFound.
	PersonToken(ctx context.Context, jti string) (*PersonTokenRecord, error)
	// PersonTokensForSubject returns the person tokens issued for the
	// directed identifier sub at resource.
	PersonTokensForSubject(ctx context.Context, resource, sub string) ([]PersonTokenRecord, error)
	// MarkPresented records that the person token jti was presented to the
	// access server as (§9.1.1).
	MarkPresented(ctx context.Context, jti, as string) error

	// RecordAuthToken records an auth token the PS issued or federated,
	// unless the person token at the root of its grant (r.PersonIssuer,
	// r.PersonJTI) has been revoked (ErrRevoked) or its mission
	// r.MissionS256 terminated (ErrMissionTerminated): then it records
	// nothing. The check and the insert MUST be atomic with respect to
	// Revoke and TerminateMission, so that a revocation or termination
	// stored before the insert is seen here and one stored after it finds
	// r through AuthTokensForPersonToken or AuthTokensForMission.
	// Otherwise an issuance racing a revocation escapes the cascade
	// (§11.12.4).
	RecordAuthToken(ctx context.Context, r AuthTokenRecord) error
	// AuthToken returns the auth token (iss, jti), or ErrNotFound.
	AuthToken(ctx context.Context, iss, jti string) (*AuthTokenRecord, error)

	// TokensForAgent returns the person and auth tokens issued to agent,
	// as the requesting agent or as the sub-agent they bind (§11.12.4: the
	// cascade is by agent identity).
	TokensForAgent(ctx context.Context, agent AgentRef) ([]PersonTokenRecord, []AuthTokenRecord, error)
	// PersonTokensFromUpstream returns the person tokens issued with the
	// upstream token (iss, jti).
	PersonTokensFromUpstream(ctx context.Context, iss, jti string) ([]PersonTokenRecord, error)
	// AuthTokensForPersonToken returns the auth tokens whose grant is
	// rooted in the person token jti.
	AuthTokensForPersonToken(ctx context.Context, jti string) ([]AuthTokenRecord, error)
	// AuthTokensForMission returns the auth tokens issued under the
	// mission s256.
	AuthTokensForMission(ctx context.Context, s256 string) ([]AuthTokenRecord, error)

	// Revoke records the revocation of (iss, jti), remembered until exp
	// (plus clock skew). Revoking again succeeds.
	Revoke(ctx context.Context, iss, jti string, exp time.Time) error
	// IsRevoked reports whether (iss, jti) was revoked.
	IsRevoked(ctx context.Context, iss, jti string) (bool, error)
}

TokenStore keeps the records revocation needs: the tokens the PS accepted and issued, and the revocations it received or made, keyed by (iss, jti) (draft -11 §11.12.1).

Jump to

Keyboard shortcuts

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