Intermediate 10 min WebSocket 1008

1008 Policy Violation — Authentication Timeout

Symptoms

- WebSocket connection closes with code 1008 and reason 'Authentication expired' or 'Session invalid'
- Connection is established successfully but closes after a predictable period (e.g., 1 hour)
- Refreshing the browser page and reconnecting restores the connection
- Server logs show 'token expired' or 'session not found' at the moment of close
- JWT `exp` claim decoded from the connection's token is in the past at the time of close

Root Causes

  • JWT access token used during WebSocket handshake has expired during the connection
  • Server-side session store (Redis, Memcached) evicted the session due to inactivity TTL
  • Server periodically revalidates auth tokens and closes the connection when the token is no longer valid
  • Token refresh flow not implemented for long-lived WebSocket connections
  • User logged out in another tab or device, causing the session to be invalidated globally

Diagnosis

**Step 1 — Capture the close code and reason in the client**

```javascript
const ws = new WebSocket('wss://api.example.com/ws');

ws.onclose = (event) => {
console.log('Close code:', event.code); // 1008
console.log('Close reason:', event.reason); // 'Authentication expired'
console.log('Was clean:', event.wasClean); // true if server sent close frame
};
```

**Step 2 — Decode the JWT to check expiry**

```bash
# Extract the token from the WebSocket handshake request header or cookie:
# In DevTools → Network → WS connection → Headers → look for Authorization or token param

# Decode the JWT payload (no verification needed for debugging):
TOKEN='eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsImV4cCI6MTcxMjAwMDAwMH0.sig'
echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Check 'exp' field — compare against current epoch:
date +%s
```

**Step 3 — Check Redis session TTL**

```bash
# If using Redis for sessions:
redis-cli TTL 'session:user123'
# -2 = key expired/missing; positive number = seconds remaining

# Check configured session lifetime:
redis-cli CONFIG GET maxmemory-policy
grep -r 'SESSION_COOKIE_AGE\|SESSION_EXPIRE' /path/to/your/settings/
```

**Step 4 — Inspect server-side authentication middleware**

```python
# Django Channels — check where auth is validated:
# consumers.py — look for periodic token revalidation
class ChatConsumer(WebsocketConsumer):
async def connect(self):
token = self.scope['query_string'] # or headers
if not await self.validate_token(token):
await self.close(code=1008)
```

**Step 5 — Check if logout in another session causes the closure**

```bash
# Test by logging out in a second browser tab while WebSocket is open
# If the WS immediately closes with 1008 → global session invalidation on logout
# Check: does your logout endpoint invalidate all sessions or only the current one?
```

Resolution

**Fix 1 — Implement token refresh over the WebSocket connection**

```javascript
// Client: send a refresh message before the token expires
let tokenExpiresAt = getTokenExpiry(); // epoch seconds

function scheduleTokenRefresh() {
const msUntilExpiry = (tokenExpiresAt - Date.now() / 1000) * 1000;
const refreshAt = msUntilExpiry - 60_000; // refresh 60s early
setTimeout(async () => {
const newToken = await refreshAccessToken();
ws.send(JSON.stringify({ type: 'auth_refresh', token: newToken }));
}, refreshAt);
}
```

```python
# Server (Django Channels): handle auth_refresh messages
async def receive(self, text_data):
data = json.loads(text_data)
if data['type'] == 'auth_refresh':
if await self.validate_token(data['token']):
self.token = data['token']
await self.send(json.dumps({'type': 'auth_ok'}))
else:
await self.close(code=1008)
```

**Fix 2 — Extend session TTL on WebSocket activity**

```python
# Touch the session on every received message to prevent Redis eviction:
import aioredis

async def receive(self, text_data):
# Extend session TTL by 1 hour on activity:
await redis.expire(f'session:{self.session_id}', 3600)
# ... handle message ...
```

**Fix 3 — Send a 1008 close reason the client can act on**

```python
# Server — close with a machine-readable reason:
await self.close(code=1008)
# Client can then redirect to login:
ws.onclose = (e) => {
if (e.code === 1008) window.location.href = '/login?reason=session_expired';
};
```

Prevention

- Design WebSocket authentication to handle token refresh without dropping the connection
- Use long-lived refresh tokens to obtain new access tokens transparently before expiry
- Implement keep-alive activity tracking to extend session TTL in Redis on received messages
- Send a warning message to the client (e.g., auth_expiring_soon) 5 minutes before closing
- Avoid invalidating all sessions on logout — instead, invalidate only the current session or implement a per-session token revocation list

Related Status Codes

Related Terms