device-token in GatewayAuthResult.methodPersistent Device Authentication
The device pairing flow enables mobile and IoT devices to establish persistent authentication without sharing static secrets. Once paired, a device token allows reconnection without re-entering credentials.
device-token in GatewayAuthResult.methodAUTH_RATE_LIMIT_SCOPE_DEVICE_TOKENauth.ts — authorizeGatewayConnect()Device pairing solves the problem of authenticating devices that cannot easily enter passwords or manage bearer tokens. Instead, a short-lived pairing code is displayed on a trusted device (e.g., the CLI), and the new device enters this code to receive a long-lived device token.
AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN rate limit scope.
sequenceDiagram
participant D as New Device
participant GW as Gateway
participant Auth as Auth Layer
participant RL as Rate Limiter
participant T as Trusted Device
T->>GW: Request pairing code
GW->>GW: Generate short-lived code
GW-->>T: Display pairing code
D->>GW: Connect with pairing code
GW->>RL: check(ip, "device-token")
alt rate limited
RL-->>D: {allowed: false, retryAfterMs}
end
RL-->>GW: {allowed: true}
GW->>Auth: validate pairing code
Auth->>Auth: verify code not expired
Auth->>Auth: verify code not already used
Auth-->>GW: code valid
GW->>GW: Generate device token
GW-->>D: {ok: true, method: "device-token", deviceToken}
Note over D: Store token securely
D->>GW: Reconnect with device token
GW->>Auth: authorizeGatewayConnect({connectAuth: {deviceToken}})
Auth-->>D: {ok: true, method: "device-token"}
The trusted device (typically the CLI running on the same machine as the Gateway) requests a new pairing code. The Gateway generates a cryptographically random code with a short TTL (typically 5 minutes).
// The pairing code is a short alphanumeric string
// displayed to the user for manual entry on the new device
const pairingCode = generatePairingCode();
// TTL ensures abandoned pairing attempts expire quickly
The new device connects to the Gateway and submits the pairing code via ConnectParams. The Gateway first checks rate limiting under the "device-token" scope.
// Rate limiting protects against brute-force pairing attempts
const rlResult = rateLimiter.check(
normalizeRateLimitClientIp(clientIp),
AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN,
);
if (!rlResult.allowed) {
return { ok: false, rateLimited: true, retryAfterMs: rlResult.retryAfterMs };
}
If the code is valid and not expired, the Gateway generates a long-lived device token and returns it. The code is immediately invalidated to prevent reuse.
// On successful pairing, the GatewayAuthResult includes the device token
const result: GatewayAuthResult = {
ok: true,
method: 'device-token',
user: 'device:' + deviceId,
};
// The device token is returned in the response payload
// The client stores it securely for future connections
For all future connections, the device includes its token in ConnectParams.connectAuth. The Gateway validates the token through authorizeGatewayConnect() without needing any user interaction.
stateDiagram-v2
[*] --> CodeGenerated: Trusted device requests code
CodeGenerated --> CodePending: Code displayed to user
CodePending --> CodeSubmitted: New device submits code
CodePending --> CodeExpired: TTL exceeded
CodeSubmitted --> RateLimitCheck: Check device-token scope
RateLimitCheck --> Rejected: Rate limited
RateLimitCheck --> Validating: Allowed
Validating --> Paired: Code valid
Validating --> Failed: Code invalid/expired
Failed --> CodePending: User retries
Paired --> TokenIssued: Generate device token
TokenIssued --> [*]: Device stores token
CodeExpired --> [*]: Pairing abandoned
Rejected --> [*]: Too many attempts
| Scenario | Behavior | User Impact |
|---|---|---|
| Code expired | Return {ok: false, reason: "code-expired"} | Must request new code |
| Code already used | Return {ok: false, reason: "code-consumed"} | Must request new code |
| Rate limited | Return {ok: false, rateLimited: true} | Wait for lockout to expire |
| Invalid code format | Return {ok: false, reason: "invalid-code"} | Re-enter code correctly |
| Token revoked | Return {ok: false, reason: "token-revoked"} | Must re-pair device |
| Gateway restart | Persistent tokens survive; in-flight codes lost | Complete pending pairings only |
| Parameter | Type | Default | Description |
|---|---|---|---|
pairing.codeTtlMs | number | 300000 | Pairing code TTL (5 min) |
pairing.codeLength | number | 6 | Pairing code character length |
rateLimit.maxAttempts | number | 10 | Max pairing attempts per window |
rateLimit.lockoutMs | number | 300000 | Lockout after max attempts |
src/gateway/
├── auth.ts ← authorizeGatewayConnect, device-token method handler
├── auth-rate-limit.ts ← AUTH_RATE_LIMIT_SCOPE_DEVICE_TOKEN
├── credentials.ts ← Device token storage and retrieval
└── protocol/
└── index.ts ← ConnectParams (includes device token field)