Sandbox¶
The AAuth sandbox runs every AAuth role from this module on your machine: an agent provider, a person server (PS), an access server (AS) and two demo resources, each on its own HTTPS origin. A signing helper lets HTTP clients that cannot sign, such as Postman, Newman and curl, act as the agent.
The sandbox is a development tool. Its keys, certificate authority and automatic consent are for local use only.
Roles¶
| Role | Origin | What it does |
|---|---|---|
| Agent provider | https://ap.aauth.test |
Issues agent tokens: POST /agent-token (two-key refresh, jkt-jwt), POST /enroll (hwk) |
| Person server | https://ps.aauth.test |
Person tokens (POST /person), auth tokens (POST /token), consent at /interaction |
| Access server | https://as.aauth.test |
Auth tokens for docs.aauth.test, federated from the PS (POST /token) |
| Resource | https://resource.aauth.test |
Resource tokens name the PS (three-party) |
| Docs resource | https://docs.aauth.test |
Resource tokens name the AS (four-party) |
Each demo resource serves:
| Path | Access mode |
|---|---|
GET /api/documents |
Agent identity (agent token), person identity (person token), or PS and federated authorization (auth token) |
GET /api/data |
Resource-managed access: a 202 with an interaction, a poll of Location, then an AAuth-Access session token presented as Authorization: AAuth <token> |
POST /authorize |
The authorization endpoint: a signed request presenting a person token, body {"scope": "data.read data.write"}, answered with a resource_token |
The scopes offered are documents:read, data.read and data.write.
Run It¶
Server identifiers are https origins without a port (draft-11 §11.1), so the sandbox listens on port 443. macOS lets ordinary users bind it; on Linux, grant CAP_NET_BIND_SERVICE or run it in a container.
The roles listen on 127.0.0.1:443 and the signing helper on 127.0.0.1:8099, on loopback only. Anyone who can reach the roles can read the trace, which holds every token, and get consent approved automatically; anyone who can reach the helper can sign requests as the test agent. serve refuses a non-loopback --listen or --signer address unless you also pass --listen-any-interface or --signer-any-interface. Use those flags only inside a container whose ports are published on the host's loopback address.
Point the names at your machine, once:
The sandbox's own server-to-server requests, and requests through the signing helper, reach the roles without hosts entries. You need them only to open the consent pages in a browser or to call the roles directly.
Run It in Docker¶
Docker runs the sandbox without a Go toolchain. Two Compose files are in deploy/docker/:
| File | Layout | Use it for |
|---|---|---|
compose.yaml |
Every role and the signing helper in one container; port 443 and the helper published on 127.0.0.1 |
Day-to-day development, Postman, browsers |
compose.roles.yaml |
One container per role on a shared network, each answering at its .aauth.test name, plus the helper in its own container |
Seeing traffic between roles cross a real network; testing a role in isolation |
# Every role in one container
docker compose -f deploy/docker/compose.yaml up -d --build --wait
# One container per role
docker compose -f deploy/docker/compose.roles.yaml up -d --build --wait
# Drive all five access modes through the signing helper
deploy/docker/smoke.sh http://127.0.0.1:8099
- Ports.
SIGNER_PORTandSANDBOX_HTTPS_PORTchange the host ports, for exampleSIGNER_PORT=8199 docker compose -f deploy/docker/compose.yaml up -d. Both are published on127.0.0.1only. - State. The CA and keys live in a Compose volume, so they survive restarts.
docker compose … down -vremoves them. With one container per role, aninitservice creates the CA and every key before the roles start, so they never race to create them. - CA certificate.
docker compose -f deploy/docker/compose.yaml exec sandbox aauth-sandbox ca export --state /state > aauth-sandbox-ca.pem. - Health. Each container's health check is
aauth-sandbox health, which asks the roles it serves (and, with--signer, the helper) to answer. The image has no shell or curl. - Image.
deploy/docker/Dockerfilebuilds a static binary on a distroless, non-root image:docker build -f deploy/docker/Dockerfile -t aauth-sandbox:dev .
To run the image without Compose, publish its ports on the host's loopback address:
Do not publish them as -p 443:443 -p 8099:8099. Docker then listens on every interface of the host, and anyone on your network can read the trace, get consent approved and sign requests through the helper. Inside the container the sandbox listens on every interface (--listen-any-interface, --signer-any-interface), so the host-side address is the only thing that keeps it local.
The CLI options the Compose files use are available outside Docker too: --roles ps serves only the person server and reaches the other roles at their usual names; --roles none runs only the signing helper; aauth-sandbox init creates the CA and every key ahead of time.
Trust the Certificate Authority¶
On first run the sandbox creates a certificate authority in ~/.aauth-sandbox and a certificate for the role names. Export it and trust it only in the tools that call the sandbox:
- Postman: Settings > Certificates > CA certificates, then select
aauth-sandbox-ca.pem. - curl:
--cacert aauth-sandbox-ca.pem. - Go and other clients: add it to the client's own root pool, as
sandbox.NewClientdoes. - Browsers: accept the warning for the consent pages, or trust the CA in a separate browser profile used only for the sandbox.
Avoid adding it to your system trust store. The CA's private key sits unencrypted in the state directory, and every program on the machine would trust what it signs.
The CA is limited to the sandbox's names. Its name constraints let it issue certificates only for the sandbox domain (aauth.test and its subdomains), localhost, any host name you serve outside the domain, and the loopback addresses; clients refuse a certificate it signs for any other site. It is valid for a year.
The sandbox replaces the CA, and logs that it did, when the CA has less than 30 days left, when it has no name constraints (a CA created by an earlier version), or when you serve a host name it does not cover, such as --upstream api.example.dev=.... Export it again and trust the new one wherever you trusted the old. Keep upstream names under the sandbox domain (--upstream petstore=... is petstore.aauth.test) to avoid a new CA.
Sign Requests with the Helper¶
The helper listens on http://127.0.0.1:8099, on loopback only. Send a request to the helper with the target URL as its path; the helper signs it as the sandbox's test agent and forwards it:
Control headers, removed before forwarding:
| Header | Values |
|---|---|
X-Sandbox-Present |
agent (default): the agent token; person or auth: the token in X-Sandbox-Token; hwk; jkt-jwt: a naming JWT from the agent's durable key; none: forward unsigned |
X-Sandbox-Token |
The token to present. For agent it replaces the helper's own agent token |
X-Sandbox-Capabilities |
The capabilities the agent declares (default interaction; empty for none) |
Without X-Sandbox-Present or X-Sandbox-Token, the helper signs as the request's own Signature-Key says, the way a signing proxy receives requests from clients that cannot sign, such as Postman collections that pidl generates with --signer proxy:
Signature-Key received |
The helper |
|---|---|
sig=jwt;jwt="<token>" |
Presents the token as the agent, person or auth token its typ names |
sig=jwt, or none |
Presents its own agent token |
sig=hwk, sig=jkt-jwt |
Signs with that scheme |
The helper adds Content-Digest to bodies and signs @method, @authority, @path, signature-key, and content-digest, content-type and authorization when present. It declares the agent's capabilities as AAuth-Capabilities to the resources, and as a capabilities member in JSON bodies sent to the PS when the body has none. Responses come back unchanged.
POST /sign with {"method", "url", "headers": [[name, value]], "body", "present", "token"} returns the headers to add, for clients that send requests themselves.
The helper is for tools on your machine, not for web pages:
- Hosts it answers at. It answers only when the request's
Hostislocalhost, a loopback address or the address it listens on. That keeps a web page from reaching it through a DNS name pointed at127.0.0.1. Add another name with--signer-host, for example the service name another container uses to reach it. - Browser requests. It refuses requests that carry
Origin, orSec-Fetch-Siteset tocross-siteorsame-site, which browsers add for requests made by web pages. curl, Postman and Newman send neither. It answersOPTIONSitself and does not forward it. - Targets. It signs and forwards only to the sandbox's role origins, including
--remoteones, and its--upstreamhosts. Pass--signer-any-targettoserveto sign for anyhttpshost. - Redirects. It returns a redirect to you as it received it and never follows it, so a signed request goes only to the URL you named.
A Full Flow with curl¶
H=http://127.0.0.1:8099
# Agent token (two-key refresh)
AT=$(curl -s -X POST -H 'X-Sandbox-Present: jkt-jwt' -H 'Content-Type: application/json' -d '{}' \
$H/https://ap.aauth.test/agent-token | jq -r .agent_token)
# Person token for the resource
PT=$(curl -s -X POST -H "X-Sandbox-Token: $AT" -H 'Content-Type: application/json' \
-d '{"resource": "https://resource.aauth.test"}' $H/https://ps.aauth.test/person | jq -r .person_token)
# Resource token from the authorization endpoint
RT=$(curl -s -X POST -H 'X-Sandbox-Present: person' -H "X-Sandbox-Token: $PT" -H 'Content-Type: application/json' \
-d '{"scope": "data.read data.write"}' $H/https://resource.aauth.test/authorize | jq -r .resource_token)
# Auth token from the PS; consent is automatic
AUTH=$(curl -s -X POST -H "X-Sandbox-Token: $AT" -H 'Prefer: wait=10' -H 'Content-Type: application/json' \
-d "{\"resource_token\": \"$RT\", \"presented_token\": \"$PT\"}" $H/https://ps.aauth.test/token | jq -r .auth_token)
# Call the API with the auth token
curl -s -H 'X-Sandbox-Present: auth' -H "X-Sandbox-Token: $AUTH" $H/https://resource.aauth.test/api/documents
If the token request answers 202, follow its Location with GET, signed as the agent, until it answers 200. Location values are absolute URLs.
Consent¶
--consent auto (the default) approves every interaction for --person (default alice) as soon as it is created. With --consent manual, open the url of the AAuth-Requirement in a browser and approve, or approve headlessly:
Trace¶
Every message the local roles send and receive is recorded, including the PS-to-AS federation that a client never sees:
The same list is at GET https://<any role host>/sandbox/trace; DELETE clears it. The trace holds every token the roles exchanged, so it refuses requests from web pages (those carrying Origin, or Sec-Fetch-Site set to cross-site or same-site). Opening it directly in a browser works.
Use It from Other Containers¶
A client or resource in its own container reaches the sandbox over a Compose network. Three things make that work:
- Names. Give the sandbox service the role names as network aliases, so a container resolves
ap.aauth.test,ps.aauth.testand the rest, and any--upstreamname, to it. - The CA. Mount the sandbox's state volume read-only (
state:/state:ro) and point the client at/state/ca.pem. The Petstore example below passes it as--ca-file. - The helper. A tool that calls the signing helper by service name, such as Newman, needs
--signer-host <service>, because the helper answers only loopback names otherwise.
services:
sandbox:
image: aauth-sandbox:dev
command: ["serve", "--state", "/state",
"--listen", "0.0.0.0:443", "--listen-any-interface",
"--signer", "0.0.0.0:8099", "--signer-any-interface", "--signer-host", "sandbox",
"--upstream", "petstore=http://petstore:8080"]
volumes:
- state:/state
networks:
default:
aliases: [ap.aauth.test, ps.aauth.test, as.aauth.test, resource.aauth.test, docs.aauth.test, petstore.aauth.test]
petstore:
image: petstore-server:dev
command: ["--listen", ":8080", "--auth", "aauth",
"--url", "https://petstore.aauth.test",
"--trust", "https://ps.aauth.test",
"--ca-file", "/state/ca.pem"]
volumes:
- state:/state:ro
volumes:
state:
A command: replaces the image's whole default command, which is what makes the roles and the helper listen on every interface inside the container. Keep --listen 0.0.0.0:443 --listen-any-interface and the helper's --signer-any-interface in a command of your own. Without them the roles listen on loopback and other containers get "connection refused". The ports you publish on the host stay on 127.0.0.1, which is what keeps the sandbox local.
Test Another Implementation¶
Replace a role with an external implementation with --remote:
The sandbox stops serving that role and calls the external origin instead: agent tokens name it as the agent's PS, and resources name it as the audience of their resource tokens. The signing helper signs for the external origin too.
Front Your Own Server¶
Run a resource you are developing behind the sandbox with --upstream:
petstore-server --listen 127.0.0.1:8080 &
aauth-sandbox serve --upstream petstore=http://127.0.0.1:8080
The sandbox serves https://petstore.aauth.test with its CA and passes each
request to the server with its Host header, so the server's identifier is
https://petstore.aauth.test and signatures over @authority still verify.
Its traffic appears in the trace, and the signing helper reaches it like a
role. Add the name to your hosts entry (aauth-sandbox hosts --upstream
petstore=... prints it). The server itself fetches the PS's keys from
https://ps.aauth.test, so it must trust the sandbox CA.
An agent of your own enrolls at the sandbox's agent provider with
agentprovider.Client.Enroll, then calls the server through
aauth.Transport:
ap := &agentprovider.Client{BaseURL: "https://ap.aauth.test",
Paths: agentprovider.Paths{Token: sandbox.PathEnroll}, HTTPClient: hc}
agent, err := ap.Enroll(ctx, key, nil)
if err != nil {
return err
}
ps := aauth.NewPSClient(agent.PS, agent)
ps.HTTPClient = hc
client := &http.Client{Transport: aauth.NewTransport(agent, ps)}
res, err := client.Get("https://petstore.aauth.test/pets")
State¶
~/.aauth-sandbox (or --state) holds the CA (ca.pem, ca-key.pem) and each role's key under keys/. Keys persist across restarts, so tokens and thumbprints stay stable. Delete the directory to start over.