6.3 KiB
Kosmi WebSocket Authentication - Reverse Engineering Findings
Date: October 31, 2025
Status: ✅ Authentication mechanism fully reverse engineered
Summary
The Kosmi WebSocket API (wss://engine.kosmi.io/gql-ws) requires a JWT token that is obtained via an HTTP POST request. The token is then sent in the connection_init message payload.
Authentication Flow
1. Browser visits: https://app.kosmi.io/room/@roomname
↓
2. JavaScript makes POST to: https://engine.kosmi.io/
↓
3. Server returns JWT token (valid for 1 year!)
↓
4. JavaScript connects WebSocket: wss://engine.kosmi.io/gql-ws
↓
5. Sends connection_init with token
↓
6. Server responds with connection_ack
↓
7. Ready to send GraphQL subscriptions/mutations
Key Findings
1. JWT Token Structure
The token is a standard JWT with the following payload:
{
"aud": "kosmi",
"exp": 1793367309,
"iat": 1761917709,
"iss": "kosmi",
"jti": "c824a175-46e6-4ffc-b69a-42f319d62460",
"nbf": 1761917708,
"sub": "a067ec32-ad5c-4831-95cc-0f88bdb33587",
"typ": "access"
}
Important fields:
sub: Anonymous user ID (UUID)exp: Expiration timestamp (1 year from issuance!)typ: "access" token type
2. Token Acquisition
Request:
POST https://engine.kosmi.io/
Content-Type: application/json
Referer: https://app.kosmi.io/
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
Body: (Likely a GraphQL query/mutation for anonymous login or session creation)
Response: Returns JWT token (details to be captured in next test)
3. WebSocket Connection Init
WebSocket URL: wss://engine.kosmi.io/gql-ws
Protocol: graphql-ws
First message (connection_init):
{
"type": "connection_init",
"payload": {
"token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...",
"ua": "TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEyMC4wLjAuMCBTYWZhcmkvNTM3LjM2",
"v": "4364",
"r": ""
}
}
Payload fields:
token: The JWT access tokenua: Base64-encoded User-Agent stringv: Version number "4364" (app version?)r: Empty string (possibly room-related, unused for anonymous)
Response:
{
"type": "connection_ack",
"payload": {}
}
4. Required Headers
For POST request:
headers := http.Header{
"Content-Type": []string{"application/json"},
"Referer": []string{"https://app.kosmi.io/"},
"User-Agent": []string{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
}
For WebSocket:
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
"User-Agent": []string{"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
}
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
}
5. Cookies
Cookie found: g_state
Value structure:
{
"i_l": 0,
"i_ll": 1761917710911,
"i_b": "w4+5eCKfslo5DMEmBzdtPYztYGoOkFIbwBzrc4xEzDk"
}
Purpose: Appears to be Google Analytics state or similar tracking.
Required for WebSocket?: ❌ NO - The JWT token is the primary authentication mechanism.
Implementation Strategy
Step 1: Token Acquisition
We need to reverse engineer the POST request body. Two approaches:
Option A: Capture the POST body
- Modify capture tool to log request bodies
- See exactly what GraphQL query/mutation is sent
Option B: Test common patterns
- Try empty body:
{} - Try anonymous login mutation
- Try session creation query
Step 2: Native Client Implementation
type NativeClient struct {
httpClient *http.Client
wsConn *websocket.Conn
token string
roomID string
log *logrus.Entry
}
func (c *NativeClient) Connect() error {
// 1. Get JWT token
token, err := c.acquireToken()
if err != nil {
return err
}
c.token = token
// 2. Connect WebSocket
dialer := websocket.Dialer{
Subprotocols: []string{"graphql-ws"},
}
headers := http.Header{
"Origin": []string{"https://app.kosmi.io"},
"User-Agent": []string{userAgent},
}
conn, _, err := dialer.Dial("wss://engine.kosmi.io/gql-ws", headers)
if err != nil {
return err
}
c.wsConn = conn
// 3. Send connection_init
return c.sendConnectionInit()
}
func (c *NativeClient) acquireToken() (string, error) {
// POST to https://engine.kosmi.io/
// Parse response for JWT token
}
func (c *NativeClient) sendConnectionInit() error {
uaEncoded := base64.StdEncoding.EncodeToString([]byte(userAgent))
msg := map[string]interface{}{
"type": "connection_init",
"payload": map[string]interface{}{
"token": c.token,
"ua": uaEncoded,
"v": "4364",
"r": "",
},
}
return c.wsConn.WriteJSON(msg)
}
Next Steps
- ✅ Capture token acquisition POST body
- ✅ Implement
acquireToken()function - ✅ Test direct WebSocket connection with token
- ✅ Verify message subscription works
- ✅ Verify message sending works
- ✅ Replace ChromeDP client
Success Criteria
- Can obtain JWT token without browser
- Can connect WebSocket with just token
- Can receive messages
- Can send messages
- No ChromeDP dependency
- < 50MB RAM usage
- < 1 second startup time
Additional Notes
Token Validity
The JWT token has a 1-year expiration! This means:
- We can cache the token
- No need to re-authenticate frequently
- Simplifies the implementation significantly
Anonymous Access
The Kosmi API supports true anonymous access:
- No login credentials needed
- Just POST to get a token
- Token is tied to an anonymous user UUID
This is excellent news for our use case!
No Cookies Required
Unlike our initial assumption, cookies are NOT required for WebSocket authentication. The JWT token in the connection_init payload is sufficient.
References
- Captured data:
auth-data.json - Capture tool:
cmd/capture-auth/main.go - Chrome extension (reference):
.examples/chrome-extension/inject.js