Passport is an identity and authentication layer for autonomous agents. It specifies how an agent establishes a cryptographic identity, proves possession of its private key on each request, and is linked to an accountable human owner through a claim flow. Passport acts as an identity provider (IdP): it issues DPoP-bound tokens that third-party resource servers can verify using a published JWKS, without any coupling to the Passport server at request time.
Shared API keys are opaque credentials with no binding to a specific caller. They can be copied, forwarded, and replayed without detection. An agent’s identity should be a cryptographic key pair, such that any authentication proof is a signature — not possession of a shared secret.
Passport represents agent identity as a Decentralized Identifier
(DID) conforming to the W3C DID Core specification. The
did:key method is used, which encodes a public key directly
in the identifier:
did:key:z6MkvqCUU9kcZvbXd7N9fjpR5UkSrpNdnXHZQekmQgd9K5ax
The identifier encodes a multicodec-prefixed Ed25519 public key in
base58btc. The prefix bytes 0xed 0x01 identify the key
type. The 32-byte raw public key is recoverable from the identifier
without any registry lookup:
bytes = base58btc_decode(did[len("did:key:z"):])
assert bytes[0:2] == [0xed, 0x01]
publicKey = bytes[2:]
Each agent generates its key pair independently. The private key is retained by the agent and never transmitted.
Because a DID encodes a specific key, a key rotation necessitates a new DID. To provide a stable reference that survives key rotation, each agent is assigned a handle at registration — a human-readable string that maps to the agent’s current DID through the Passport registry.
handle ──────► Passport record ──────► DID (current key)
The handle does not change when the DID changes. Systems that identify agents by handle are unaffected by key rotation.
For interoperability with DID-aware systems, Passport exposes a DID document at:
GET /registry/{handle}/did.json
The document conforms to W3C DID Core and the Ed25519 Signature 2020 suite:
{
"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"],
"id": "did:key:z6Mk...",
"verificationMethod": [{
"id": "did:key:z6Mk...#z6Mk...",
"type": "Ed25519VerificationKey2020",
"controller": "did:key:z6Mk...",
"publicKeyMultibase": "z6Mk..."
}],
"authentication": ["did:key:z6Mk...#z6Mk..."],
"assertionMethod": ["did:key:z6Mk...#z6Mk..."]
}A token issued after a single challenge-response proof is a bearer credential. Any party in possession of the token — regardless of whether they hold the corresponding private key — can present it successfully. This property is undesirable for autonomous agents operating in multi-party environments.
Passport uses OAuth DPoP (RFC 9449) to bind access tokens to the agent’s public key. Every request to a protected resource must include both the access token and a fresh proof of possession of the corresponding private key.
Authorization: Bearer <access-token>
DPoP: <proof>
The access token is a JWT signed by the Passport server. It contains
a cnf (confirmation) claim carrying the JWK Thumbprint (RFC
7638) of the agent’s public key, and an aud claim scoped to
the intended resource server:
{
"iss": "https://passport.domain",
"aud": "https://resource-server.com",
"sub": "did:key:z6Mk...",
"cnf": { "jkt": "<base64url(SHA-256(canonical JWK))>" }
}Tokens carry no expiry. The key is the credential; per-request DPoP proofs prevent replay. Revocation is enforced by revoking the Passport record in the registry.
Because the agent’s public key is encoded in its DID, the JWK thumbprint is derivable from the DID without additional stored data.
The agent constructs a fresh proof JWT for each request, signed with its private key:
Header: { "typ": "dpop+jwt", "alg": "EdDSA", "jwk": { <agent public key as JWK> } }
Payload: { "jti": "<uuid>", "htm": "<HTTP method>", "htu": "<HTTP URI>", "iat": <unix time> }
The server performs the following checks:
| Check | Purpose |
|---|---|
Proof signature valid against jwk in header |
Confirms the proof was created by the key holder |
cnf.jkt in token equals thumbprint of jwk
in proof |
Confirms the token and proof belong to the same key |
htm and htu match the current request |
Prevents cross-endpoint replay |
iat within an acceptable skew window (±60 s) |
Prevents stale proof replay |
jti present |
Identifies the proof uniquely |
An access token that is intercepted in transit cannot be used by a party that does not hold the private key.
Agent Passport
│ │
│ GET /me (no credentials) │
│───────────────────────────────────►│
│◄───────────────────────────────────│
│ 401 Unauthorized │
│ WWW-Authenticate: Bearer │
│ resource_metadata="…/oauth- │
│ protected-resource" │
│ │
│ (agent follows resource_metadata, discovers /auth/token)
│ │
│ POST /auth/challenge │
│ { did } │
│───────────────────────────────────►│ create nonce (32 random bytes, 5 min TTL, single-use)
│◄───────────────────────────────────│
│ { nonce, expiresAt } │
│ │
│ compute: signature = Ed25519.sign(nonce_bytes, privateKey)
│ compute: DPoP proof JWT for POST /auth/token
│ │
│ POST /auth/token │
│ DPoP: <proof> │
│ { did, nonce, signature, aud? } │
│───────────────────────────────────►│ verify Ed25519 signature over nonce bytes
│ │ verify DPoP proof (all checks in §2.2)
│ │ derive cnf.jkt from DID
│ │ issue JWT signed with server key
│◄───────────────────────────────────│
│ { token } │
│ │
│ compute: DPoP proof JWT for GET /me
│ │
│ GET /me │
│ Authorization: Bearer <token> │
│ DPoP: <proof> │
│───────────────────────────────────►│ verify token signature via JWKS
│ │ verify DPoP proof, match cnf.jkt
│◄───────────────────────────────────│
│ 200 OK { did, handle, status } │
Passport publishes standard OAuth discovery documents so agents and resource servers can locate endpoints without hardcoding URLs.
auth.mdA human- and machine-readable protocol guide is served at:
GET /auth.md
Agents that receive a 401 from a service using Passport
as its IdP can fetch /auth.md for step-by-step instructions
on obtaining and presenting credentials.
GET /.well-known/oauth-authorization-server
{
"issuer": "https://passport.domain",
"token_endpoint": "https://passport.domain/auth/token",
"jwks_uri": "https://passport.domain/.well-known/jwks.json",
"dpop_signing_alg_values_supported": ["EdDSA"]
}GET /.well-known/oauth-protected-resource
{
"resource": "https://passport.domain",
"authorization_servers": ["https://passport.domain"],
"jwks_uri": "https://passport.domain/.well-known/jwks.json",
"resource_documentation": "https://passport.domain/auth.md"
}The WWW-Authenticate header on any 401
response from a protected resource points to this document:
WWW-Authenticate: Bearer realm="passport",
resource_metadata="https://passport.domain/.well-known/oauth-protected-resource"
Passport signs all issued tokens with a server-held Ed25519 key
identified by kid: passport-v1. The corresponding public
key is published at:
GET /.well-known/jwks.json
{
"keys": [{
"kty": "OKP",
"crv": "Ed25519",
"x": "<base64url public key>",
"kid": "passport-v1",
"use": "sig",
"alg": "EdDSA"
}]
}The system uses two distinct public keys with non-overlapping roles:
| Key | Location | Role |
|---|---|---|
| Agent’s Ed25519 key | Encoded in did:key; present in DPoP proof
jwk |
Verifies challenge signatures and DPoP proofs |
| Server’s Ed25519 key | Published at /.well-known/jwks.json |
Verifies token signatures |
A downstream service verifies a request as follows:
1. Extract JWT from Authorization: Bearer header; read kid from header
2. Retrieve corresponding key from JWKS (cache by kid; re-fetch on miss)
3. jwtVerify(token, serverKey, { issuer, audience: self, algorithms: ["EdDSA"] })
4. Extract jwk from DPoP proof header
5. Verify proof signature using jwk
6. Confirm htm and htu match the current request; iat within skew; jti present
7. Compute thumbprint of proof jwk; assert equals token.cnf.jkt
8. Read identity from verified payload: sub (DID), handle, status, aud
Step 3 establishes that the token was issued by Passport and is
intended for this resource server (aud). Steps 4–7
establish that the presenter holds the private key corresponding to the
token. Both verifications are required.
JWKS retrieval is the only network dependency. All subsequent verification is local.
An agent operates autonomously. When an agent is registered, no assertion is made about who controls it. The claim flow establishes a verified link between the agent and a human owner, providing an accountable contact for key loss, misuse, or decommissioning.
Each Passport record carries one of three statuses:
UNCLAIMED ──── claim token redeemed ────► CLAIMED
│ │
└──────────── revocation ────────────► REVOKED
ownerEmail is a confirmed contact.403.POST /auth/register
{ "did": "did:key:z6Mk...", "ownerEmail": "owner@example.com", "name": "..." }
The server validates the DID format, assigns a handle, creates the
Passport record, and — if ownerEmail is provided —
generates a claim token:
raw_token = secure_random(32 bytes)
stored_hash = SHA-256(raw_token) ← only the hash is persisted
email = /claim?token={raw_token} ← raw token sent via email, never stored
The hash prevents exposure of the raw token in the event of a database breach.
POST /auth/claim
{ "token": "<raw token from email>" }
The claim flow is provider-defined — the mechanism by which the token
reaches the owner (email link, OTP, push notification) is an
implementation detail of the identity provider and is not specified by
this protocol. The endpoint contract is: present the raw token; receive
confirmation that passport.status is now
CLAIMED.
The server hashes the presented token, retrieves the matching record, and verifies: - The token has not been used - The token has not expired (24-hour TTL)
Claim tokens are single-use.
If an agent loses its private key, authentication is no longer
possible. The owner identified by ownerEmail can authorize
a DID rotation: the Passport record is updated to reference a new DID
while the handle and all downstream references remain unchanged.
The token carries status to allow downstream services to
gate access on claimed status. ownerEmail is not included
in the token. Services that require the owner’s contact information
query the registry directly:
GET /registry/{handle}
→ { "ownerEmail": "x***@example.com", ... } (address is masked)
An agent may be authorized to act on behalf of a principal — another
agent or a human owner. The token structure accommodates this through
the RFC 8693 act claim.
{
"iss": "https://passport.domain",
"aud": "https://resource-server.com",
"sub": "did:key:z6MkPrincipal...",
"handle": "quietly-brave-crane",
"status": "CLAIMED",
"act": {
"sub": "did:key:z6MkActor...",
"handle": "swiftly-golden-fox"
},
"scope": ["read:data", "tool:bash"],
"cnf": { "jkt": "<actor's JWK thumbprint>" }
}sub identifies the principal — the entity on whose
behalf the action is being taken. Authorization decisions are made
against sub. act.sub identifies the actor —
the agent presenting the token. Audit records reference
act.sub. scope limits the operations the actor
may perform as the principal. cnf.jkt is the actor’s key
thumbprint; the actor signs DPoP proofs.
The principal authorizes a specific actor and scope through the Passport API. The actor presents its own valid token to exchange for a delegation token. The server confirms the authorization is active before issuing.
Delegation tokens are signed with the same server key as self-identity tokens and verified identically via JWKS. No additional verification infrastructure is required.
The verification procedure in §4.3 applies without modification. After step 8, the verifier additionally checks:
if token.act is present:
principal = token.sub
actor = token.act.sub
assert required_operation in token.scope
Tokens without act are self-identity tokens. The two
forms are structurally compatible; a verifier that does not inspect
act will still correctly verify the token signature and
principal identity.
Delegation is non-transitive: a token carrying an act
claim cannot be used to issue further delegation tokens.
Key binding. Every access token is bound to the
agent’s public key via cnf.jkt. A token intercepted in
transit cannot be used by a party that does not hold the corresponding
private key.
No token expiry. Tokens carry no exp
claim. Revocation is enforced by marking the Passport record as
REVOKED in the registry. Resource servers that require
real-time revocation checking should call the registry at
GET /registry/{handle} before accepting a token.
Proof freshness. The DPoP proof iat
claim and jti uniqueness requirement ensure that each proof
is valid only within a short time window and cannot be replayed.
Single-use nonces. Challenge nonces are marked used on first consumption and expire after five minutes, preventing replay of the initial authentication.
Audience binding. The aud claim scopes
each token to a specific resource server. A token issued for one service
cannot be replayed at another.
Handle stability. Key rotation updates the DID on the Passport record without changing the handle, preserving downstream references and audit history.
Delegation depth. An authorization grant is non-transitive. A delegation token cannot authorize further delegations, ensuring the trust graph has bounded depth.
Storage independence. The protocol requires only that the agent can produce an Ed25519 signature over arbitrary bytes. The mechanism by which the private key is stored and protected — browser storage, a secrets manager, a hardware security module — is outside the scope of this specification.
{
"iss": "https://passport.domain",
"aud": "https://resource-server.com",
"sub": "did:key:z6MkAgent...",
"jti": "550e8400-e29b-41d4-a716-446655440000",
"iat": 1716400000,
"handle": "swiftly-golden-fox",
"status": "CLAIMED",
"name": "My Research Agent",
"cnf": { "jkt": "v-6x4fDIRPF5mz9_xJPaYlYoLRQ_Gz8bXv-Q7nG9Ue4" }
}{
"iss": "https://passport.domain",
"aud": "https://resource-server.com",
"sub": "did:key:z6MkPrincipal...",
"jti": "660e8400-f31c-52e5-b827-557766551111",
"iat": 1716400000,
"handle": "quietly-brave-crane",
"status": "CLAIMED",
"name": "Principal Agent",
"act": { "sub": "did:key:z6MkActor...", "handle": "swiftly-golden-fox" },
"scope": ["read:data", "tool:bash"],
"cnf": { "jkt": "a-1y5gEJRQF6nz0_yKQbZmYpMSR_Hz9cYw-R8oH0Vf5" }
}| Method | Path | Purpose |
|---|---|---|
POST |
/auth/register |
Register a new agent DID |
POST |
/auth/challenge |
Request a nonce for signing |
POST |
/auth/token |
Exchange signed nonce for a DPoP-bound JWT |
POST |
/auth/claim |
Redeem a claim token to set status to CLAIMED |
| Method | Path | Purpose |
|---|---|---|
GET |
/api/registry |
List all registered agents |
GET |
/registry/{handle} |
Get passport detail by handle |
GET |
/registry/{handle}/did.json |
DID document for the agent |
| Method | Path | Purpose |
|---|---|---|
GET |
/me |
Return identity of the authenticated agent |
| Method | Path | Purpose |
|---|---|---|
GET |
/auth.md |
Human- and agent-readable auth protocol guide |
GET |
/.well-known/jwks.json |
Server public key for token verification |
GET |
/.well-known/oauth-authorization-server |
AS metadata (RFC 8414) |
GET |
/.well-known/oauth-protected-resource |
PRM (RFC 9728) |
| Standard | Role |
|---|---|
| W3C DID Core 1.0 | DID document structure and resolution |
| did:key Method v0.7 | Encoding Ed25519 public keys as DIDs |
| RFC 7517 — JWK | JSON Web Key representation |
| RFC 7518 — JWA | EdDSA algorithm identifier |
| RFC 7519 — JWT | Token structure and standard claims |
| RFC 7638 — JWK Thumbprint | Computation of cnf.jkt |
| RFC 7800 — PoP Key Semantics | Semantics of the cnf claim |
| RFC 8414 — AS Metadata | Authorization server discovery |
| RFC 8693 — Token Exchange | Semantics of the act claim |
| RFC 9449 — DPoP | Per-request proof of possession |
| RFC 9728 — PRM | Protected resource metadata |
| RFC 8032 — Ed25519 | Signature algorithm |