This guide demonstrates how to integrate Auth0, add authentication, and display user profile information in a Single-Page Application (SPA) that uses Cap’n Web RPC, using the Auth0 SPA SDK.
This Quickstart is currently in Beta. We’d love to hear your feedback!
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 applicationAI PERSONA & PRIMARY OBJECTIVEYou 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 INSTRUCTIONS1. 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 FLOWStep 1: Create Cap'n Web Projectmkdir capnweb-auth0-app && cd capnweb-auth0-appnpm init -y && npm pkg set type="module"mkdir -p client server && touch server/index.js client/index.html client/app.js .envStep 2: Install Dependenciesnpm install capnweb ws dotenvnpm install @auth0/auth0-spa-js @auth0/auth0-api-jsnpm pkg set scripts.start="node server/index.js"Step 3: Setup Auth0 App (use CLI command from Step 3 in the quickstart)Step 4: Configure Auth0 Application and API- Create Auth0 Application (SPA type)- Create Auth0 API with required scopes- Set callback URLs and originsStep 5: Implement Server with JWT Validation- Create WebSocket server with Cap'n Web RPC- Extend RpcTarget class for ProfileService- Validate JWT tokens from Auth0 for each RPC call- Use newWebSocketRpcSession() to handle WebSocket connections- Implement secure profile management endpointsStep 6: Implement Client with Auth0 Integration- Initialize Auth0 SPA client with refresh tokens enabled- Use newWebSocketRpcSession() from capnweb for RPC- Connect to WebSocket only after authentication is confirmed- Handle login/logout flows- Send JWT tokens with RPC calls- Build modern UI with authentication stateStep 7: Run the Applicationnpm run startSECURITY REQUIREMENTS- NEVER accept unauthenticated RPC calls- ALWAYS validate JWT signatures using JWKS- Implement proper error handling for expired tokens- Use secure WebSocket connections in productionStep 3: Setup Auth0 Application and APIAFTER the commands in Steps 1 and 2 have successfully executed, you will perform the Auth0 configuration.🚨 DIRECTORY NAVIGATION RULES:1. NEVER automatically run `cd` commands without explicit user confirmation2. ALWAYS check current directory with `pwd` before proceeding3. If working with existing project: Stay in current directory4. If created new project: User must manually navigate to capnweb-auth0-app directory firstStep 3.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-appThen execute the environment setup command for your OS:⚠️ CRITICAL DIRECTORY VERIFICATION STEP:BEFORE executing the Auth0 CLI setup command, you MUST run: pwd && ls -laThis 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 .envIf 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 .envStep 3.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 EOFStep 3.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 4: Implement Secure WebSocket Server with JWT ValidationAFTER Auth0 setup is complete, create the server with comprehensive security:4.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}`); } }4.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 5: Implement Server with JWT ValidationAFTER Auth0 setup is complete, create the server with Cap'n Web RPC:5.1: Create the main server file (server/index.js)Import required modules and set up Auth0 token verification: import { RpcTarget, newWebSocketRpcSession } 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; const PORT = process.env.PORT || 3000; // Initialize Auth0 API client for token verification 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}`); } }5.2: Create ProfileService RpcTarget with authentication: // ProfileService - extends RpcTarget for Cap'n Web RPC class ProfileService extends RpcTarget { async getProfile(accessToken) { 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 }; } async updateProfile(accessToken, bio) { const decoded = await verifyToken(accessToken); const userId = decoded.sub; userProfiles.set(userId, { bio }); return { success: true, message: 'Profile updated successfully' }; } }5.3: Create HTTP server and WebSocket server: // Create HTTP server to serve static files and Auth0 config const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); 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; } // Serve HTML, JS files, and npm modules if (req.url === '/' || req.url === '/index.html') { 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; } // Serve Auth0 SPA SDK from node_modules if (req.url === '/@auth0/auth0-spa-js') { const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js'); const js = readFileSync(modulePath, 'utf8'); res.setHeader('Content-Type', 'application/javascript'); res.writeHead(200); res.end(js); return; } // Serve capnweb from node_modules if (req.url === '/capnweb') { const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js'); const js = readFileSync(modulePath, '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, req) => { // Only handle RPC connections on /api path if (req.url === '/api') { console.log('🔗 New Cap\'n Web RPC connection'); // Create a new ProfileService instance for this connection const profileService = new ProfileService(); // Use capnweb's newWebSocketRpcSession to handle the connection newWebSocketRpcSession(ws, profileService); } }); // Start server 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}`); });Step 6: Create Modern Client with Auth0 Integration6.1: Create the main HTML file (client/index.html) with import map: <!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 type="importmap"> { "imports": { "@auth0/auth0-spa-js": "/@auth0/auth0-spa-js", "capnweb": "/capnweb" } } </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 type="module" src="/app.js"></script> </body> </html>6.2: Create the JavaScript client application (client/app.js)Use capnweb's newWebSocketRpcSession and Auth0 SDK with proper ES module imports: import { createAuth0Client } from '@auth0/auth0-spa-js'; import { newWebSocketRpcSession } from 'capnweb'; // Auth0 Configuration let auth0Client = null; let profileApi = null; let AUTH0_CONFIG = null; // Load Auth0 config from server async function loadConfig() { const response = await fetch('/api/config'); const config = await response.json(); AUTH0_CONFIG = { domain: config.auth0.domain, clientId: config.auth0.clientId, authorizationParams: { redirect_uri: window.location.origin, audience: config.auth0.audience, scope: 'openid profile email' }, useRefreshTokens: true, cacheLocation: 'localstorage' }; return AUTH0_CONFIG; } // Initialize the application async function initializeApp() { try { showStatus('Loading configuration...', 'info'); const config = await loadConfig(); showStatus('Initializing Auth0 client...', 'info'); auth0Client = await createAuth0Client(config); // Handle redirect callback const query = window.location.search; if (query.includes('code=') && query.includes('state=')) { showStatus('Processing login...', 'info'); await auth0Client.handleRedirectCallback(); window.history.replaceState({}, document.title, window.location.pathname); } // Check authentication status const isAuthenticated = await auth0Client.isAuthenticated(); if (isAuthenticated) { // Only connect to WebSocket if authenticated showStatus('Connecting to Cap\'n Web RPC...', 'info'); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; profileApi = newWebSocketRpcSession(`${protocol}//${window.location.host}/api`); await showProfileSection(); } else { showAuthSection(); } setupEventListeners(); } catch (error) { console.error('Initialization error:', error); showStatus(`Failed to initialize: ${error.message}`, 'error'); } } // Authentication functions async function login() { try { showStatus('Redirecting to Auth0...', 'info'); await auth0Client.loginWithRedirect(); } catch (error) { showStatus(`Login failed: ${error.message}`, 'error'); } } async function logout() { try { if (profileApi) { profileApi[Symbol.dispose](); } await auth0Client.logout({ logoutParams: { returnTo: window.location.origin } }); } catch (error) { showStatus(`Logout failed: ${error.message}`, 'error'); } } async function getAccessToken() { try { return await auth0Client.getTokenSilently({ authorizationParams: { audience: AUTH0_CONFIG.authorizationParams.audience } }); } catch (error) { if (error.error === 'consent_required' || error.error === 'interaction_required') { await auth0Client.loginWithRedirect({ authorizationParams: { audience: AUTH0_CONFIG.authorizationParams.audience, scope: 'openid profile email', prompt: 'consent' } }); } throw error; } } // Profile management using Cap'n Web RPC async function fetchProfile() { try { showStatus('Fetching profile...', 'info'); const token = await getAccessToken(); const user = await auth0Client.getUser(); // Call RPC method directly on the profileApi stub const profile = await profileApi.getProfile(token); document.getElementById('userEmail').textContent = user.email || profile.email || 'No email available'; document.getElementById('bioTextarea').value = profile.bio || ''; showStatus('Profile loaded successfully!', 'success'); } catch (error) { showStatus(`Failed to fetch profile: ${error.message}`, 'error'); } } async function saveProfile() { try { showStatus('Saving profile...', 'info'); const token = await getAccessToken(); const bio = document.getElementById('bioTextarea').value; // Call RPC method directly on the profileApi stub const result = await profileApi.updateProfile(token, bio); showStatus(result.message || 'Profile saved successfully!', 'success'); } catch (error) { showStatus(`Failed to save profile: ${error.message}`, 'error'); } } // UI helper functions function showAuthSection() { document.getElementById('authSection').style.display = 'block'; document.getElementById('profileSection').style.display = 'none'; showStatus('Ready to login', 'info'); } async function showProfileSection() { document.getElementById('authSection').style.display = 'none'; document.getElementById('profileSection').style.display = 'block'; await fetchProfile(); } function showStatus(message, type) { const statusEl = document.getElementById('status'); statusEl.textContent = message; statusEl.className = `status ${type}`; } // Event listeners function setupEventListeners() { document.getElementById('loginBtn').addEventListener('click', login); document.getElementById('logoutBtn').addEventListener('click', logout); document.getElementById('fetchBtn').addEventListener('click', fetchProfile); document.getElementById('saveBtn').addEventListener('click', saveProfile); } // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', initializeApp); 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 placeholderImage = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='50' fill='%2363b3ed'/%3E%3Cpath d='M50 45c7.5 0 13.64-6.14 13.64-13.64S57.5 17.72 50 17.72s-13.64 6.14-13.64 13.64S42.5 45 50 45zm0 6.82c-9.09 0-27.28 4.56-27.28 13.64v3.41c0 1.88 1.53 3.41 3.41 3.41h47.74c1.88 0 3.41-1.53 3.41-3.41v-3.41c0-9.08-18.19-13.64-27.28-13.64z' fill='%23fff'/%3E%3C/svg%3E`; const profileHtml = ` <img src="${this.user.picture || placeholderImage}" alt="Profile" class="profile-picture" onerror="this.src='${placeholderImage}'"> <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 => { // Initialize app when DOM is loaded document.addEventListener('DOMContentLoaded', initializeApp);Step 7: Test and Run the Application7.1: Start the development server: npm run start7.2: Open http://localhost:3000 in your browser7.3: Test the complete authentication flow: - Click "Login" to authenticate with Auth0 - View your profile information (email will be displayed from Auth0) - Update your bio and save - Refresh the page - you should remain logged in (thanks to refresh tokens) - Test logout functionality - Test RPC calls (Get Profile, Update Profile, Get Public Data) - Verify WebSocket connection status - Test logout functionalitySECURITY 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 transmissionTROUBLESHOOTING 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
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 and set up the basic structure:
mkdir capnweb-auth0-app && cd capnweb-auth0-app
Initialize the project and configure it for ES modules:
@auth0/auth0-spa-js is used on the client side to handle user authentication, login flows, and token management in the browser.@auth0/auth0-api-js is used on the server side to verify access tokens and validate JWT signatures using Auth0’s JWKS.
Set up the start script in package.json:
npm pkg set scripts.start="node server/index.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:
Before you start, create a .env file on your project’s root directory
Click on Applications > Applications > Create Application
In the popup, enter a name for your app, select Single Page Web Application as the app type and click Create
Switch to the Settings tab on the Application Details page
Replace YOUR_AUTH0_APP_DOMAIN and YOUR_AUTH0_APP_CLIENT_ID on the .env file with the Domain and Client ID values from the dashboard
Finally, on the Settings tab of your Application Details page, configure the following URLs:Allowed Callback URLs:
http://localhost:3000
Allowed Logout URLs:
http://localhost:3000
Allowed Web Origins:
http://localhost:3000
Allowed Callback URLs are a critical security measure to ensure users are safely returned to your application after authentication. Without a matching URL, the login process will fail, and users will be blocked by an Auth0 error page instead of accessing your app.Allowed Logout URLs are essential for providing a seamless user experience upon signing out. Without a matching URL, users will not be redirected back to your application after logout and will instead be left on a generic Auth0 page.Allowed Web Origins is critical for silent authentication. Without it, users will be logged out when they refresh the page or return to your app later.
Required: Create an Auth0 APIYou must create an Auth0 API for token validation to work:
In the Auth0 Dashboard, go to APIs > Create API
Set these values:
Name: Cap'n Web API
Identifier: https://capnweb-api.yourproject.com (use your own unique identifier)
Signing Algorithm: RS256 (default)
Click Create
In the Scopes tab, add these scopes:
read:profile - Read user profile data
write:profile - Update user profile data
Update your .env file with the API identifier as AUTH0_AUDIENCE
Without creating an Auth0 API, you’ll get an “access_denied” error during login. The API identifier must match your AUTH0_AUDIENCE environment variable exactly.
5
Create the server
Create the Cap’n Web server with Auth0 integration:
server/index.js
import { RpcTarget, newWebSocketRpcSession } 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 configurationconst AUTH0_DOMAIN = process.env.AUTH0_DOMAIN;const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID;const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE;const PORT = process.env.PORT || 3000;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 verificationconst 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 Targetclass ProfileService extends RpcTarget { async getProfile(accessToken) { 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 }; } async updateProfile(accessToken, bio) { const decoded = await verifyToken(accessToken); const userId = decoded.sub; userProfiles.set(userId, { bio }); return { success: true, message: 'Profile updated successfully' }; }}// Create HTTP serverconst 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 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; } // Serve Auth0 SPA JS SDK from node_modules if (req.url === '/@auth0/auth0-spa-js') { const modulePath = join(__dirname, '../node_modules/@auth0/auth0-spa-js/dist/auth0-spa-js.production.esm.js'); const js = readFileSync(modulePath, 'utf8'); res.setHeader('Content-Type', 'application/javascript'); res.writeHead(200); res.end(js); return; } // Serve capnweb from node_modules if (req.url === '/capnweb') { const modulePath = join(__dirname, '../node_modules/capnweb/dist/index.js'); const js = readFileSync(modulePath, '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 RPCconst wss = new WebSocketServer({ server });wss.on('connection', (ws, req) => { // Only handle RPC connections on /api path if (req.url === '/api') { console.log('🔗 New Cap\'n Web RPC connection'); // Create a new ProfileService instance for this connection const profileService = new ProfileService(); // Use capnweb's newWebSocketRpcSession to handle the connection newWebSocketRpcSession(ws, profileService); }});// Start serverserver.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}`);});
6
Create the client interface
Create the frontend HTML and JavaScript files:
7
Run your app
npm run start
CheckpointYou should now have a fully functional Auth0 login page running on your localhost