# 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: ```json { "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**: ```http 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)**: ```json { "type": "connection_init", "payload": { "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9...", "ua": "TW96aWxsYS81LjAgKE1hY2ludG9zaDsgSW50ZWwgTWFjIE9TIFggMTBfMTVfNykgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEyMC4wLjAuMCBTYWZhcmkvNTM3LjM2", "v": "4364", "r": "" } } ``` **Payload fields**: - `token`: The JWT access token - `ua`: Base64-encoded User-Agent string - `v`: Version number "4364" (app version?) - `r`: Empty string (possibly room-related, unused for anonymous) **Response**: ```json { "type": "connection_ack", "payload": {} } ``` ### 4. Required Headers **For POST request**: ```go 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**: ```go 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**: ```json { "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 ```go 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 1. ✅ Capture token acquisition POST body 2. ✅ Implement `acquireToken()` function 3. ✅ Test direct WebSocket connection with token 4. ✅ Verify message subscription works 5. ✅ Verify message sending works 6. ✅ 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`