Skip to content

Latest commit

 

History

History
259 lines (204 loc) · 6.86 KB

File metadata and controls

259 lines (204 loc) · 6.86 KB

Browser Pairing

Secure authentication flow for custom WebSocket clients using 8-character pairing codes. Ideal for private web apps and desktop clients that need to verify device identity.

Browser pairing vs. cookie sync. Browser pairing authenticates a client (a browser or desktop app) so it can talk to the gateway. Selected cookie sync copies site login cookies into an agent's server-side browser so the agent can act as a signed-in session. Use pairing to let a device connect; use cookie sync to give an agent access to a site you are logged into. See Browser Automation → Selected Cookie Sync.

Pairing Flow

sequenceDiagram
    participant C as Client (Browser)
    participant G as Gateway
    participant O as Owner (CLI/Dashboard)

    C->>G: Request pairing code
    G->>C: Generate code: ABCD1234<br/>(valid 60 min)
    G->>O: Notify: New pairing request<br/>from client_id

    Note over C: User shows code to owner

    O->>G: Approve code: device.pair.approve<br/>code=ABCD1234
    G->>G: Add to paired_devices<br/>Mark request resolved

    C->>G: Connect with code: ABCD1234
    G->>G: Verify against paired_devices
    G->>C: OK, authenticated!<br/>Issue session token

    C->>G: WebSocket: chat.send<br/>with pairing token
    G->>C: Response + events
Loading

Code Format

Generation:

  • Length: 8 characters
  • Alphabet: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 (excludes ambiguous: 0, O, 1, I, L)
  • TTL: 60 minutes
  • Max pending per account: 3

Example codes:

  • ABCD1234
  • XY8PQRST
  • 2M5H9JKL

Implementation

Step 1: Request Code (Client)

curl -X POST http://localhost:8080/v1/device/pair/request \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "browser_myclient_1",
    "device_name": "My Web App"
  }'

Response:

{
  "code": "ABCD1234",
  "expires_at": 1709865000,
  "url": "http://localhost:8080/pair?code=ABCD1234"
}

Display code to user:

Please share this code with your gateway owner:

  ABCD1234

It expires in 60 minutes.

Step 2: Approve Code (Owner)

Owner runs CLI command or uses dashboard to approve:

goclaw device.pair.approve --code ABCD1234

Or via WebSocket (admin only):

{
  "type": "req",
  "id": "100",
  "method": "device.pair.approve",
  "params": {
    "code": "ABCD1234"
  }
}

Response:

{
  "type": "res",
  "id": "100",
  "ok": true,
  "payload": {
    "client_id": "browser_myclient_1",
    "device_name": "My Web App",
    "paired_at": 1709864400
  }
}

Step 3: Connect (Client)

Client uses the code to authenticate:

{
  "type": "req",
  "id": "1",
  "method": "connect",
  "params": {
    "pairing_code": "ABCD1234",
    "user_id": "web_user_1"
  }
}

Response:

{
  "type": "res",
  "id": "1",
  "ok": true,
  "payload": {
    "protocol": 3,
    "role": "operator",
    "user_id": "web_user_1",
    "session_token": "session_xyz..."
  }
}

Client stores session_token for future connections.

Step 4: Use Session (Client)

On reconnect, use stored token:

{
  "type": "req",
  "id": "1",
  "method": "connect",
  "params": {
    "session_token": "session_xyz...",
    "user_id": "web_user_1"
  }
}

Security Properties

  • One-time use: Each pairing code is used once and invalidated
  • Expiring: Codes expire after 60 minutes (TTL enforced server-side)
  • Limited pending: Max 3 pending requests per account (prevents spam)
  • Owner approval: Only gateway owner can approve codes (admin role required)
  • Session tokens: Issued after approval; tied to device and user
  • Debouncing: Pairing approval notifications debounced per sender (60 seconds)
  • Fail-closed auth: Authentication failures default to deny — no partial or ambiguous approval states
  • Rate limiting: Pairing code requests are rate-limited per sender to prevent brute-force enumeration
  • Transient DB error handling: IsPaired checks handle transient database errors gracefully — a DB error returns denied rather than accidentally allowing access

Channel-Specific Pairing Behaviour

How channels trigger the pairing flow varies by channel type.

Discord groups: When group_policy: "pairing" is active and require_mention: true (the Discord default), a pairing invite is only sent to an unpaired user if they explicitly @mentioned the bot or replied to one of the bot's messages. Messages sent to the group that do not address the bot are silently buffered and do not produce a pairing reply. This prevents pairing spam in public servers where many users can see the bot's messages.

For DMs, the pairing invite fires unconditionally whenever an unpaired user sends a message (same as other channels).

JavaScript Example

class PairingClient {
  constructor(gatewayUrl) {
    this.url = gatewayUrl;
    this.ws = null;
    this.sessionToken = localStorage.getItem('goclaw_token');
  }

  async requestPairingCode() {
    const res = await fetch(`${this.url}/v1/device/pair/request`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: 'browser_' + Date.now(),
        device_name: navigator.userAgent
      })
    });
    const data = await res.json();
    return data.code;
  }

  connect() {
    this.ws = new WebSocket(this.url.replace('http', 'ws') + '/ws');
    this.ws.onopen = () => {
      if (this.sessionToken) {
        // Resume with token
        this.send('connect', {
          session_token: this.sessionToken,
          user_id: 'user_' + Date.now()
        });
      } else {
        console.log('No session token. Request pairing code first.');
      }
    };
    this.ws.onmessage = (e) => this.handleMessage(JSON.parse(e.data));
  }

  send(method, params) {
    this.ws.send(JSON.stringify({
      type: 'req',
      id: Date.now().toString(),
      method,
      params
    }));
  }

  handleMessage(frame) {
    if (frame.type === 'res' && frame.payload?.session_token) {
      localStorage.setItem('goclaw_token', frame.payload.session_token);
    }
    // Handle response...
  }
}

Troubleshooting

Issue Solution
"Code expired" Code is valid only 60 minutes. Request new code.
"Code not found" Code never existed or already used. Request new code.
"Max pending exceeded" Too many pending requests. Wait or have owner revoke old codes.
"Unauthorized" Owner has not approved the code yet. Check with owner.
Session token invalid Token may have expired or been revoked. Request new pairing code.

What's Next