JWT Signing Key Flow & Security
This document describes the JWT signing key authentication mechanism used in Wallcrawler for securing Chrome DevTools Protocol (CDP) access, with important security considerations for SDK usage.
Overview
The JWT signing key is a time-limited bearer token that authenticates access to Chrome browser instances running in ECS containers. It ensures that only authorized clients can connect to the Chrome DevTools Protocol interface.
⚠️ Security Warning
The Wallcrawler SDK is designed for server-side use only. Never use the SDK directly in browser/frontend code as it will expose sensitive authentication tokens to end users.
Architecture
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Client │ ──JWT──▶│ CDP Proxy │ ──────▶ │ Chrome │
│ (Server) │ │ (Port 9223) │ │ (Port 9222) │
└─────────────┘ └──────────────┘ └─────────────────┘
▲
│ Validates JWT
│
┌──────────────┐
│ AWS Secrets │
│ Manager │
└──────────────┘
JWT Token Structure
The JWT token contains the following claims:
{
// Standard JWT Claims
"iss": "wallcrawler", // Issuer
"sub": "sess_[uuid]", // Subject (Session ID)
"aud": ["cdp-access"], // Audience
"exp": 1234567890, // Expiration time (Unix timestamp)
"iat": 1234567890, // Issued at (Unix timestamp)
"nbf": 1234567890, // Not before (Unix timestamp)
"jti": "nonce_abc123", // JWT ID (nonce)
// Custom Claims
"sessionId": "sess_[uuid]", // Session identifier
"projectId": "proj_[uuid]", // Project identifier
"userId": "user_[uuid]", // Optional user identifier
"nonce": "abc123...", // Cryptographic nonce
"ipAddress": "1.2.3.4" // Optional client IP
}
Flow Sequence
1. Session Creation
sequenceDiagram
participant Client
participant Lambda as sessions-create Lambda
participant Secrets as AWS Secrets Manager
participant Sessions as DynamoDB (wallcrawler-sessions)
participant ECS as ECS Fargate
Client->>Lambda: POST /v1/sessions
Lambda->>Secrets: Get JWT signing key
Secrets-->>Lambda: Return secret key
Lambda->>Lambda: Generate JWT token
Lambda->>Sessions: Store session (CREATING) with signingKey
Lambda->>ECS: Launch browser task
Note over Lambda,ECS: Lambda waits for the SNS "ready" notification before responding
Lambda-->>Client: Return signingKey & connectUrl
- Client Request: Client sends POST to
/v1/sessionswith API key and project ID - Secret Retrieval: Lambda fetches the JWT signing secret from AWS Secrets Manager
- Token Generation: Lambda creates a JWT token with:
- Session ID (format:
sess_[uuid]) - Project ID
- Expiration time (defaults to the session timeout — 1 hour unless overridden)
- Cryptographically secure random nonce
- Session ID (format:
- Storage: The session record (including the JWT) is written to DynamoDB (
wallcrawler-sessions). - Provisioning: The Lambda launches the ECS task and waits for the
sessions-stream-processorSNS notification that signals the browser is ready. - Response: The client receives:
signingKey: The JWT token (expires with the session)connectUrl: Pre-constructed WebSocket URL with the signing key
2. Connection Authentication
sequenceDiagram
participant Client
participant Proxy as CDP Proxy
participant Secrets as AWS Secrets Manager
participant Chrome
Client->>Proxy: WS Connect with ?signingKey=JWT
Proxy->>Secrets: Get JWT signing key
Secrets-->>Proxy: Return secret key
Proxy->>Proxy: Validate JWT signature
Proxy->>Proxy: Check expiration & claims
alt Valid Token
Proxy->>Chrome: Forward WebSocket
Chrome-->>Proxy: CDP Connection
Proxy-->>Client: Proxied CDP Connection
else Invalid Token
Proxy-->>Client: 401 Unauthorized
end
- Connection Request: Client connects to
ws://<ip>:9223?signingKey=<jwt> - Token Extraction: CDP proxy extracts signing key from query parameters
- Validation: Proxy validates the JWT by:
- Verifying signature using shared secret from Secrets Manager
- Checking expiration time hasn't passed
- Validating required claims (sessionId, projectId)
- Confirming token hasn't been used before expiration
- Connection: If valid, proxy establishes WebSocket connection to Chrome on localhost:9222
- Rejection: If invalid, returns 401 Unauthorized
Security Considerations
Bearer Token Risk
The JWT token is a bearer token - anyone who possesses it can use it to control the browser instance. This means:
- No Additional Authentication Required: The token itself is the complete authentication
- Full Browser Control: Token holders can:
- Navigate to any website
- Execute JavaScript
- Extract cookies and local storage
- Take screenshots
- Download files
- Access any data the browser can access
Attack Scenarios
If Tokens Are Exposed in Frontend Code:
// ❌ WRONG: Browser code exposes tokens
const session = await wallcrawler.sessions.create({ projectId });
// Token is now visible in:
// - Browser DevTools Network tab
// - JavaScript console
// - Client-side code bundles
Attackers Could:
- Resource Theft: Use your browser instances for their own automation
- Data Extraction: Access any data your application loads
- Billing Fraud: Run expensive operations on your account
- Network Access: If browsers are in your VPC, access internal resources
Secure Implementation Patterns
✅ Correct: Server-Side Usage with Next.js
// pages/api/browser/scrape.js (or app/api/browser/scrape/route.js)
import { Wallcrawler } from '@wallcrawler/sdk-node';
import { Stagehand } from '@browserbase/stagehand';
const wallcrawler = new Wallcrawler({
apiKey: process.env.WALLCRAWLER_API_KEY,
});
export async function POST(request) {
try {
// Create session - credentials stay on server
const session = await wallcrawler.sessions.create({
projectId: process.env.WALLCRAWLER_PROJECT_ID,
});
// Use browser - all server-side
const stagehand = new Stagehand({
env: 'BROWSERBASE',
sessionConnectUrl: session.connectUrl,
});
const { url, selector } = await request.json();
await stagehand.page.goto(url);
const data = await stagehand.page.extract(selector);
// Return ONLY results, never credentials
return Response.json({
success: true,
data,
// Never include: connectUrl, signingKey, seleniumRemoteUrl
});
} catch (error) {
console.error('Server error:', error); // Log details server-side
return Response.json(
{ success: false, error: 'Operation failed' }, // Generic error for client
{ status: 500 }
);
}
}
// Frontend component - no SDK usage
export default function ScraperComponent() {
const handleScrape = async () => {
// Call your API route, not Wallcrawler directly
const response = await fetch('/api/browser/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: 'https://example.com',
selector: '.price',
}),
});
const result = await response.json();
// Only receives scraped data, no tokens
};
}
❌ Wrong: Frontend SDK Usage
// NEVER DO THIS in browser code
'use client';
import { Wallcrawler } from '@wallcrawler/sdk-node';
export default function BadComponent() {
const handleAction = async () => {
// This exposes all tokens to the browser!
const wallcrawler = new Wallcrawler({
apiKey: 'key_...', // Visible in browser
});
const session = await wallcrawler.sessions.create({
projectId: 'proj_...', // Also visible
});
// connectUrl and signingKey now exposed to end users
};
}
Security Features
1. Time-Limited Access
- Tokens expire after a configurable duration (default matches the session timeout, 1 hour)
- Expiration is enforced at validation time
- No token refresh mechanism - clients must create new sessions
2. Cryptographic Security
- HMAC-SHA256 signing algorithm
- Shared secret stored in AWS Secrets Manager
- Secret key caching with 5-minute TTL to reduce API calls
- Cryptographically secure random nonce in each token
3. Network Isolation
- Chrome CDP port (9222) bound to localhost only
- All external access through authenticated proxy (9223)
- No direct access to Chrome without valid token
4. Secret Management
- JWT signing key stored in AWS Secrets Manager
- Automatic secret rotation supported
- Environment variable override for development (
WALLCRAWLER_JWT_SIGNING_KEY)
Best Practices
1. Architecture
- Always use server-side proxies (e.g., Next.js API routes)
- Never expose SDK responses to frontend code
- Implement proper error handling without leaking details
2. Token Handling
- Never log JWT tokens in error messages
- Use HTTPS when transmitting tokens
- Implement request signing for additional security
3. Session Management
- Create new sessions rather than trying to refresh tokens
- Clean up sessions when done to avoid resource waste
- Monitor token expiration and handle gracefully
4. Security Monitoring
- Implement rate limiting on session creation
- Monitor for unusual CDP activity patterns
- Set up alerts for suspicious usage
5. Network Security
- Ensure browser containers have minimal network access
- Use VPC security groups to restrict access
- Consider IP allowlisting for additional security
API Response Formats
Session Create Response
{
"id": "sess_abc123",
"connectUrl": "ws://1.2.3.4:9223?signingKey=eyJhbGc...",
"seleniumRemoteUrl": "http://1.2.3.4:4444/wd/hub",
"signingKey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"status": "RUNNING",
// ... other fields
}
What to Return to Frontend Clients
{
"success": true,
"data": {
// Only your automation results
"scrapedText": "...",
"screenshot": "base64...",
},
// Never include: connectUrl, signingKey, seleniumRemoteUrl
}
Summary
The JWT signing key provides secure, time-limited access to browser instances. However, because it's a bearer token, it must never be exposed to frontend code. Always use server-side proxies (like Next.js API routes) to keep credentials secure while providing browser automation capabilities to your users.
