agent-did-server
v0.1.1
Published
Production-ready W3C DID authentication server for AI agents and applications
Maintainers
Readme
agent-did-server
Production-ready W3C DID authentication server for AI agents and applications.
Implements the complete challenge-response authentication flow using Decentralized Identifiers (DIDs) and Ed25519 signatures. Works seamlessly with the agent-did CLI.
npm install agent-did-server expressFeatures
✅ Complete Authentication Flow
- Challenge generation with cryptographic nonces
- Ed25519 signature verification
- JWT issuance and validation
- Protected route middleware
✅ Multiple Database Adapters
- In-Memory - Development and testing
- SQLite - Single-server production
- PostgreSQL - Multi-server production
- Redis - High-traffic with automatic expiration
✅ Production-Ready
- TypeScript with full type safety
- Security headers (Helmet)
- CORS configuration
- Automatic challenge cleanup
- Comprehensive error handling
✅ Flexible Integration
- Use as standalone server
- Add to existing Express apps
- Framework-agnostic middleware
- Customizable routes and adapters
Quick Start
Standalone Server
import { createServer } from 'agent-did-server';
const app = createServer({
jwtSecret: process.env.JWT_SECRET!,
jwtIssuer: 'did:web:example.com',
audience: 'my-app',
domain: 'example.com',
});
app.listen(3000, () => {
console.log('DID Auth Server running on port 3000');
});Add to Existing Express App
import express from 'express';
import {
createAuthRoute,
createVerifyRoute,
authenticate,
MemoryAdapter,
} from 'agent-did-server';
const app = express();
app.use(express.json());
const config = {
jwtSecret: process.env.JWT_SECRET!,
jwtIssuer: 'did:web:example.com',
jwtExpiresIn: 900, // 15 minutes
challengeExpiresIn: 120, // 2 minutes
audience: 'my-app',
domain: 'example.com',
adapter: new MemoryAdapter(),
};
// Add DID authentication routes
app.use(createAuthRoute(config));
app.use(createVerifyRoute(config));
// Protect your routes
app.get(
'/api/data',
authenticate(config.jwtSecret, config.jwtIssuer),
(req, res) => {
const { did } = req.auth!;
res.json({ message: `Hello, ${did}!`, data: 'protected' });
}
);
app.listen(3000);API Endpoints
POST /auth - Request Challenge
Generate an authentication challenge for a DID.
Request:
{
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
}Response:
{
"challengeId": "550e8400-e29b-41d4-a716-446655440000",
"nonce": "Q5VNB8jBnp68ev_9Z8nS4yLZWxR7bvFSmW2rKJW17To",
"expiresAt": "2024-02-04T10:32:00Z",
"audience": "my-app",
"domain": "example.com"
}POST /verify - Verify Signature & Get JWT
Verify the signed challenge and receive a JWT for authenticated sessions.
Request:
{
"challengeId": "550e8400-e29b-41d4-a716-446655440000",
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"payloadB64": "eyJub25jZSI6IlE1Vk5COGpCbnA2OGV2XzlaOG5TNHlMWld4UjdidkZTbVcycktKVzE3VG8iLCJhdWQiOiJteS1hcHAiLCJkb21haW4iOiJleGFtcGxlLmNvbSIsImlhdCI6MTcwNTMxODIwMCwiZXhwIjoxNzA1MzE4MzIwLCJkaWQiOiJkaWQ6a2V5Ono2TWtoYVhnQlpEdm90RGtMNTI1N2ZhaXp0aUdpQzJRdEtMR3Bibm5FR3RhMmRvSyJ9",
"signature": "G_8u2IKtJcW7eLWgRXQWpHCwvDF5mzODp2kT9HoLLHU8cHjiNYddfQN3JDFruS2fOheYqcJ8mfeCKH298V8oAA",
"alg": "EdDSA"
}Response:
{
"jwt": "eyJhbGciOiJIUzI1NiJ9.eyJzY29wZSI6ImFnZW50OmF1dGgiLCJzdWIiOiJkaWQ6a2V5Ono2TWtoYVhnQlpEdm90RGtMNTI1N2ZhaXp0aUdpQzJRdEtMR3Bibm5FR3RhMmRvSyIsImlzcyI6ImRpZDp3ZWI6ZXhhbXBsZS5jb20iLCJpYXQiOjE3MDUzMTgyMDAsImV4cCI6MTcwNTMxOTEwMH0.signature",
"expiresAt": "2024-02-04T10:47:00Z"
}GET /account - Protected Endpoint Example
Example protected endpoint that requires JWT authentication.
Request:
GET /account
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...Response:
{
"did": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
"scope": "agent:auth",
"issuer": "did:web:example.com",
"authenticated": true,
"issuedAt": 1705318200,
"expiresAt": 1705319100
}Complete Authentication Flow
Using agent-did CLI
# 1. Request challenge
curl -X POST http://localhost:3000/auth \
-H "Content-Type: application/json" \
-d '{"did": "did:key:z6Mkj7yH..."}'
# Response: { "challengeId": "...", "nonce": "...", ... }
# 2. Sign challenge with agent-did CLI
agent-did auth sign \
--did "did:key:z6Mkj7yH..." \
--challenge "NONCE_FROM_STEP_1" \
--audience "my-app" \
--domain "example.com" \
--json > signed-auth.json
# 3. Verify and get JWT
curl -X POST http://localhost:3000/verify \
-H "Content-Type: application/json" \
-d @signed-auth.json
# Response: { "jwt": "...", "expiresAt": "..." }
# 4. Access protected endpoints
curl http://localhost:3000/account \
-H "Authorization: Bearer YOUR_JWT_HERE"Database Adapters
In-Memory (Default)
Best for development and testing. Data is lost on restart.
import { createServer, MemoryAdapter } from 'agent-did-server';
const app = createServer({
adapter: new MemoryAdapter(),
});SQLite
Best for single-server production deployments. Persistent storage.
import { createServer, SQLiteAdapter } from 'agent-did-server';
const app = createServer({
adapter: new SQLiteAdapter('./data/challenges.db'),
});PostgreSQL
Best for multi-server production deployments. Shared database.
import { createServer, PostgresAdapter } from 'agent-did-server';
const app = createServer({
adapter: new PostgresAdapter(process.env.DATABASE_URL!),
});Redis
Best for high-traffic production. Automatic TTL expiration.
import { createServer, RedisAdapter } from 'agent-did-server';
const app = createServer({
adapter: new RedisAdapter(process.env.REDIS_URL!),
});Configuration Options
interface ServerConfig {
// Required
jwtSecret: string; // JWT signing secret (use strong random value)
jwtIssuer: string; // JWT issuer identifier (e.g., "did:web:example.com")
// Optional
port?: number; // Server port (default: 3000)
jwtExpiresIn?: number; // JWT expiration in seconds (default: 900 = 15 min)
challengeExpiresIn?: number; // Challenge expiration in seconds (default: 120 = 2 min)
audience?: string; // Authentication audience (default: "agent-did-auth")
domain?: string; // Server domain (default: "localhost")
adapter?: ChallengeAdapter; // Database adapter (default: MemoryAdapter)
cors?: boolean; // Enable CORS (default: true)
corsOrigin?: string | string[]; // CORS origins (default: "*")
}Protecting Your Routes
Use the authenticate middleware to protect custom routes:
import { authenticate, AuthenticatedRequest } from 'agent-did-server';
// Protect a single route
app.get(
'/protected',
authenticate(jwtSecret, jwtIssuer),
(req: AuthenticatedRequest, res) => {
const { did, scope } = req.auth!;
res.json({ message: `Hello, ${did}!` });
}
);
// Protect multiple routes
const auth = authenticate(jwtSecret, jwtIssuer);
app.get('/route1', auth, handler1);
app.post('/route2', auth, handler2);
app.delete('/route3', auth, handler3);Security Best Practices
- Always use HTTPS in production
- Generate strong JWT secrets (32+ bytes, cryptographically random):
openssl rand -base64 32 - Set appropriate CORS origins (don't use
*in production) - Use persistent storage (SQLite, PostgreSQL, Redis) in production
- Monitor challenge usage to detect replay attacks
- Rotate JWT secrets periodically
- Set short JWT expiration times (15 minutes recommended)
- Rate limit challenge and verify endpoints
Critical Implementation Notes
Ed25519 Signature Verification
This package correctly configures @noble/ed25519 with SHA-512 hashing. Without this configuration, signature verification fails:
import * as ed25519 from '@noble/ed25519';
import { createHash } from 'crypto';
// REQUIRED for @noble/ed25519 v2.x
ed25519.etc.sha512Sync = (...m) =>
createHash('sha512').update(Buffer.concat(m)).digest();This is already handled internally by agent-did-server.
Dual Signature Verification
The server tries two verification approaches for maximum compatibility:
- Verify signature against base64url-decoded payload bytes
- Verify signature against UTF-8-encoded base64url string (agent-did CLI uses this)
Environment Variables
Create a .env file in your project root:
# Server
PORT=3000
# JWT (REQUIRED)
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
JWT_ISSUER=did:web:example.com
# Authentication
AUDIENCE=my-app
DOMAIN=example.com
# CORS
CORS_ORIGIN=*
# Database (optional, based on adapter)
# SQLITE_DB_PATH=./data/challenges.db
# DATABASE_URL=postgresql://user:password@localhost:5432/agent_did
# REDIS_URL=redis://localhost:6379TypeScript Support
Fully typed with TypeScript. Import types as needed:
import type {
ServerConfig,
Challenge,
ChallengeAdapter,
JWTPayload,
AuthenticatedRequest,
} from 'agent-did-server';Testing
npm test
npm run test:watch
npm run test:coverageExamples
See the examples/ directory for:
- Basic standalone server
- SQLite integration
- PostgreSQL integration
- Redis integration
- Custom protected routes
- Next.js integration
Documentation
- STANDARDS.md - W3C DID authentication protocol specification
- INTEGRATION.md - Integration guide for existing apps
- ADAPTERS.md - Creating custom database adapters
Related Projects
- agent-did - CLI for creating DIDs and signing challenges
- agent-did.xyz - Project website and documentation
Standards Compliance
- W3C DID Core 1.0: https://www.w3.org/TR/did-core/
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC 7519: JSON Web Token (JWT)
License
MIT - see LICENSE
Contributing
Contributions are welcome! Please open an issue or PR at github.com/dantber/agent-did-server
Built with ❤️ for the agentic future
