Authentication
The hub is the issuer. Everything else in the suite verifies its tokens offline against a published key set, rather than calling back to ask whether a token is good.
Getting a token
Section titled “Getting a token”apn fleet loginThis runs an authorization code flow with PKCE:
apnbinds a loopback listener on an ephemeral port, before sending you anywhere. Asking for a port after the browser is already open is a race the browser wins.- It opens your browser to the hub’s authorize endpoint. A top-level navigation is the point — you can see the hub’s own domain in the address bar while you sign in.
- The hub redirects back with a one-time code.
apnexchanges the code and its verifier for a token over HTTP from the process itself, never in the browser.
There is no client secret, which is exactly why PKCE exists: a CLI on your laptop cannot keep one. The verifier proves that the client redeeming the code is the client that asked for it.
Verifying a token
Section titled “Verifying a token”GET /api/auth/jwksOne URL, carrying every key a token of ours may be signed with. Verify against it offline.
Two properties worth knowing:
- The algorithm is pinned to
EdDSAand is never read from the token’s own header. Letting a token nominate the algorithm it should be checked with is the classic JWT confusion attack. - A token names a principal kind —
human,agentorservice— and that kind is load-bearing. The operator API refuses non-human principals at the door./mcpaccepts agents, because serving agents is its whole purpose, and serves them a different tool set.
Redirect URIs
Section titled “Redirect URIs”Clients register exact redirect URIs. No wildcards — a registry that accepts one wildcard has already lost the property that makes exact matching safe.
Native clients like apn register the marker loopback instead, which permits exactly this
shape:
| Clause | Rule |
|---|---|
| Scheme | http only |
| Host | 127.0.0.1 or [::1], exactly |
| Port | Any — this is the whole reason the exception exists |
| Path | Exactly /callback |
| Query, fragment, userinfo | None |
localhost is refused. It is a name, not an address: it resolves through DNS and
/etc/hosts, so whoever can answer for it receives your code. This is the clause people skip.
A redirect_uri that fails any of these gets a 400 with no Location header. An
authorize endpoint that redirects somewhere it was not told to is a credential-minting open
redirector.
Dispatch authority
Section titled “Dispatch authority”GET /api/fleet/dispatchableReturns the agents the token’s holder may dispatch. The answer is read from a mayDispatch
claim the hub signed into the token — not from a query parameter, not from a header, and
not from anything else the caller sent. There is nothing in the request that can change the
answer.
A missing or non-array claim is read as permitted nothing, never as permitted everything. An absent claim means the issuer does not speak that control; reading it as “all” is the one mistake that cannot be walked back.
An agent’s token is refused here regardless of what it may dispatch.
- MCP tools — what a token opens
- The apn command — the two credentials, and why they never mix