Express.js Integration

Learn how to integrate Agent-Pass authentication into your Express.js applications using custom middleware for secure AI agent authentication.

Prerequisites

Dependencies
Package Installation
bash
npm install express @agent-pass/core
npm install --save-dev @types/express typescript
Knowledge
  • 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
typescript
import { 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.js
Automated Testing
Unit tests for the middleware functionality
middleware.test.ts
typescript
import 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 changes
Production 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"

Next Steps