Agent DID JWT Verification: Third-Party Integration Guide
This document is for third-party service developers who want to integrate Agent DID identity verification — extract a JWT from HTTP requests, verify the signature, and confirm the Agent's identity.
Background
The Agent DID service issues RS256 (RSA + SHA-256) signed JWTs for each AI Agent. When an Agent calls a third-party API, it includes the JWT in the HTTP header. Third-party services do not need access to the Agent DID private key — they can independently verify tokens using the public key from the JWKS endpoint.
Trust chain:
Agent DID Service (Issuer)
│
├── Signs JWT with RSA private key (contains agent_id)
│
└── Exposes public JWKS endpoint /.well-known/jwks.json
│
▼
Third-Party Service (Verifier)
│
├── Extracts JWT from request header
├── Fetches public key from JWKS
├── Verifies signature + expiration
└── Extracts agent_id → identity confirmedJWT Structure
JWTs issued by Agent DID consist of three parts (Header.Payload.Signature):
Header:
{
"alg": "RS256",
"typ": "JWT",
"kid": "agent-did-key"
}Payload:
{
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"email": "agent@example.com",
"iat": 1710000000,
"exp": 1710000900
}| Field | Description |
|---|---|
agent_id | The Agent's unique identifier (UUID), always present |
email | Email provided during registration, optional |
iat | Issued-at timestamp (Unix epoch) |
exp | Expiration time (default: 15 minutes after issuance) |
Integration Steps
Step 1: Extract JWT from HTTP Header
Agents pass JWTs using the standard Bearer Token scheme:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...Extraction logic:
// Node.js / Express
function extractJWT(req) {
const auth = req.headers.authorization || '';
if (!auth.startsWith('Bearer ')) return null;
return auth.slice(7); // "Bearer " is 7 characters
}# Python / Flask
def extract_jwt(request):
auth = request.headers.get('Authorization', '')
if not auth.startswith('Bearer '):
return None
return auth[7:]// Go
func extractJWT(r *http.Request) string {
auth := r.Header.Get("Authorization")
if !strings.HasPrefix(auth, "Bearer ") {
return ""
}
return auth[7:]
}Step 2: Fetch the Public Key
The Agent DID service provides two ways to obtain the public key:
Option A: JWKS Endpoint (Recommended)
GET https://<agent-did-host>/.well-known/jwks.jsonExample response:
{
"keys": [
{
"kty": "RSA",
"n": "0vx7agoebGc...",
"e": "AQAB",
"use": "sig",
"kid": "agent-did-key",
"alg": "RS256"
}
]
}JWKS is an industry standard (RFC 7517) that supports key rotation — match the kid in the JWT header to find the correct public key.
Option B: PEM Endpoint (Simple Scenarios)
GET https://<agent-did-host>/public-key.pemReturns the RSA public key in PEM format. Suitable for single-key setups with no rotation requirements.
Step 3: Verify JWT Signature and Extract Identity
Node.js (using jsonwebtoken + jwks-rsa)
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
// Initialize JWKS client (with built-in caching)
const client = jwksClient({
jwksUri: 'https://<agent-did-host>/.well-known/jwks.json',
cache: true,
cacheMaxAge: 600000, // Cache for 10 minutes
rateLimit: true
});
function getSigningKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}
// Express middleware
function agentAuth(req, res, next) {
const token = extractJWT(req);
if (!token) {
return res.status(401).json({ error: 'missing_bearer_token' });
}
jwt.verify(token, getSigningKey, { algorithms: ['RS256'] }, (err, payload) => {
if (err) {
return res.status(401).json({ error: 'invalid_or_expired_jwt' });
}
// payload.agent_id is the verified Agent identity
req.agent = {
agent_id: payload.agent_id,
email: payload.email || null
};
next();
});
}
// Usage
app.post('/your-api', agentAuth, (req, res) => {
console.log('Request from Agent:', req.agent.agent_id);
// Your business logic...
});Python (using PyJWT)
import jwt
from jwt import PyJWKClient
JWKS_URL = "https://<agent-did-host>/.well-known/jwks.json"
jwks_client = PyJWKClient(JWKS_URL, cache_keys=True)
def verify_agent_jwt(token: str) -> dict:
"""Verify JWT and return payload. Raises on failure."""
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
)
return payload
# Flask decorator
from flask import request, jsonify
from functools import wraps
def require_agent_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
token = extract_jwt(request)
if not token:
return jsonify({"error": "missing_bearer_token"}), 401
try:
payload = verify_agent_jwt(token)
except jwt.ExpiredSignatureError:
return jsonify({"error": "jwt_expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "invalid_jwt"}), 401
request.agent_id = payload["agent_id"]
request.agent_email = payload.get("email")
return f(*args, **kwargs)
return decorated
@app.route("/your-api", methods=["POST"])
@require_agent_auth
def your_api():
print(f"Request from Agent: {request.agent_id}")
# Your business logic...Go (using golang-jwt + lestrrat-go/jwx)
package main
import (
"context"
"net/http"
"strings"
"time"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/golang-jwt/jwt/v5"
)
var jwksCache *jwk.Cache
func init() {
ctx := context.Background()
jwksCache = jwk.NewCache(ctx)
jwksCache.Register("https://<agent-did-host>/.well-known/jwks.json",
jwk.WithRefreshInterval(10*time.Minute))
}
func agentAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := extractJWT(r)
if tokenStr == "" {
http.Error(w, `{"error":"missing_bearer_token"}`, 401)
return
}
ctx := r.Context()
set, err := jwksCache.Get(ctx,
"https://<agent-did-host>/.well-known/jwks.json")
if err != nil {
http.Error(w, `{"error":"jwks_fetch_failed"}`, 500)
return
}
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
kid, _ := t.Header["kid"].(string)
key, ok := set.LookupKeyID(kid)
if !ok {
return nil, jwt.ErrTokenMalformed
}
var rawKey interface{}
key.Raw(&rawKey)
return rawKey, nil
}, jwt.WithValidMethods([]string{"RS256"}))
if err != nil || !token.Valid {
http.Error(w, `{"error":"invalid_or_expired_jwt"}`, 401)
return
}
claims := token.Claims.(jwt.MapClaims)
agentID := claims["agent_id"].(string)
ctx = context.WithValue(ctx, "agent_id", agentID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Step 4 (Optional): Query Agent Details
After verifying the JWT, you have the agent_id. To get more information about the Agent (name, URL, wallet address, etc.), call:
GET https://<agent-did-host>/agent/<agent_id>Example response:
{
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"agent_name": "MyAgent",
"agent_alias": "helper-bot",
"agent_url": "https://myagent.example.com",
"wallet_address": "0xabc...def",
"email": "agent@example.com",
"created_at": 1710000000
}End-to-End Verification Flow
Agent (Client) Third-Party Service (Your API) Agent DID Service
│ │ │
│ POST /your-api │ │
│ Authorization: Bearer <jwt> │ │
│ ─────────────────────────────────> │ │
│ │ │
│ │ 1. Extract Bearer token │
│ │ 2. Decode JWT header → kid │
│ │ │
│ │ GET /.well-known/jwks.json │
│ │ ─────────────────────────────> │
│ │ <───────── { keys: [...] } │
│ │ │
│ │ 3. Match kid to public key │
│ │ 4. Verify RS256 signature │
│ │ 5. Check exp not expired │
│ │ 6. Extract agent_id │
│ │ │
│ │ (Optional) GET /agent/<id> │
│ │ ─────────────────────────────> │
│ │ <───── { agent_name, ... } │
│ │ │
│ <── 200 OK (response) │ │
│ │ │Security Best Practices
Must Do
- Only trust RS256: Explicitly specify
algorithms: ['RS256']during verification to prevent algorithm downgrade attacks (e.g.,noneorHS256) - Check expiration: Most JWT libraries check
expautomatically — make sure this check is not disabled - Use HTTPS: All JWKS fetches and API communication must use TLS to prevent man-in-the-middle public key substitution
- Cache public keys: JWKS does not change frequently — cache for 5–10 minutes to avoid fetching on every request
Should Do
- Allow clock skew: JWT libraries typically support
clockTolerance(e.g., 30 seconds) to handle minor clock differences between servers - Log verification failures: Record failed
agent_idand failure reason for security auditing - Fallback to verify-jwt endpoint: If you cannot verify locally (e.g., environment constraints), Agent DID provides
POST /verify-jwtas a fallback — but local verification is always preferred (no network latency, no single point of dependency)
Do Not
- Do not verify RS256 tokens with HS256: This is a well-known security vulnerability
- Do not ignore
kid: After key rotation, failing to matchkidwill cause verification failures - Do not log JWTs: JWTs are credentials — if leaked, they can be used to impersonate the Agent until expiration
FAQ
Q: What happens when a JWT expires? A: The Agent must call Agent DID's POST /refresh with agent_id + token (the one-time token saved during registration) to obtain a new JWT. Your service just returns 401 — the Agent client is responsible for refreshing.
Q: How do I distinguish between different Agents? A: The agent_id in the JWT payload is a globally unique UUID. Use it for access control, rate limiting, audit logging, etc.
Q: Will key rotation cause service disruption? A: No. Agent DID publishes both old and new public keys in JWKS (with different kid values). Old JWTs remain verifiable until they expire. Third parties just need to periodically refresh their JWKS cache for a smooth transition.
Q: Can I hardcode the public key instead of using JWKS? A: Yes (fetch it from /public-key.pem), but not recommended. Hardcoded keys require manual updates and redeployment during key rotation. JWKS is more flexible.
