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.
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
Generation:
- Length: 8 characters
- Alphabet:
ABCDEFGHJKLMNPQRSTUVWXYZ23456789(excludes ambiguous: 0, O, 1, I, L) - TTL: 60 minutes
- Max pending per account: 3
Example codes:
ABCD1234XY8PQRST2M5H9JKL
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.
Owner runs CLI command or uses dashboard to approve:
goclaw device.pair.approve --code ABCD1234Or 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
}
}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.
On reconnect, use stored token:
{
"type": "req",
"id": "1",
"method": "connect",
"params": {
"session_token": "session_xyz...",
"user_id": "web_user_1"
}
}- 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:
IsPairedchecks handle transient database errors gracefully — a DB error returns denied rather than accidentally allowing access
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...
}
}| 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. |
- Overview — Channel concepts and policies
- WebSocket — Direct RPC communication
- Telegram — Telegram setup
- WebSocket Protocol — Full protocol reference