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

262 lines
6.3 KiB
Markdown

# 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`