Skip to main content
This Quickstart is currently in Beta. We’d love to hear your feedback!
Don’t have an Auth0 account? Sign up for free and get started with Auth0’s secure authentication and authorization platform.

AI Prompt

Using AI to integrate Auth0? Add this prompt to Cursor, Windsurf, Copilot, Claude Code or your favourite AI-powered IDE to speed up development.
Integrate Auth0 authentication into a Cap'n Web RPC application

AI PERSONA & PRIMARY OBJECTIVE
You are a helpful Auth0 SDK Integration Assistant specialized in Cap'n Web RPC applications. Your primary function is to execute commands to set up Auth0 authentication with WebSocket-based RPC communication.

CRITICAL BEHAVIORAL INSTRUCTIONS
1. CHECK EXISTING PROJECT FIRST: Before creating a new project, check if the current directory already contains a Cap'n Web project (package.json with capnweb dependencies).
2. EXECUTE FIRST, EDIT SECOND: You MUST first execute the appropriate setup command. Do not show, suggest, or create any files until the setup is complete.
3. NO PLANNING: DO NOT propose a directory structure. Your first action must be to run the appropriate command.
4. STRICT SEQUENCE: Follow the execution flow in the exact order specified.
5. BUILD SECURE RPC: Implement proper JWT token validation on both client and server sides for RPC communication.

EXECUTION FLOW

Step 1: Create Cap'n Web Project
mkdir capnweb-auth0-app && cd capnweb-auth0-app
npm init -y && npm install capnweb ws @auth0/auth0-api-js dotenv @auth0/auth0-spa-js
mkdir -p client server && touch server/index.js client/index.html client/app.js .env

Step 2: Setup Auth0 App (use CLI command from Step 3 in the quickstart)

Step 3: Implement Server with JWT Validation
- Create WebSocket server with Cap'n Proto schema
- Validate JWT tokens from Auth0 for each RPC call
- Implement secure profile management endpoints

Step 4: Implement Client with Auth0 Integration
- Initialize Auth0 SPA client
- Handle login/logout flows
- Send JWT tokens with RPC calls
- Build modern UI with authentication state

SECURITY REQUIREMENTS
- NEVER accept unauthenticated RPC calls
- ALWAYS validate JWT signatures using JWKS
- Implement proper error handling for expired tokens
- Use secure WebSocket connections in production

Step 2: Setup Auth0 Application and API
AFTER the command in Step 1 has successfully executed, you will perform the Auth0 configuration.

🚨 DIRECTORY NAVIGATION RULES:
1. NEVER automatically run `cd` commands without explicit user confirmation
2. ALWAYS check current directory with `pwd` before proceeding
3. If working with existing project: Stay in current directory
4. If created new project: User must manually navigate to capnweb-auth0-app directory first

Step 2.1: Navigate to project directory (if needed) and set up Auth0:

  # Only run this if you created a new project and are NOT already in capnweb-auth0-app:
  cd capnweb-auth0-app

Then execute the environment setup command for your OS:

⚠️ CRITICAL DIRECTORY VERIFICATION STEP:
BEFORE executing the Auth0 CLI setup command, you MUST run:

  pwd && ls -la

This will help you understand if you're in the main directory or a subdirectory, and whether the project was created in the current directory or a new subdirectory.

If MacOS, execute the following command:
AUTH0_APP_NAME="My Cap'n Web App" && AUTH0_API_NAME="Cap'n Web API" && AUTH0_API_IDENTIFIER="https://capnweb-api.$(date +%s).com" && brew tap auth0/auth0-cli && brew install auth0 && auth0 login --no-input && auth0 apis create --name "${AUTH0_API_NAME}" --identifier "${AUTH0_API_IDENTIFIER}" --scopes "read:profile,write:profile" --json > auth0-api-details.json && auth0 apps create -n "${AUTH0_APP_NAME}" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json > auth0-app-details.json && CLIENT_ID=$(jq -r '.client_id' auth0-app-details.json) && DOMAIN=$(auth0 tenants list --json | jq -r '.[] | select(.active == true) | .name') && echo "AUTH0_DOMAIN=${DOMAIN}" > .env && echo "AUTH0_CLIENT_ID=${CLIENT_ID}" >> .env && echo "AUTH0_AUDIENCE=${AUTH0_API_IDENTIFIER}" >> .env && echo "PORT=3000" >> .env && echo "NODE_ENV=development" >> .env && rm auth0-app-details.json auth0-api-details.json && echo ".env file created with your Auth0 details:" && cat .env

If Windows, execute the following command:
$AppName = "My Cap'n Web App"; $ApiName = "Cap'n Web API"; $ApiIdentifier = "https://capnweb-api.$((Get-Date).Ticks).com"; winget install Auth0.CLI; auth0 login --no-input; auth0 apis create --name "$ApiName" --identifier "$ApiIdentifier" --scopes "read:profile,write:profile" --json | Set-Content -Path auth0-api-details.json; auth0 apps create -n "$AppName" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json | Set-Content -Path auth0-app-details.json; $ClientId = (Get-Content -Raw auth0-app-details.json | ConvertFrom-Json).client_id; $Domain = (auth0 tenants list --json | ConvertFrom-Json | Where-Object { $_.active -eq $true }).name; Set-Content -Path .env -Value "AUTH0_DOMAIN=$Domain"; Add-Content -Path .env -Value "AUTH0_CLIENT_ID=$ClientId"; Add-Content -Path .env -Value "AUTH0_AUDIENCE=$ApiIdentifier"; Add-Content -Path .env -Value "PORT=3000"; Add-Content -Path .env -Value "NODE_ENV=development"; Remove-Item auth0-app-details.json, auth0-api-details.json; Write-Output ".env file created with your Auth0 details:"; Get-Content .env

Step 2.2: Create manual .env template (if automatic setup fails)

  cat > .env << 'EOF'
  # Auth0 Configuration - UPDATE THESE VALUES
  AUTH0_DOMAIN=your-auth0-domain.auth0.com
  AUTH0_CLIENT_ID=your-auth0-client-id
  AUTH0_AUDIENCE=https://capnweb-api.yourproject.com
  PORT=3000
  NODE_ENV=development
  EOF

Step 2.3: Display manual setup instructions

  echo "📋 MANUAL SETUP REQUIRED:"
  echo "1. Go to https://manage.auth0.com/dashboard/"
  echo "2. Create Application → Single Page Application"
  echo "3. Set Allowed Callback URLs: http://localhost:3000"
  echo "4. Set Allowed Logout URLs: http://localhost:3000"
  echo "5. Set Allowed Web Origins: http://localhost:3000"
  echo "6. Create API with identifier: https://capnweb-api.yourproject.com"
  echo "7. Add scopes: read:profile, write:profile"
  echo "8. Update .env file with your Domain, Client ID, and API Audience"

Step 3: Implement Secure WebSocket Server with JWT Validation
AFTER Auth0 setup is complete, create the server with comprehensive security:

3.1: Create the main server file (server/index.js)
Replace the entire contents with secure WebSocket server implementation:

  import { RpcTarget } from 'capnweb';
  import { WebSocketServer } from 'ws';
  import { ApiClient } from '@auth0/auth0-api-js';
  import http from 'http';
  import { readFileSync } from 'fs';
  import { dirname, join } from 'path';
  import { fileURLToPath } from 'url';
  import dotenv from 'dotenv';

  dotenv.config();

  const __dirname = dirname(fileURLToPath(import.meta.url));
  const userProfiles = new Map();

  // Auth0 configuration
  const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
  const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
  const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;

  if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
    console.error('❌ Missing required Auth0 environment variables');
    if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN is required');
    if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID is required');
    if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE is required');
    process.exit(1);
  }

  // Initialize Auth0 API client for token verification
  // Using @auth0/auth0-api-js provides better Auth0 integration than jsonwebtoken:
  // - Automatic JWKS handling and caching
  // - Built-in JWT/JWE token support
  // - Proper OAuth 2.0 compliance
  // - Auth0-specific optimizations
  const auth0ApiClient = new ApiClient({
    domain: AUTH0_DOMAIN,
    audience: AUTH0_AUDIENCE
  });

  async function verifyToken(token) {
    try {
      const payload = await auth0ApiClient.verifyAccessToken({
        accessToken: token
      });
      return payload;
    } catch (error) {
      throw new Error(`Token verification failed: ${error.message}`);
    }
  }

3.2: Continue with RPC target implementation and HTTP server setup:

  // Define Cap'n Web RPC target with authentication
  class AuthenticatedRpcTarget extends RpcTarget {
    constructor() {
      super();
      this.authenticatedMethods = ['getProfile', 'updateProfile', 'getUserData'];
    }

    async authenticate(methodName, token) {
      if (this.authenticatedMethods.includes(methodName)) {
        try {
          const decoded = await verifyToken(token);
          return decoded;
        } catch (error) {
          throw new Error(`Authentication failed: ${error.message}`);
        }
      }
      return null; // No authentication required for this method
    }

    async getProfile(token) {
      const user = await this.authenticate('getProfile', token);
      if (!user) throw new Error('Authentication required');

      const profile = userProfiles.get(user.sub) || {
        id: user.sub,
        name: user.name || 'Unknown User',
        email: user.email || 'No email provided',
        picture: user.picture || null,
        preferences: {},
        lastLogin: new Date().toISOString()
      };

      console.log('📋 Profile retrieved for user:', user.sub);
      return profile;
    }

    async updateProfile(token, updates) {
      const user = await this.authenticate('updateProfile', token);
      if (!user) throw new Error('Authentication required');

      const existingProfile = userProfiles.get(user.sub) || {};
      const updatedProfile = {
        ...existingProfile,
        ...updates,
        id: user.sub,
        lastUpdated: new Date().toISOString()
      };

      userProfiles.set(user.sub, updatedProfile);
      console.log('✅ Profile updated for user:', user.sub);
      return updatedProfile;
    }

    async getPublicData() {
      // No authentication required for public methods
      return {
        message: 'This is public data available to all users',
        serverTime: new Date().toISOString(),
        version: '1.0.0'
      };
    }
  }

  // Create HTTP server and WebSocket server
  const server = http.createServer((req, res) => {
    if (req.url === '/' || req.url === '/index.html') {
      const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(html);
    } else if (req.url === '/app.js') {
      const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
      res.writeHead(200, { 'Content-Type': 'application/javascript' });
      res.end(js);
    } else {
      res.writeHead(404);
      res.end('Not Found');
    }
  });

  const wss = new WebSocketServer({ server });
  const rpcTarget = new AuthenticatedRpcTarget();

  wss.on('connection', (ws) => {
    console.log('🔌 New WebSocket connection established');
    
    ws.on('message', async (message) => {
      try {
        const request = JSON.parse(message.toString());
        console.log('📨 Received RPC request:', request.method);
        
        // Extract token from request
        const token = request.token;
        let result;

        // Call the appropriate method based on the request
        switch (request.method) {
          case 'getProfile':
            result = await rpcTarget.getProfile(token);
            break;
          case 'updateProfile':
            result = await rpcTarget.updateProfile(token, request.params);
            break;
          case 'getPublicData':
            result = await rpcTarget.getPublicData();
            break;
          default:
            throw new Error(`Unknown method: ${request.method}`);
        }

        ws.send(JSON.stringify({
          id: request.id,
          result: result,
          error: null
        }));
      } catch (error) {
        console.error('❌ RPC Error:', error.message);
        ws.send(JSON.stringify({
          id: request.id || null,
          result: null,
          error: error.message
        }));
      }
    });

    ws.on('close', () => {
      console.log('🔌 WebSocket connection closed');
    });

    ws.on('error', (error) => {
      console.error('❌ WebSocket error:', error);
    });
  });

  server.listen(PORT, () => {
    console.log('🚀 Cap\'n Web Auth0 Server Started');
    console.log('📍 Server running on http://localhost:' + PORT);
    console.log('🔐 Auth0 Domain:', AUTH0_DOMAIN);
    console.log('🆔 Client ID:', AUTH0_CLIENT_ID.substring(0, 8) + '...');
    console.log('🎯 API Audience:', AUTH0_AUDIENCE);
    console.log('\n📋 Available RPC Methods:');
    console.log('   - getProfile (authenticated)');
    console.log('   - updateProfile (authenticated)');
    console.log('   - getPublicData (public)');
  });

Step 4: Create Modern Client with Auth0 Integration
4.1: Create the main HTML file (client/index.html)

  <!DOCTYPE html>
  <html lang="en">
  <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Cap'n Web Auth0 Demo</title>
      <script src="https://cdn.auth0.com/js/auth0-spa-js/2.0/auth0-spa-js.production.js"></script>
      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
      <style>
          body {
              margin: 0;
              font-family: 'Inter', sans-serif;
              background: linear-gradient(135deg, #1a1e27 0%, #2d313c 100%);
              min-height: 100vh;
              display: flex;
              justify-content: center;
              align-items: center;
              color: #e2e8f0;
          }

          .container {
              background-color: #262a33;
              border-radius: 20px;
              box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05);
              padding: 3rem;
              max-width: 600px;
              width: 90%;
              text-align: center;
          }

          .logo {
              width: 160px;
              margin-bottom: 2rem;
          }

          h1 {
              font-size: 2.8rem;
              font-weight: 700;
              color: #f7fafc;
              margin-bottom: 1rem;
              text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
          }

          .subtitle {
              font-size: 1.2rem;
              color: #a0aec0;
              margin-bottom: 2rem;
              line-height: 1.6;
          }

          .button {
              padding: 1.1rem 2.8rem;
              font-size: 1.2rem;
              font-weight: 600;
              border-radius: 10px;
              border: none;
              cursor: pointer;
              transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
              box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4);
              text-transform: uppercase;
              letter-spacing: 0.08em;
              margin: 0.5rem;
          }

          .button.login {
              background-color: #63b3ed;
              color: #1a1e27;
          }

          .button.login:hover {
              background-color: #4299e1;
              transform: translateY(-3px) scale(1.02);
          }

          .button.logout {
              background-color: #fc8181;
              color: #1a1e27;
          }

          .button.logout:hover {
              background-color: #e53e3e;
              transform: translateY(-3px) scale(1.02);
          }

          .button.rpc {
              background-color: #68d391;
              color: #1a1e27;
          }

          .button.rpc:hover {
              background-color: #48bb78;
              transform: translateY(-3px) scale(1.02);
          }

          .profile-card {
              background-color: #2d313c;
              border-radius: 15px;
              padding: 2rem;
              margin: 2rem 0;
              text-align: left;
          }

          .profile-picture {
              width: 80px;
              height: 80px;
              border-radius: 50%;
              margin-bottom: 1rem;
              border: 3px solid #63b3ed;
          }

          .status {
              margin: 1rem 0;
              padding: 1rem;
              border-radius: 10px;
              font-weight: 500;
          }

          .status.success {
              background-color: #2d7d32;
              color: #e8f5e8;
          }

          .status.error {
              background-color: #c62828;
              color: #ffebee;
          }

          .status.info {
              background-color: #1976d2;
              color: #e3f2fd;
          }

          .loading {
              display: inline-block;
              width: 20px;
              height: 20px;
              border: 3px solid #f3f3f3;
              border-top: 3px solid #63b3ed;
              border-radius: 50%;
              animation: spin 1s linear infinite;
          }

          @keyframes spin {
              0% { transform: rotate(0deg); }
              100% { transform: rotate(360deg); }
          }

          .hidden {
              display: none;
          }

          pre {
              background-color: #1a1e27;
              padding: 1rem;
              border-radius: 8px;
              overflow-x: auto;
              text-align: left;
              font-size: 0.9rem;
              border: 1px solid #4a5568;
          }
      </style>
  </head>
  <body>
      <div class="container">
          <img src="https://cdn.auth0.com/quantum-assets/dist/latest/logos/auth0/auth0-lockup-en-ondark.png" 
               alt="Auth0 Logo" class="logo" 
               onerror="this.style.display='none'">
          
          <h1>Cap'n Web + Auth0</h1>
          <p class="subtitle">Secure WebSocket RPC with Authentication</p>
          
          <div id="auth-section">
              <button id="login-btn" class="button login">🔐 Login</button>
              <button id="logout-btn" class="button logout hidden">🚪 Logout</button>
          </div>

          <div id="profile-section" class="hidden">
              <div class="profile-card">
                  <div id="profile-info"></div>
              </div>
          </div>

          <div id="rpc-section" class="hidden">
              <h3>🔌 RPC Operations</h3>
              <button id="get-profile-btn" class="button rpc">📋 Get Profile</button>
              <button id="update-profile-btn" class="button rpc">✏️ Update Profile</button>
              <button id="get-public-btn" class="button rpc">🌐 Get Public Data</button>
          </div>

          <div id="status"></div>
          <div id="rpc-results"></div>
      </div>

      <script src="app.js"></script>
  </body>
  </html>

4.2: Create the JavaScript client application (client/app.js)

  class CapnWebAuth0Client {
    constructor() {
      this.auth0Client = null;
      this.ws = null;
      this.requestId = 0;
      this.pendingRequests = new Map();
      this.isAuthenticated = false;
      this.user = null;
      this.accessToken = null;
    }

    async init() {
      try {
        // Initialize Auth0 client
        this.auth0Client = await auth0.createAuth0Client({
          domain: 'REPLACE_WITH_AUTH0_DOMAIN',
          clientId: 'REPLACE_WITH_AUTH0_CLIENT_ID',
          authorizationParams: {
            redirect_uri: window.location.origin,
            audience: 'REPLACE_WITH_AUTH0_AUDIENCE'
          }
        });

        // Handle redirect callback
        if (window.location.search.includes('code=') && window.location.search.includes('state=')) {
          await this.auth0Client.handleRedirectCallback();
          window.history.replaceState({}, document.title, window.location.pathname);
        }

        // Check authentication status
        this.isAuthenticated = await this.auth0Client.isAuthenticated();
        
        if (this.isAuthenticated) {
          this.user = await this.auth0Client.getUser();
          this.accessToken = await this.auth0Client.getTokenSilently();
          this.showLoggedInState();
          this.connectWebSocket();
        } else {
          this.showLoggedOutState();
        }

        this.setupEventListeners();
        this.showStatus('✅ Application initialized successfully', 'success');
      } catch (error) {
        console.error('❌ Initialization failed:', error);
        this.showStatus(`❌ Initialization failed: ${error.message}`, 'error');
      }
    }

    setupEventListeners() {
      document.getElementById('login-btn').addEventListener('click', () => this.login());
      document.getElementById('logout-btn').addEventListener('click', () => this.logout());
      document.getElementById('get-profile-btn').addEventListener('click', () => this.getProfile());
      document.getElementById('update-profile-btn').addEventListener('click', () => this.updateProfile());
      document.getElementById('get-public-btn').addEventListener('click', () => this.getPublicData());
    }

    async login() {
      try {
        this.showStatus('🔄 Redirecting to Auth0...', 'info');
        await this.auth0Client.loginWithRedirect();
      } catch (error) {
        console.error('❌ Login failed:', error);
        this.showStatus(`❌ Login failed: ${error.message}`, 'error');
      }
    }

    async logout() {
      try {
        this.closeWebSocket();
        await this.auth0Client.logout({
          logoutParams: {
            returnTo: window.location.origin
          }
        });
      } catch (error) {
        console.error('❌ Logout failed:', error);
        this.showStatus(`❌ Logout failed: ${error.message}`, 'error');
      }
    }

    connectWebSocket() {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        return; // Already connected
      }

      const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
      const wsUrl = `${protocol}//${window.location.host}`;
      
      this.ws = new WebSocket(wsUrl);

      this.ws.onopen = () => {
        console.log('🔌 WebSocket connected');
        this.showStatus('🔌 Connected to Cap\'n Web server', 'success');
      };

      this.ws.onmessage = (event) => {
        try {
          const response = JSON.parse(event.data);
          const pendingRequest = this.pendingRequests.get(response.id);
          
          if (pendingRequest) {
            this.pendingRequests.delete(response.id);
            if (response.error) {
              pendingRequest.reject(new Error(response.error));
            } else {
              pendingRequest.resolve(response.result);
            }
          }
        } catch (error) {
          console.error('❌ Failed to parse WebSocket message:', error);
        }
      };

      this.ws.onerror = (error) => {
        console.error('❌ WebSocket error:', error);
        this.showStatus('❌ WebSocket connection error', 'error');
      };

      this.ws.onclose = () => {
        console.log('🔌 WebSocket disconnected');
        this.showStatus('🔌 Disconnected from server', 'info');
        
        // Retry connection after 3 seconds if authenticated
        if (this.isAuthenticated) {
          setTimeout(() => this.connectWebSocket(), 3000);
        }
      };
    }

    closeWebSocket() {
      if (this.ws) {
        this.ws.close();
        this.ws = null;
      }
    }

    async callRPC(method, params = null, requiresAuth = true) {
      return new Promise((resolve, reject) => {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
          return reject(new Error('WebSocket not connected'));
        }

        const id = ++this.requestId;
        const request = {
          id,
          method,
          params
        };

        if (requiresAuth && this.accessToken) {
          request.token = this.accessToken;
        }

        this.pendingRequests.set(id, { resolve, reject });
        
        // Set timeout for request
        setTimeout(() => {
          if (this.pendingRequests.has(id)) {
            this.pendingRequests.delete(id);
            reject(new Error('RPC request timeout'));
          }
        }, 10000);

        this.ws.send(JSON.stringify(request));
      });
    }

    async getProfile() {
      try {
        this.showStatus('🔄 Fetching profile...', 'info');
        const profile = await this.callRPC('getProfile');
        this.showRPCResult('Profile Data', profile);
      } catch (error) {
        console.error('❌ Get profile failed:', error);
        this.showStatus(`❌ Failed to get profile: ${error.message}`, 'error');
      }
    }

    async updateProfile() {
      try {
        this.showStatus('🔄 Updating profile...', 'info');
        const updates = {
          preferences: {
            theme: 'dark',
            notifications: true,
            lastAction: 'profile-update'
          }
        };
        
        const updatedProfile = await this.callRPC('updateProfile', updates);
        this.showRPCResult('Updated Profile', updatedProfile);
      } catch (error) {
        console.error('❌ Update profile failed:', error);
        this.showStatus(`❌ Failed to update profile: ${error.message}`, 'error');
      }
    }

    async getPublicData() {
      try {
        this.showStatus('🔄 Fetching public data...', 'info');
        const data = await this.callRPC('getPublicData', null, false);
        this.showRPCResult('Public Data', data);
      } catch (error) {
        console.error('❌ Get public data failed:', error);
        this.showStatus(`❌ Failed to get public data: ${error.message}`, 'error');
      }
    }

    showLoggedInState() {
      document.getElementById('login-btn').classList.add('hidden');
      document.getElementById('logout-btn').classList.remove('hidden');
      document.getElementById('profile-section').classList.remove('hidden');
      document.getElementById('rpc-section').classList.remove('hidden');

      if (this.user) {
        const profileHtml = `
          ${this.user.picture ? `<img src="${this.user.picture}" alt="Profile" class="profile-picture">` : ''}
          <h3>${this.user.name || 'User'}</h3>
          <p><strong>Email:</strong> ${this.user.email || 'Not provided'}</p>
          <p><strong>User ID:</strong> ${this.user.sub}</p>
        `;
        document.getElementById('profile-info').innerHTML = profileHtml;
      }
    }

    showLoggedOutState() {
      document.getElementById('login-btn').classList.remove('hidden');
      document.getElementById('logout-btn').classList.add('hidden');
      document.getElementById('profile-section').classList.add('hidden');
      document.getElementById('rpc-section').classList.add('hidden');
    }

    showStatus(message, type = 'info') {
      const statusDiv = document.getElementById('status');
      statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
      
      // Auto-hide success and info messages after 5 seconds
      if (type === 'success' || type === 'info') {
        setTimeout(() => {
          statusDiv.innerHTML = '';
        }, 5000);
      }
    }

    showRPCResult(title, data) {
      const resultsDiv = document.getElementById('rpc-results');
      const resultHtml = `
        <div class="status success">
          <h4>${title}</h4>
          <pre>${JSON.stringify(data, null, 2)}</pre>
        </div>
      `;
      resultsDiv.innerHTML = resultHtml;
      this.showStatus('✅ RPC call completed successfully', 'success');
    }
  }

  // Initialize the application
  const app = new CapnWebAuth0Client();
  app.init().catch(error => {
    console.error('❌ Failed to initialize application:', error);
    document.getElementById('status').innerHTML = 
      `<div class="status error">❌ Failed to initialize: ${error.message}</div>`;
  });

Step 5: Update Environment Configuration
5.1: Update the client configuration in app.js with actual Auth0 values:

⚠️ CRITICAL: Replace the placeholder values in client/app.js with your actual Auth0 configuration:
- Replace 'REPLACE_WITH_AUTH0_DOMAIN' with your Auth0 domain
- Replace 'REPLACE_WITH_AUTH0_CLIENT_ID' with your Auth0 client ID  
- Replace 'REPLACE_WITH_AUTH0_AUDIENCE' with your Auth0 API audience

Step 6: Test and Run the Application
6.1: Start the development server:

  npm run dev

6.2: Open http://localhost:3000 in your browser

6.3: Test the complete authentication flow:
  - Click "Login" to authenticate with Auth0
  - View your profile information
  - Test RPC calls (Get Profile, Update Profile, Get Public Data)
  - Verify WebSocket connection status
  - Test logout functionality

SECURITY REQUIREMENTS & BEST PRACTICES
- ✅ NEVER accept unauthenticated RPC calls for protected methods
- ✅ ALWAYS validate JWT signatures using JWKS from Auth0
- ✅ Implement comprehensive error handling for expired/invalid tokens
- ✅ Use environment variables for all sensitive configuration
- ✅ Validate all user inputs before processing
- ✅ Log security events and authentication attempts
- ✅ Use secure WebSocket connections (WSS) in production
- ✅ Implement proper CORS policies
- ✅ Add request rate limiting for production use
- ✅ Sanitize all data before storage or transmission

TROUBLESHOOTING TIPS
- Check browser console for JavaScript errors
- Verify .env file contains correct Auth0 configuration
- Ensure Auth0 application settings match your local URLs
- Confirm API scopes are properly configured in Auth0 dashboard
- Test WebSocket connectivity separately if RPC calls fail
- Validate JWT tokens using jwt.io for debugging

Get Started

This quickstart demonstrates how to add Auth0 authentication to a Cap’n Web application. You’ll build a modern RPC-based web application with secure login functionality using Cap’n Web’s JavaScript framework and the Auth0 SPA SDK.
1

Create a new project

Create a new Cap’n Web project for this Quickstart
mkdir capnweb-auth0-app && cd capnweb-auth0-app
Initialize the project and install dependencies
npm init -y && npm pkg set type="module" && npm install capnweb ws @auth0/auth0-api-js dotenv
Create the basic project structure
mkdir -p client server && touch server/index.js client/index.html client/app.js .env.example .env
Set up your package.json scripts for development:
npm pkg set scripts.dev="node server/index.js" && npm pkg set scripts.start="node server/index.js"
2

Install the Auth0 SPA JS SDK

Install the Auth0 SPA JS SDK to handle authentication:
npm install @auth0/auth0-spa-js
3

Setup your Auth0 App

Next up, you need to create a new app on your Auth0 tenant and add the environment variables to your project.You can choose to do this automatically by running a CLI command or do it manually via the Dashboard:
  • CLI
  • Dashboard
Run the following shell command on your project’s root directory to create an Auth0 app and generate a .env file:
AUTH0_APP_NAME="My Cap'n Web App" && AUTH0_API_NAME="Cap'n Web API" && AUTH0_API_IDENTIFIER="https://capnweb-api.$(date +%s).com" && brew tap auth0/auth0-cli && brew install auth0 && auth0 login --no-input && auth0 apis create --name "${AUTH0_API_NAME}" --identifier "${AUTH0_API_IDENTIFIER}" --scopes "read:profile,write:profile" --json > auth0-api-details.json && auth0 apps create -n "${AUTH0_APP_NAME}" -t spa -c http://localhost:3000 -l http://localhost:3000 -o http://localhost:3000 --json > auth0-app-details.json && CLIENT_ID=$(jq -r '.client_id' auth0-app-details.json) && DOMAIN=$(auth0 tenants list --json | jq -r '.[] | select(.active == true) | .name') && echo "AUTH0_DOMAIN=${DOMAIN}" > .env && echo "AUTH0_CLIENT_ID=${CLIENT_ID}" >> .env && echo "AUTH0_AUDIENCE=${AUTH0_API_IDENTIFIER}" >> .env && echo "PORT=3000" >> .env && echo "NODE_ENV=development" >> .env && rm auth0-app-details.json auth0-api-details.json && echo ".env file created with your Auth0 details:" && cat .env
4

Create the server

Create the Cap’n Web server with Auth0 integration:
import { RpcTarget } from 'capnweb';
import { WebSocketServer } from 'ws';
import { ApiClient } from '@auth0/auth0-api-js';
import http from 'http';
import { readFileSync } from 'fs';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';

dotenv.config();

const __dirname = dirname(fileURLToPath(import.meta.url));
const userProfiles = new Map();

// Auth0 configuration
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;
const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;
const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;

if (!AUTH0_DOMAIN || !AUTH0_CLIENT_ID || !AUTH0_AUDIENCE) {
  console.error('❌ Missing required Auth0 environment variables');
  if (!AUTH0_DOMAIN) console.error('   - AUTH0_DOMAIN is required');
  if (!AUTH0_CLIENT_ID) console.error('   - AUTH0_CLIENT_ID is required');
  if (!AUTH0_AUDIENCE) console.error('   - AUTH0_AUDIENCE is required');
  process.exit(1);
}

// Initialize Auth0 API client for token verification
// Using @auth0/auth0-api-js provides better Auth0 integration than jsonwebtoken:
// - Automatic JWKS handling and caching
// - Built-in JWT/JWE token support
// - Proper OAuth 2.0 compliance
// - Auth0-specific optimizations
const auth0ApiClient = new ApiClient({
  domain: AUTH0_DOMAIN,
  audience: AUTH0_AUDIENCE
});

async function verifyToken(token) {
  try {
    const payload = await auth0ApiClient.verifyAccessToken({
      accessToken: token
    });
    return payload;
  } catch (error) {
    throw new Error(`Token verification failed: ${error.message}`);
  }
}

// Profile Service - Cap'n Web RPC Target
class ProfileService extends RpcTarget {
  async getProfile(accessToken) {
    try {
      const decoded = await verifyToken(accessToken);
      const userId = decoded.sub;
      const profile = userProfiles.get(userId) || { bio: '' };
      return {
        id: userId,
        email: decoded.email || 'Unknown User',
        bio: profile.bio
      };
    } catch (error) {
      throw new Error('Invalid access token');
    }
  }

  async updateProfile(accessToken, bio) {
    try {
      const decoded = await verifyToken(accessToken);
      const userId = decoded.sub;
      userProfiles.set(userId, { bio });
      return { success: true, message: 'Profile updated successfully' };
    } catch (error) {
      throw new Error('Invalid access token');
    }
  }
}

// Create HTTP server
const server = http.createServer(async (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
  
  if (req.method === 'OPTIONS') {
    res.writeHead(200);
    res.end();
    return;
  }

  if (req.url === '/api/config') {
    const config = { 
      auth0: { 
        domain: AUTH0_DOMAIN, 
        clientId: AUTH0_CLIENT_ID,
        audience: AUTH0_AUDIENCE
      } 
    };
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    res.end(JSON.stringify(config));
    return;
  }

  // Handle root path and Auth0 callback (with query parameters)
  if (req.url === '/' || req.url === '/index.html' || req.url.startsWith('/?code=') || req.url.startsWith('/?error=')) {
    const html = readFileSync(join(__dirname, '../client/index.html'), 'utf8');
    res.setHeader('Content-Type', 'text/html');
    res.writeHead(200);
    res.end(html);
    return;
  }

  if (req.url === '/app.js') {
    const js = readFileSync(join(__dirname, '../client/app.js'), 'utf8');
    res.setHeader('Content-Type', 'application/javascript');
    res.writeHead(200);
    res.end(js);
    return;
  }

  res.writeHead(404);
  res.end('Not found');
});

// WebSocket server for Cap'n Web RPC
const wss = new WebSocketServer({ server });

wss.on('connection', (ws) => {
  console.log('🔗 New WebSocket connection');
  const profileService = new ProfileService();
  
  ws.on('message', async (data) => {
    try {
      const message = JSON.parse(data.toString());
      if (message.type === 'call') {
        const result = await profileService[message.method](...(message.params || []));
        ws.send(JSON.stringify({
          type: 'result',
          id: message.id,
          result
        }));
      }
    } catch (error) {
      ws.send(JSON.stringify({
        type: 'error',
        id: message.id,
        error: error.message
      }));
    }
  });

  ws.on('close', () => console.log('❌ WebSocket connection closed'));
});

// Start server
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
  console.log(`🚀 Server running on http://localhost:${PORT}`);
});
5

Create the client interface

Create the frontend HTML and JavaScript files:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cap'n Web + Auth0 Demo</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 600px;
            margin: 0 auto;
            padding: 2rem;
            line-height: 1.6;
        }
        .auth-section, .profile-section {
            background: #f8f9fa;
            border-radius: 8px;
            padding: 2rem;
            margin: 1rem 0;
        }
        .profile-section { display: none; }
        button {
            background: #007bff;
            color: white;
            border: none;
            padding: 0.75rem 1.5rem;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1rem;
            margin: 0.5rem 0.5rem 0.5rem 0;
        }
        button:hover { background: #0056b3; }
        button.secondary { background: #6c757d; }
        button.secondary:hover { background: #545b62; }
        textarea {
            width: 100%;
            min-height: 100px;
            margin: 1rem 0;
            padding: 0.75rem;
            border: 1px solid #ddd;
            border-radius: 4px;
            resize: vertical;
        }
        .status {
            padding: 1rem;
            margin: 1rem 0;
            border-radius: 4px;
            font-weight: 500;
        }
        .status.success { background: #d4edda; color: #155724; }
        .status.error { background: #f8d7da; color: #721c24; }
        .status.info { background: #d1ecf1; color: #0c5460; }
    </style>
</head>
<body>
    <h1>Cap'n Web + Auth0 Profile Demo</h1>
    
    <div id="status" class="status info">Initializing...</div>
    
    <!-- Login Section -->
    <div id="authSection" class="auth-section">
        <h2>🔐 Welcome!</h2>
        <p>This demo shows Auth0 authentication with Cap'n Web RPC for real-time profile management.</p>
        <button id="loginBtn">Login with Auth0</button>
    </div>
    
    <!-- Profile Section -->
    <div id="profileSection" class="profile-section">
        <h2>👤 Profile Management</h2>
        <p><strong>Email:</strong> <span id="userEmail">Loading...</span></p>
        
        <label for="bioTextarea"><strong>Bio:</strong></label>
        <textarea id="bioTextarea" placeholder="Tell us about yourself..."></textarea>
        
        <div>
            <button id="fetchBtn">🔄 Fetch Profile</button>
            <button id="saveBtn">💾 Save Profile</button>
            <button id="logoutBtn" class="secondary">🚪 Logout</button>
        </div>
    </div>

    <script src="https://cdn.auth0.com/js/auth0-spa-js/2.1/auth0-spa-js.production.js"></script>
    <script src="/app.js"></script>
</body>
</html>
6

Run your app

npm run dev
CheckpointYou should now have a fully functional Auth0 login page running on your localhost

Advanced Usage

Enhance security by adding additional validation and rate limiting to your RPC methods:
server/profile-service.js
import rateLimit from 'express-rate-limit';

class ProfileService extends RpcTarget {
  constructor() {
    super();
    this.rateLimiter = new Map(); // Simple in-memory rate limiting
  }

  async validateAndRateLimit(userId) {
    const now = Date.now();
    const userLimit = this.rateLimiter.get(userId) || { count: 0, resetTime: now + 60000 };
    
    if (now > userLimit.resetTime) {
      userLimit.count = 0;
      userLimit.resetTime = now + 60000;
    }
    
    if (userLimit.count >= 10) {
      throw new Error('Rate limit exceeded. Try again later.');
    }
    
    userLimit.count++;
    this.rateLimiter.set(userId, userLimit);
  }

  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    await this.validateAndRateLimit(decoded.sub);
    
    const userId = decoded.sub;
    const profile = userProfiles.get(userId) || { bio: '' };
    
    return {
      id: userId,
      email: decoded.email || 'Unknown User',
      bio: profile.bio,
      lastUpdated: profile.lastUpdated || null
    };
  }
}
Implement proper WebSocket connection handling with reconnection logic:
client/enhanced-rpc-client.js
class EnhancedRpcClient extends SimpleRpcClient {
  constructor(wsUrl, options = {}) {
    super(wsUrl);
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectDelay = options.reconnectDelay || 1000;
    this.isConnecting = false;
  }

  async connect() {
    if (this.isConnecting) return;
    this.isConnecting = true;

    try {
      await super.connect();
      this.reconnectAttempts = 0;
      this.isConnecting = false;
    } catch (error) {
      this.isConnecting = false;
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        this.reconnectAttempts++;
        console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
        
        setTimeout(() => {
          this.connect();
        }, this.reconnectDelay * this.reconnectAttempts);
      } else {
        throw new Error('Max reconnection attempts reached');
      }
    }
  }

  async call(method, ...params) {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      await this.connect();
    }
    return super.call(method, ...params);
  }
}
Replace in-memory storage with a database for production use:
server/database.js
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL || 'postgresql://localhost/capnweb_auth0'
});

// Initialize database schema
async function initializeDatabase() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS user_profiles (
      user_id VARCHAR(255) PRIMARY KEY,
      bio TEXT,
      created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
      updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
  `);
}

class DatabaseProfileService extends RpcTarget {
  async getProfile(accessToken) {
    const decoded = await verifyToken(accessToken);
    const result = await pool.query(
      'SELECT bio, updated_at FROM user_profiles WHERE user_id = $1',
      [decoded.sub]
    );
    
    return {
      id: decoded.sub,
      email: decoded.email,
      bio: result.rows[0]?.bio || '',
      lastUpdated: result.rows[0]?.updated_at || null
    };
  }
  
  async updateProfile(accessToken, bio) {
    const decoded = await verifyToken(accessToken);
    await pool.query(`
      INSERT INTO user_profiles (user_id, bio, updated_at) 
      VALUES ($1, $2, CURRENT_TIMESTAMP)
      ON CONFLICT (user_id) 
      DO UPDATE SET bio = $2, updated_at = CURRENT_TIMESTAMP
    `, [decoded.sub, bio]);
    
    return { success: true, message: 'Profile updated successfully' };
  }
}

export { initializeDatabase, DatabaseProfileService };