Express.js Integration
Learn how to integrate Agent-Pass authentication into your Express.js applications using custom middleware for secure AI agent authentication.
Development Status: This middleware is functional for development and testing. Production features like session management and error handling are still in development.
Prerequisites
Dependencies
Package Installation
bashnpm install express @agent-pass/core
npm install --save-dev @types/express typescriptKnowledge
- Express.js middleware concepts
- HTTP Authorization headers
- Agent-Pass basic flow
Agent-Pass Middleware
The Agent-Pass middleware validates Authorization headers containing Verifiable Presentations and performs dual-signature verification before allowing access to protected routes.
agent-pass-middleware.ts
typescriptimport { AgentPass, VerificationResult } from '@agent-pass/core';
import { Request, Response, NextFunction } from 'express';
// Extend Express Request interface
interface AuthenticatedRequest extends Request {
agent?: {
did: string;
controllerDid: string;
scope: string[];
constraints?: any;
};
}
/**
* Agent-Pass Authentication Middleware
*
* Validates Authorization header containing a Verifiable Presentation
* and performs dual-signature verification.
*/
export function agentPassAuth(agentPass: AgentPass) {
return async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
// Extract Authorization header
const authHeader = req.headers.authorization;
const authString = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!authString || !authString.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Missing or invalid Authorization header',
message: 'Expected: Authorization: Bearer <verifiable-presentation>',
code: 'MISSING_AUTHORIZATION'
});
}
// Extract and parse the Verifiable Presentation
const presentationData = authString.substring(7); // Remove "Bearer "
let presentation;
try {
presentation = JSON.parse(presentationData);
} catch (parseError) {
return res.status(400).json({
error: 'Invalid presentation format',
message: 'Presentation must be valid JSON',
code: 'INVALID_JSON'
});
}
// Extract challenge and domain from query parameters
const challenge = req.query.challenge as string;
const domain = req.hostname || req.get('host') || 'localhost';
if (!challenge) {
return res.status(400).json({
error: 'Missing challenge parameter',
message: 'Challenge is required for verification',
code: 'MISSING_CHALLENGE'
});
}
// Perform dual-signature verification
const verification: VerificationResult = await agentPass.verifyVerifiablePresentation(
presentation,
challenge,
domain
);
if (!verification.verified) {
return res.status(401).json({
error: 'Authentication failed',
message: verification.error || 'Invalid presentation or signatures',
code: 'VERIFICATION_FAILED'
});
}
// Attach agent information to request
req.agent = {
did: verification.agentDid!,
controllerDid: verification.controllerDid!,
scope: verification.scope!,
constraints: verification.constraints
};
// Authentication successful, proceed to next middleware
next();
} catch (error) {
console.error('Agent-Pass authentication error:', error);
return res.status(500).json({
error: 'Internal authentication error',
message: 'An error occurred during authentication',
code: 'INTERNAL_ERROR'
});
}
};
}
/**
* Scope Authorization Middleware
*
* Checks if the authenticated agent has a specific scope permission.
*/
export function requireScope(requiredScope: string) {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.agent) {
return res.status(401).json({
error: 'Not authenticated',
message: 'Agent authentication required',
code: 'NOT_AUTHENTICATED'
});
}
if (!req.agent.scope.includes(requiredScope)) {
return res.status(403).json({
error: 'Insufficient permissions',
message: `Required scope: ${requiredScope}`,
code: 'INSUFFICIENT_SCOPE',
details: {
required: requiredScope,
granted: req.agent.scope
}
});
}
next();
};
}Security Considerations
Best Practices
- Always validate challenges and domain binding
- Use HTTPS in production for header protection
- Implement rate limiting on auth endpoints
- Log all authentication attempts for monitoring
Current Limitations
- No session management (stateless only)
- Basic error handling (production needs enhancement)
- No credential caching (performance impact)
- Manual challenge management required
Testing the Integration
Manual Testing with curl
Test the authentication flow using command-line tools
Testing Commands
bash# 1. Start the server
npm run dev
# 2. Get a challenge
curl -X GET http://localhost:3000/auth/challenge
# 3. Test public endpoint (no auth required)
curl -X GET http://localhost:3000/public
# 4. Test protected endpoint without auth (should fail)
curl -X GET http://localhost:3000/api/profile
# 5. Test with Agent-Pass authentication
# (You'll need to run the client example to generate a proper presentation)
node client-example.jsAutomated Testing
Unit tests for the middleware functionality
middleware.test.ts
typescriptimport request from 'supertest';
import { AgentPass } from '@agent-pass/core';
import app from './express-server';
describe('Agent-Pass Express Middleware', () => {
let agentPass: AgentPass;
let credential: any;
let agent: any;
beforeAll(async () => {
agentPass = new AgentPass();
// Setup test identities and credential
const controller = await agentPass.createControllerIdentity();
agent = await agentPass.createAgentIdentity();
credential = await agentPass.createAgentCapabilityCredential(
controller,
agent,
{ scope: ['read:emails'] }
);
});
test('should allow access to public endpoints', async () => {
const response = await request(app)
.get('/public')
.expect(200);
expect(response.body.message).toBe('This is a public endpoint');
});
test('should reject protected endpoints without auth', async () => {
await request(app)
.get('/api/profile')
.expect(401);
});
test('should authenticate valid Agent-Pass presentations', async () => {
// Get challenge
const challengeResponse = await request(app)
.get('/auth/challenge')
.expect(200);
const { challenge, domain } = challengeResponse.body;
// Create presentation
const presentation = await agentPass.createVerifiablePresentation(
agent,
credential,
challenge,
domain
);
// Test authenticated request
const response = await request(app)
.get(`/api/profile?challenge=${challenge}`)
.set('Authorization', `Bearer ${JSON.stringify(presentation)}`)
.expect(200);
expect(response.body.message).toBe('Successfully authenticated!');
expect(response.body.agent.did).toBe(agent.did);
});
});Running the Server
Development Mode
Run the server with hot reloading for development
Development
bash# Using nodemon for auto-restart
npm install -g nodemon
nodemon --exec ts-node express-server.ts
# Or with package.json script
npm run dev
# Server will restart on file changesProduction Mode
Compile and run for production deployment
Production
bash# Compile TypeScript
npx tsc
# Run compiled JavaScript
node dist/express-server.js
# Or with PM2 for production
pm2 start dist/express-server.js --name "agent-pass-api"