Skip to content

fix(client): use form-urlencoded for OAuth token endpoint#24

Open
omarshahine wants to merge 2 commits intosteipete:mainfrom
omarshahine:fix/oauth-form-encoding
Open

fix(client): use form-urlencoded for OAuth token endpoint#24
omarshahine wants to merge 2 commits intosteipete:mainfrom
omarshahine:fix/oauth-form-encoding

Conversation

@omarshahine
Copy link

@omarshahine omarshahine commented Mar 15, 2026

Problem

Two bugs in the API client that make eightctl fail:

1. Wrong OAuth content type and credentials in token endpoint

authTokenEndpoint() sends application/json but the Eight Sleep auth server (auth-api.8slp.net/v1/tokens) expects standard OAuth2 application/x-www-form-urlencoded. It also hardcodes client_id: "sleep-client" with an empty client_secret instead of using the actual app credentials.

The JSON request always returns 400 (Joi validation), causing every authentication to fall through to authLegacyLogin(). After a few attempts the rate limiter kicks in → permanent 429 loop.

Note: PR #15 correctly identified the credential issue but kept JSON encoding, which is why the token endpoint still fails even with that fix applied.

2. Gzip responses not decompressed

do() sends Accept-Encoding: gzip but never decompresses the response body, causing:

Error: invalid character '\x1f' looking for beginning of value

(0x1f is the gzip magic byte.)

This bug is masked when the token cache isn't working (e.g., macOS Keychain inaccessible in headless/daemon environments), because every call re-authenticates via the token endpoint which returns plain JSON. Once tokens cache properly, subsequent API calls hit the gzip issue.

Fix

Commit 1 — OAuth auth:

  • Use application/x-www-form-urlencoded content type (standard OAuth2 token request format)
  • Use c.ClientID and c.ClientSecret instead of hardcoded "sleep-client" / ""
  • Make authURL a package-level var (was const) so tests can redirect it to a local server

Commit 2 — Gzip decompression:

  • Check Content-Encoding: gzip on responses and wrap body in gzip.Reader before JSON decoding

Tests added:

  • Token endpoint sends form-urlencoded with correct client credentials
  • Fallback to legacy login when token endpoint fails
  • Gzip-encoded API response is decompressed correctly

Tested against a live Pod 2 Pro — both auth and API calls work correctly.

Fixes #7, fixes #8, fixes #12


Note on headless/daemon environments: The 99designs/keyring library tries macOS Keychain first, which hangs indefinitely in LaunchAgent/launchd contexts (error -61: "User interaction is not allowed"). For daemon use, consider changing AllowedBackends to prefer FileBackend over KeychainBackend. This is not included in this PR as it's a broader design decision.

Lobster and others added 2 commits March 14, 2026 23:59
The Eight Sleep auth server (auth-api.8slp.net/v1/tokens) expects
standard OAuth2 form-urlencoded requests, not JSON. The previous
implementation sent JSON with hardcoded "sleep-client" credentials,
which caused a 400 from Joi validation. The fallback to legacy
/login then tripped the rate limiter, resulting in a permanent 429
loop.

Changes:
- Send application/x-www-form-urlencoded instead of application/json
- Use c.ClientID and c.ClientSecret (the real app creds extracted
  from the Android APK) instead of hardcoded "sleep-client"/""
- Make authURL a var so tests can point it at a local server
- Add tests for form encoding, credential passthrough, and
  legacy login fallback

Fixes steipete#7, fixes steipete#8, fixes steipete#12

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The do() method sends Accept-Encoding: gzip but never decompresses
the response body, causing json.Decode to fail with:

    invalid character '\x1f' looking for beginning of value

(0x1f is the gzip magic byte.)

Check Content-Encoding: gzip on responses and wrap the body in a
gzip.Reader before decoding. Added test with a mock gzip response.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant