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 ¶
- Variables
- type AuthTokenRecord
- type AuthorizationRequest
- type Authorizer
- type AuthorizerFunc
- type Client
- func (c *Client) Answer(ctx context.Context, pendingURL string, body any) error
- func (c *Client) Federate(ctx context.Context, r *personserver.FederationRequest) (*personserver.FederationResponse, error)
- func (c *Client) Poll(ctx context.Context, pendingURL string) (*personserver.FederationResponse, error)
- type Config
- type Decision
- type Exchange
- type MemoryStore
- func (m *MemoryStore) AuthTokensIssuedAgainst(_ context.Context, iss, jti string) ([]AuthTokenRecord, error)
- func (m *MemoryStore) CreatePending(_ context.Context, p *Pending) error
- func (m *MemoryStore) IsRevoked(_ context.Context, iss, jti string) (bool, error)
- func (m *MemoryStore) Pending(_ context.Context, id string) (*Pending, error)
- func (m *MemoryStore) PendingByCode(_ context.Context, code string) (*Pending, error)
- func (m *MemoryStore) PendingForResourceToken(_ context.Context, iss, jti string) ([]*Pending, error)
- func (m *MemoryStore) RecordAuthToken(_ context.Context, r AuthTokenRecord) error
- func (m *MemoryStore) Revoke(_ context.Context, iss, jti string, exp time.Time) error
- func (m *MemoryStore) UpdatePending(_ context.Context, p *Pending) error
- type Outcome
- type Paths
- type Pending
- type Question
- type Result
- type Revoker
- type Server
- func (s *Server) Approve(ctx context.Context, id, scope string) error
- func (s *Server) Ask(ctx context.Context, id string, q Question) error
- func (s *Server) ConsumeCode(ctx context.Context, code string) (*Pending, error)
- func (s *Server) Deny(ctx context.Context, id, reason string) error
- func (s *Server) Issuer() string
- func (s *Server) JWKS() aauth.JWKS
- func (s *Server) Local(ps string) personserver.Federator
- func (s *Server) Metadata() aauth.AccessServerMetadata
- func (s *Server) PendingRequest(ctx context.Context, id string) (*Pending, error)
- func (s *Server) Register(mux *http.ServeMux)
- func (s *Server) RevokeAuthToken(ctx context.Context, rec AuthTokenRecord) ([]aauth.RevocationOutcome, error)
- func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
- func (s *Server) Signer() aauth.ServerSigner
- type State
- type Store
- type TokenRequest
Constants ¶
This section is empty.
Variables ¶
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.
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 ¶
func (f AuthorizerFunc) Authorize(ctx context.Context, r *AuthorizationRequest) (Decision, error)
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 ¶
Answer implements personserver.Federator: a signed POST of body to the pending URL.
func (*Client) Federate ¶
func (c *Client) Federate(ctx context.Context, r *personserver.FederationRequest) (*personserver.FederationResponse, error)
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 DeferApproval ¶
func DeferApproval() Decision
DeferApproval defers while the AS obtains approval itself (§11.6.4).
func DeferClarification ¶
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 RequireClaims ¶
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) PendingByCode ¶
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) UpdatePending ¶
func (m *MemoryStore) UpdatePending(_ context.Context, p *Pending) error
UpdatePending implements Store.
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).
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.
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 (*Server) Approve ¶
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) ConsumeCode ¶
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) 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 ¶
PendingRequest returns pending request id, for an approval UI.
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 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).