Skip to content

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 confirmed

JWT Structure

JWTs issued by Agent DID consist of three parts (Header.Payload.Signature):

Header:

json
{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "agent-did-key"
}

Payload:

json
{
  "agent_id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "agent@example.com",
  "iat": 1710000000,
  "exp": 1710000900
}
FieldDescription
agent_idThe Agent's unique identifier (UUID), always present
emailEmail provided during registration, optional
iatIssued-at timestamp (Unix epoch)
expExpiration 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:

javascript
// 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
# Python / Flask
def extract_jwt(request):
    auth = request.headers.get('Authorization', '')
    if not auth.startswith('Bearer '):
        return None
    return auth[7:]
go
// 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:

GET https://<agent-did-host>/.well-known/jwks.json

Example response:

json
{
  "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.pem

Returns 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)

javascript
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)

python
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)

go
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:

json
{
  "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., none or HS256)
  • Check expiration: Most JWT libraries check exp automatically — 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_id and failure reason for security auditing
  • Fallback to verify-jwt endpoint: If you cannot verify locally (e.g., environment constraints), Agent DID provides POST /verify-jwt as 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 match kid will 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.

Released under the MIT License.