accessserver

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: 21 Imported by: 0

Documentation

Overview

Package accessserver implements the Access Server role of AAuth (draft-hardt-oauth-aauth-protocol-11 §9) and the PS side of PS-AS federation.

A Server is an http.Handler serving the AS token endpoint (§9.1), its pending URLs, its revocation endpoint (§11.12), the metadata document aauth-access.json, and its JWKS. The PS signs every request under the jwks_uri scheme; the AS verifies the agent token, any sub-agent token, the resource token addressed to it together with the presented token it names (re-verified here, §9.1.1), and any upstream token, then asks the caller's Authorizer — the library never decides resource policy. A decision allows (an auth token with dwk aauth-access.json), denies, requires identity claims (§9.2), or defers to an interaction, approval, or clarification that the hosting application resolves with Server.Approve, Server.Deny, or Server.Ask.

Client is a personserver.Federator that makes PS-to-AS requests over HTTP. Server.Local is an in-process Federator for PS-AS collapse (§9.3.3), when one deployment hosts both roles:

as, _ := accessserver.New(accessserver.Config{
	Issuer: "https://example.com", Key: asKey, Store: asStore,
	Authorizer: accessserver.AuthorizerFunc(func(ctx context.Context, r *accessserver.AuthorizationRequest) (accessserver.Decision, error) {
		return accessserver.Allow(""), nil // the resource token's scope
	}),
})
ps, _ := personserver.New(personserver.Config{
	Issuer: "https://example.com", Key: psKey, ..., Federator: as.Local("https://example.com"),
})
mux := http.NewServeMux()
ps.Register(mux) // /ps/..., /.well-known/aauth-person.json
as.Register(mux) // /as/..., /.well-known/aauth-access.json

Each role keeps its own key, metadata document, and JWKS, so tokens and signatures stay distinguishable by dwk even on one origin.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrResolved           = errors.New("accessserver: pending request already resolved")
	ErrClarificationLimit = errors.New("accessserver: clarification round limit reached")
	ErrInvalidCode        = errors.New("accessserver: invalid_code")
)

Errors returned by the pending-request API.

View Source
var (
	// ErrNotFound means no record exists for the key.
	ErrNotFound = errors.New("accessserver: not found")
	// ErrConflict means an optimistic update lost a race.
	ErrConflict = errors.New("accessserver: version conflict")
	// ErrExists means a record with the key already exists.
	ErrExists = errors.New("accessserver: already exists")
	// ErrRevoked means the token an auth token was issued against has
	// been revoked, so the auth token was not recorded.
	ErrRevoked = errors.New("accessserver: presented token revoked")
)

Store errors.

Functions

This section is empty.

Types

type AuthTokenRecord

type AuthTokenRecord struct {
	JTI      string    `json:"jti"`
	Resource string    `json:"aud"`
	Exp      time.Time `json:"exp"`
	// PS is the person server that requested it.
	PS string `json:"ps"`
	// PresentedIssuer and PresentedJTI name the presented token: a person
	// token from the PS, or an auth token from this AS on a step-up.
	PresentedIssuer string `json:"presented_iss"`
	PresentedJTI    string `json:"presented_jti"`
	// Agent is the agent token's sub (the parent, for a sub-agent grant).
	Agent       string `json:"agent"`
	Subject     string `json:"sub"`
	Scope       string `json:"scope,omitempty"`
	MissionS256 string `json:"mission_s256,omitempty"`
}

AuthTokenRecord is an auth token the AS issued (draft -11 §11.12.4: an AS records each auth token with the presented token it was issued against).

type AuthorizationRequest

type AuthorizationRequest struct {
	// PS is the person server that signed the request.
	PS string
	// Agent is the agent token (the parent's, for a sub-agent grant), and
	// Subagent the sub-agent the token will bind. Agent tokens may carry
	// posture claims bearing on policy (§9.1.1).
	Agent    *aauth.AgentClaims
	Subagent *aauth.AgentClaims
	// Resource is the verified resource token: the resource, the person
	// (ps, sub), the requested scope, account, and mission.
	Resource *aauth.ResourceClaims
	// Presented is the verified presented token.
	Presented aauth.PresentedToken
	// Upstream is the verified upstream token of a call chaining request.
	Upstream aauth.PresentedToken
	// Claims are the identity claims the PS supplied after a
	// requirement=claims (§9.2), nil before.
	Claims map[string]any
	// Transcript holds the PS's answers to earlier clarifications.
	Transcript []Exchange
}

AuthorizationRequest is a verified PS-to-AS token request (draft -11 §9.1.1).

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, r *AuthorizationRequest) (Decision, error)
}

Authorizer decides token requests at the AS.

type AuthorizerFunc

type AuthorizerFunc func(ctx context.Context, r *AuthorizationRequest) (Decision, error)

AuthorizerFunc adapts a function to Authorizer.

func (AuthorizerFunc) Authorize

Authorize calls f.

type Client

type Client struct {
	// Signer signs as the PS (DWK aauth-person.json). Required.
	Signer aauth.ServerSigner
	// HTTPClient makes requests; nil uses [aauth.DiscoveryClient], which
	// reaches only public https destinations: the access server is named
	// by a resource token. A client supplied here SHOULD apply the same
	// egress admission.
	HTTPClient *http.Client
	// PreferWaitSeconds, when positive, is sent as Prefer: wait=N.
	PreferWaitSeconds int
	// contains filtered or unexported fields
}

Client is the PS side of PS-AS federation over HTTP (draft -11 §9.1.1): a personserver.Federator that signs every request as the PS under the jwks_uri scheme (§11.3.2), discovers each access server's auth_token_endpoint from /.well-known/aauth-access.json, and maps its responses. Configure a PS with it:

ps, _ := personserver.New(personserver.Config{..., Federator: accessserver.NewClient(psSigner, hc)})

func NewClient

func NewClient(signer aauth.ServerSigner, hc *http.Client) *Client

NewClient returns a Client signing with signer.

func (*Client) Answer

func (c *Client) Answer(ctx context.Context, pendingURL string, body any) error

Answer implements personserver.Federator: a signed POST of body to the pending URL.

func (*Client) Federate

Federate implements personserver.Federator.

func (*Client) Poll

func (c *Client) Poll(ctx context.Context, pendingURL string) (*personserver.FederationResponse, error)

Poll implements personserver.Federator: a signed GET of the pending URL.

type Config

type Config struct {
	// Issuer is the AS's server identifier: the iss of the auth tokens it
	// issues, and the aud of the resource tokens it redeems.
	Issuer string
	// Key signs auth tokens and the AS's own requests; KeyID is its kid
	// (default: the RFC 7638 thumbprint). PublishedKeys are extra keys to
	// publish, for rotation.
	Key           crypto.Signer
	KeyID         string
	PublishedKeys []aauth.JWK
	// Store persists issued tokens, revocations, and pending requests.
	Store Store
	// Authorizer decides token requests.
	Authorizer Authorizer
	// Resources, when set, reports whether the AS serves the resource
	// (the resource token's iss); others are invalid_resource_token.
	Resources func(resource string) bool
	// TrustPS, when set, decides whether a verified PS may request tokens
	// at all; refused ones are answered 403 denied. Nil admits every PS
	// whose signature verifies — trust then emerges from the Authorizer
	// (claims, interaction; §9.3.1).
	TrustPS func(ctx context.Context, ps string) bool
	// InteractionURL is the AS's user-facing interaction page, for
	// requirement=interaction decisions; it calls [Server.ConsumeCode].
	InteractionURL string
	// Paths overrides endpoint paths.
	Paths Paths
	// Metadata supplies display members of the metadata document.
	Metadata aauth.ServerMetadata
	// ServerResolver resolves PS keys (jwks_uri scheme); TokenResolver
	// resolves resource token and person token keys; AgentResolver
	// resolves agent token keys. Each defaults to JWKS discovery through
	// HTTPClient. Tokens this AS issued are verified against its own keys.
	ServerResolver aauth.KeyResolver
	TokenResolver  aauth.KeyResolver
	AgentResolver  aauth.KeyResolver
	// 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
	// Revoker delivers the AS's revocations to resources; nil discovers
	// each resource's revocation endpoint and signs as the AS.
	Revoker Revoker
	// AcceptRevocation, when set, decides whether a verified PS or
	// resource may revoke here; refused callers get unsupported_iss.
	AcceptRevocation func(ctx context.Context, caller aauth.ServerCaller) bool
	// AuthTokenTTL is the default auth token lifetime (at most one hour).
	AuthTokenTTL time.Duration
	// SignatureWindow, PendingTTL (default ten minutes), MaxWait (default
	// 30 seconds), and MaxClarificationRounds (default five) tune requests
	// and deferred responses.
	SignatureWindow        time.Duration
	PendingTTL             time.Duration
	MaxWait                time.Duration
	MaxClarificationRounds int
	// Limiter, when set, limits polls per pending request ("poll:",
	// answered slow_down) and revocations per issuer ("revoke:", answered
	// rate_limited with Retry-After).
	Limiter aauth.Limiter
	// Notify is called after a pending request is created or changes.
	Notify func(ctx context.Context, p *Pending)
	// Logger receives operational errors; 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, for development and tests. Never set it in production.
	InsecureSkipIdentifierCheck bool
}

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

type Decision

type Decision struct {
	Outcome Outcome
	// Scope is the granted scope (Allowed); empty grants the resource
	// token's scope. It may not be broader than the request.
	Scope string
	// TTL caps the auth token's lifetime (at most one hour).
	TTL time.Duration
	// Requirement qualifies a Deferred decision:
	// aauth.RequirementClaims with RequiredClaims (§9.2),
	// aauth.RequirementInteraction (the person goes to InteractionURL),
	// aauth.RequirementClarification with Question, or
	// aauth.RequirementApproval (the default).
	Requirement    string
	RequiredClaims []string
	Question       *Question
	RetryAfter     int
	// Code and Reason qualify a Denied decision; Code defaults to denied.
	Code   string
	Reason string
}

Decision is the Authorizer's verdict on a token request.

func Allow

func Allow(scope string) Decision

Allow grants scope (empty: the requested scope).

func DeferApproval

func DeferApproval() Decision

DeferApproval defers while the AS obtains approval itself (§11.6.4).

func DeferClarification

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

DeferClarification asks the PS a question (§7.5.1).

func DeferInteraction

func DeferInteraction() Decision

DeferInteraction defers until the person completes an interaction at the AS — for example binding their PS at the AS (§9.3.1).

func Deny

func Deny(reason string) Decision

Deny refuses with denied.

func RequireClaims

func RequireClaims(names ...string) Decision

RequireClaims asks the PS for identity claims (draft -11 §9.2). sub is never requested: the presented token already identifies the person.

type Exchange

type Exchange struct {
	Time     time.Time `json:"time"`
	Question string    `json:"question"`
	Answer   string    `json:"answer,omitempty"`
	Updated  bool      `json:"updated,omitempty"`
}

Exchange is one clarification round with the PS.

type MemoryStore

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

MemoryStore is an in-memory Store for tests and examples; state is lost on restart and not shared between instances.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) AuthTokensIssuedAgainst

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

AuthTokensIssuedAgainst implements Store.

func (*MemoryStore) CreatePending

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

CreatePending implements Store.

func (*MemoryStore) IsRevoked

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

IsRevoked implements Store.

func (*MemoryStore) Pending

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

Pending implements Store.

func (*MemoryStore) PendingByCode

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

PendingByCode implements Store.

func (*MemoryStore) PendingForResourceToken

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

PendingForResourceToken implements Store.

func (*MemoryStore) RecordAuthToken

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

RecordAuthToken implements Store.

func (*MemoryStore) Revoke

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

Revoke implements Store.

func (*MemoryStore) UpdatePending

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

UpdatePending implements Store.

type Outcome

type Outcome int

Outcome is the verdict of a Decision.

const (
	Denied   Outcome = iota // refuse: 403 denied, or Code
	Allowed                 // issue the auth token
	Deferred                // answer 202 and decide later
)

Outcomes. The zero value denies.

type Paths

type Paths struct {
	JWKS       string
	Token      string
	Revocation string
	// Pending is the prefix of pending URLs.
	Pending string
}

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

func DefaultPaths

func DefaultPaths() Paths

DefaultPaths returns the default endpoint paths.

type Pending

type Pending struct {
	ID    string `json:"id"`
	PS    string `json:"ps"`
	State State  `json:"state"`
	// Agent, Resource, and Scope describe the request for display.
	Agent    string `json:"agent"`
	Resource string `json:"resource"`
	Scope    string `json:"scope,omitempty"`
	// Requirement is the 202's AAuth-Requirement; RequiredClaims go with
	// claims, InteractionURL and Code with interaction.
	Requirement    string   `json:"requirement"`
	RequiredClaims []string `json:"required_claims,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"`
	// Claims are the identity claims the PS supplied (§9.2).
	Claims map[string]any `json:"claims,omitempty"`
	// Question awaits the PS's answer; Transcript holds earlier rounds.
	Question   *Question  `json:"question,omitempty"`
	QuestionAt time.Time  `json:"question_at,omitzero"`
	Rounds     int        `json:"rounds,omitempty"`
	Transcript []Exchange `json:"transcript,omitempty"`

	ResourceTokenIssuer string `json:"resource_token_iss"`
	ResourceTokenJTI    string `json:"resource_token_jti"`
	// Request is the original request body, verified again as of
	// ReceivedAt when the request completes.
	Request    json.RawMessage `json:"request"`
	ReceivedAt time.Time       `json:"received_at"`
	CreatedAt  time.Time       `json:"created_at"`
	ExpiresAt  time.Time       `json:"expires_at"`
	Result     *Result         `json:"result,omitempty"`
	Version    int64           `json:"version"`
}

Pending is a deferred token request (draft -11 §9.1.2) that the PS polls. Only the PS that made the request may poll it.

func (*Pending) Open

func (p *Pending) Open() bool

Open reports whether the request awaits resolution.

type Question

type Question struct {
	Text    string   `json:"clarification"`
	Options []string `json:"options,omitempty"`
	Timeout int      `json:"timeout,omitempty"`
}

Question is a clarification for the PS (draft -11 §7.5.1).

type Result

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

Result is a stored response: what the token endpoint or a pending URL answers once a 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 a resource (§11.12.1).

type Server

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

Server is an Access Server (draft -11 §9): an http.Handler serving the AS token endpoint, pending URLs, the revocation endpoint, the metadata document, and the JWKS.

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, scope string) error

Approve resolves pending id by issuing the auth token with scope (empty: the requested scope), after verifying the request again as of its receipt; a token that expired meanwhile resolves it with expired.

func (*Server) Ask

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

Ask puts a clarification question to the PS (§7.5.1).

func (*Server) ConsumeCode

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

ConsumeCode is called by the AS's interaction page when the person arrives with ?code= (§11.6.3.1): it consumes the single-use code and marks the request interacting. The page MUST authenticate the person before approving.

func (*Server) Deny

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

Deny resolves pending id with denied (403).

func (*Server) Issuer

func (s *Server) Issuer() string

Issuer returns the AS's issuer.

func (*Server) JWKS

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

JWKS returns the key set the AS publishes.

func (*Server) Local

func (s *Server) Local(ps string) personserver.Federator

Local returns an in-process Federator for the PS ps, which must be the PS sharing this server (its issuer).

func (*Server) Metadata

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

Metadata returns the AS metadata document (§11.2.3).

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 AS routes to mux.

func (*Server) RevokeAuthToken

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

RevokeAuthToken revokes an auth token the AS issued at its resource (§11.12.2), with the step-up tokens issued against it.

func (*Server) ServeHTTP

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

ServeHTTP serves the AS routes.

func (*Server) Signer

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

Signer returns the AS's server signer (§11.3.2).

type State

type State string

State is the lifecycle state of a pending request.

const (
	StatePending     State = "pending"
	StateInteracting State = "interacting"
	StateDone        State = "done"
	StateCancelled   State = "cancelled"
)

Pending request states.

type Store

type Store interface {
	// RecordAuthToken records an issued auth token, unless the presented
	// token (r.PresentedIssuer, r.PresentedJTI) has been revoked: then it
	// records nothing and returns ErrRevoked. The check and the insert
	// MUST be atomic with respect to Revoke, so that a revocation stored
	// before the insert is seen here and one stored after it finds r
	// through AuthTokensIssuedAgainst. Otherwise an issuance racing a
	// revocation escapes the cascade (§11.12.4).
	RecordAuthToken(ctx context.Context, r AuthTokenRecord) error
	// AuthTokensIssuedAgainst returns the auth tokens issued against the
	// presented token (iss, jti).
	AuthTokensIssuedAgainst(ctx context.Context, iss, jti string) ([]AuthTokenRecord, error)
	// Revoke records the revocation of (iss, jti) until exp; IsRevoked
	// reports it.
	Revoke(ctx context.Context, iss, jti string, exp time.Time) error
	IsRevoked(ctx context.Context, iss, jti string) (bool, error)

	// CreatePending stores a new pending request (Version 1).
	CreatePending(ctx context.Context, p *Pending) error
	// Pending returns pending request id, or ErrNotFound.
	Pending(ctx context.Context, id string) (*Pending, error)
	// PendingByCode returns the pending request with the canonical
	// interaction code, or ErrNotFound.
	PendingByCode(ctx context.Context, code string) (*Pending, error)
	// UpdatePending replaces p if its Version matches (ErrConflict
	// otherwise) and increments it.
	UpdatePending(ctx context.Context, p *Pending) error
	// PendingForResourceToken returns the open pending requests started
	// for the resource token (iss, jti).
	PendingForResourceToken(ctx context.Context, iss, jti string) ([]*Pending, error)
}

Store is everything the AS persists.

type TokenRequest

type TokenRequest struct {
	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"`
}

TokenRequest is the body of the PS-to-AS token request (draft -11 §9.1.1).

Jump to

Keyboard shortcuts

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