← Back to Agent Passport

Agent Passport OAuth for External Software Providers

A comprehensive guide for external software providers to implement Agent Passport authentication, enabling AI agents to securely access your services without passwords or API keys

Why Agent Passport OAuth?

Traditional OAuth relies on browser redirects and human interaction. AI agents need programmatic flows while maintaining cryptographic security. Agent Passport authentication provides:

  • Self-sovereign identity (portable across platforms)
  • No stored secrets (agents sign requests with private keys)
  • Replay protection (one-time nonces)
  • Audit trails (every authentication logged)
  • Capability-based permissions (transaction limits, scopes)

Overview

This guide shows you how to add Agent Passport authentication to your platform, allowing AI agents to authenticate using cryptographic signatures instead of traditional OAuth flows. Agents using NervePay can access your services with their passport credentials.

Compatibility: This authentication method works alongside traditional OAuth. You can support both human users (OAuth) and AI agents (Agent Passport) simultaneously.

Authentication Flow

1

Agent Initiates Request

Agent makes API request to your service with signature headers (Agent-Passport, X-Agent-Signature, X-Agent-Nonce, X-Signature-Timestamp)

GET /api/your-endpoint
Agent-Passport: <passport_id>
X-Agent-Signature: base64EncodedSignature
X-Agent-Nonce: unique-uuid-v4
X-Signature-Timestamp: 2026-02-03T10:30:00Z
2

Your Service Validates Headers

Extract and validate required headers. Check timestamp freshness (5-minute window) and verify nonce hasn't been used before.

// Check timestamp is within 5 minutes
const now = new Date();
const timestamp = new Date(headers['x-signature-timestamp']);
const diff = Math.abs(now - timestamp) / 1000;
if (diff > 300) throw new Error('Timestamp expired');

// Check nonce hasn't been used (store in Redis/DB)
if (await nonceExists(headers['x-agent-nonce'])) {
  throw new Error('Nonce already used');
}
3

Resolve Passport Document

Query the agent's passport document to retrieve their public key. Passport documents can be resolved via NervePay's public API.

// Resolve passport document
const passportId = headers['agent-passport'];
const response = await fetch(
  `https://api.nervepay.xyz/v1/passport/resolve/${encodeURIComponent(passportId)}`
);
const passportDocument = await response.json();
const publicKey = passportDocument.verificationMethod[0].publicKeyBase58;
4

Verify Signature

Reconstruct the signature payload and verify it using the agent's public key (Ed25519). The payload includes method, path, query, body hash, nonce, timestamp, and passport ID.

import nacl from 'tweetnacl';
import { decodeBase58 } from 'some-base58-library';

// Build signature payload (must match agent's payload)
const payload = {
  method: req.method,
  path: req.path,
  query: req.query ? JSON.stringify(req.query) : null,
  body: req.body ? hashBody(req.body) : null, // SHA-256 hash
  nonce: headers['x-agent-nonce'],
  timestamp: headers['x-signature-timestamp'],
  agent_passport: headers['agent-passport']
};

// Verify signature
const message = Buffer.from(JSON.stringify(payload));
const signature = Buffer.from(headers['x-agent-signature'], 'base64');
const publicKey = decodeBase58(publicKey);

const valid = nacl.sign.detached.verify(message, signature, publicKey);
if (!valid) throw new Error('Invalid signature');
5

Check Capabilities & Permissions

Verify the agent has permission to access this endpoint. Check their passport document for transaction limits, allowed operations, and scope.

// Check agent capabilities
const capabilities = passportDocument.capabilities;
if (!capabilities.operations.includes('your_service:read')) {
  throw new Error('Agent lacks required permission');
}

// Check transaction limits if applicable
if (requestAmount > capabilities.payments.max_per_transaction) {
  throw new Error('Amount exceeds agent limit');
}
6

Grant Access & Log Activity

Store the nonce (mark as used), log the authentication attempt for audit, and proceed with the request. Return your API response.

// Store nonce (expires in 10 minutes)
await storeNonce(nonce, passportId, expiresAt: Date.now() + 600000);

// Log authentication
await db.logAgentAuth({
  agent_passport: passportId,
  endpoint: req.path,
  status: 'success',
  timestamp: new Date()
});

// Proceed with request
return res.json({ success: true, data: yourData });

Complete Implementation Examples

Node.js / Express Middleware

// agent-auth-middleware.js
import nacl from 'tweetnacl';
import bs58 from 'bs58';
import crypto from 'crypto';

export async function authenticateAgent(req, res, next) {
  try {
    // Extract headers
    const passportId = req.headers['agent-passport'];
    const signature = req.headers['x-agent-signature'];
    const nonce = req.headers['x-agent-nonce'];
    const timestamp = req.headers['x-signature-timestamp'];

    if (!passportId || !signature || !nonce || !timestamp) {
      return res.status(401).json({ error: 'Missing authentication headers' });
    }

    // Validate timestamp (5-minute window)
    const now = new Date();
    const reqTime = new Date(timestamp);
    const diffSeconds = Math.abs((now - reqTime) / 1000);
    if (diffSeconds > 300) {
      return res.status(401).json({ error: 'Timestamp expired' });
    }

    // Check nonce hasn't been used (Redis recommended)
    const nonceUsed = await redis.get(`nonce:${nonce}`);
    if (nonceUsed) {
      return res.status(401).json({ error: 'Nonce already used' });
    }

    // Resolve passport document
    const passportResponse = await fetch(
      `https://api.nervepay.xyz/v1/passport/resolve/${encodeURIComponent(passportId)}`
    );
    if (!passportResponse.ok) {
      return res.status(401).json({ error: 'Invalid passport' });
    }
    const passportDocument = await passportResponse.json();
    const publicKeyBase58 = passportDocument.verificationMethod[0].publicKeyBase58;

    // Build signature payload
    const bodyHash = req.body ?
      crypto.createHash('sha256').update(JSON.stringify(req.body)).digest('hex') :
      null;

    const payload = {
      method: req.method,
      path: req.path,
      query: Object.keys(req.query).length > 0 ? JSON.stringify(req.query) : null,
      body: bodyHash,
      nonce,
      timestamp,
      agent_passport: passportId
    };

    // Verify signature
    const message = Buffer.from(JSON.stringify(payload));
    const signatureBytes = Buffer.from(signature, 'base64');
    const publicKeyBytes = bs58.decode(publicKeyBase58);

    const valid = nacl.sign.detached.verify(
      message,
      signatureBytes,
      publicKeyBytes
    );

    if (!valid) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Store nonce (expires in 10 minutes)
    await redis.set(`nonce:${nonce}`, '1', 'EX', 600);

    // Attach agent info to request
    req.agent = {
      passportId,
      capabilities: passportDocument.capabilities,
      reputation: passportDocument.reputation_score
    };

    next();
  } catch (error) {
    console.error('Agent authentication error:', error);
    return res.status(500).json({ error: 'Authentication failed' });
  }
}

// Usage in routes
app.get('/api/protected', authenticateAgent, (req, res) => {
  res.json({
    message: 'Authenticated!',
    agent: req.agent.passportId
  });
});

Python / FastAPI Dependency

# agent_auth.py
from fastapi import Header, HTTPException, Depends
from datetime import datetime, timezone, timedelta
import httpx
import base64
import hashlib
import json
from nacl.signing import VerifyKey
from nacl.encoding import Base58Encoder

class AgentAuth:
    def __init__(self, passport_id: str, capabilities: dict, reputation: float):
        self.passport_id = passport_id
        self.capabilities = capabilities
        self.reputation = reputation

async def verify_agent_signature(
    agent_passport: str = Header(..., alias="Agent-Passport"),
    signature: str = Header(..., alias="X-Agent-Signature"),
    nonce: str = Header(..., alias="X-Agent-Nonce"),
    timestamp: str = Header(..., alias="X-Signature-Timestamp"),
    request: Request
) -> AgentAuth:
    """Verify agent passport signature and return authenticated agent"""

    # Validate timestamp (5-minute window)
    req_time = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
    now = datetime.now(timezone.utc)
    diff = abs((now - req_time).total_seconds())
    if diff > 300:
        raise HTTPException(401, "Timestamp expired")

    # Check nonce (use Redis in production)
    if await redis.exists(f"nonce:{nonce}"):
        raise HTTPException(401, "Nonce already used")

    # Resolve passport document
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"https://api.nervepay.xyz/v1/passport/resolve/{agent_passport}"
        )
        if response.status_code != 200:
            raise HTTPException(401, "Invalid passport")
        passport_doc = response.json()

    public_key_base58 = passport_doc["verificationMethod"][0]["publicKeyBase58"]

    # Build signature payload
    body = await request.body()
    body_hash = hashlib.sha256(body).hexdigest() if body else None

    payload = {
        "method": request.method,
        "path": str(request.url.path),
        "query": str(request.url.query) if request.url.query else None,
        "body": body_hash,
        "nonce": nonce,
        "timestamp": timestamp,
        "agent_passport": agent_passport
    }

    # Verify signature
    message = json.dumps(payload, separators=(',', ':')).encode()
    signature_bytes = base64.b64decode(signature)
    verify_key = VerifyKey(public_key_base58, encoder=Base58Encoder)

    try:
        verify_key.verify(message, signature_bytes)
    except Exception:
        raise HTTPException(401, "Invalid signature")

    # Store nonce (expires in 10 minutes)
    await redis.set(f"nonce:{nonce}", "1", ex=600)

    # Return authenticated agent
    return AgentAuth(
        passport_id=agent_passport,
        capabilities=passport_doc.get("capabilities", {}),
        reputation=passport_doc.get("reputation_score", 50.0)
    )

# Usage in endpoints
@app.get("/api/protected")
async def protected_endpoint(agent: AgentAuth = Depends(verify_agent_signature)):
    return {
        "message": "Authenticated!",
        "agent_passport": agent.passport_id,
        "reputation": agent.reputation
    }

Passport Resolution

To verify agent signatures, you need to resolve their passport to get their public key. You have two options:

Option 1: Use NervePay's Passport Resolution API

Query NervePay's public API to resolve passports. This is the easiest option and ensures you always get up-to-date agent information.

GET https://api.nervepay.xyz/v1/passport/resolve/{passport_id}

Response:
{
  "passport_id": "<passport_id>",
  "verificationMethod": [{
    "id": "<passport_id>#key-1",
    "type": "Ed25519VerificationKey2020",
    "publicKeyBase58": "..."
  }],
  "capabilities": { ... },
  "reputation_score": 85.5
}

Option 2: Cache Passport Documents Locally

Cache resolved passport documents in your database for faster lookups. Refresh periodically (e.g., daily) to stay updated.

// Cache passport documents with TTL
const cachedPassport = await cache.get(passportId);
if (!cachedPassport || isExpired(cachedPassport)) {
  const fresh = await fetchPassportDocument(passportId);
  await cache.set(passportId, fresh, ttl: 86400);
  return fresh;
}
return cachedPassport;

Security Best Practices

🕐

Timestamp Validation

Always enforce a 5-minute window for request freshness. Reject requests with timestamps too old or in the future.

🔑

Nonce Storage

Use Redis or a similar fast key-value store for nonce tracking. Set TTL to 10 minutes. Reject duplicate nonces immediately.

⏱️

Rate Limiting

Implement rate limits per passport to prevent abuse. Consider agent reputation scores when setting limits.

📝

Audit Logging

Log every authentication attempt (success and failure) with timestamp, passport ID, endpoint, and result for compliance.

Capability Checks

Always verify agent has permission for the requested operation. Check transaction limits before processing payments.

🔒

HTTPS Only

Enforce HTTPS for all API endpoints. Never accept agent signatures over unencrypted HTTP connections.

Testing Your Implementation

Before going live, test your agent authentication with these tools:

1. Create a Test Agent Identity

Register a test agent on NervePay's dashboard to get a DID and private key for testing.

Create Test Agent →

2. Use the Signature Test Script

Use our example Python/Node.js scripts to generate valid signatures and test your endpoint.

View Test Scripts →

3. Verify Edge Cases

Test expired timestamps, duplicate nonces, invalid signatures, and missing headers to ensure your implementation is robust.

Integration Checklist

Need Help?

We're here to help you integrate agent authentication. Reach out if you have questions or need guidance.