Files
IRC-kosmi-relay/AUTH_FINDINGS.md
2025-10-31 16:17:04 -04:00

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 token
  • ua: Base64-encoded User-Agent string
  • v: 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

  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