xgoose logoxgoose

Account

Plugin device flow

The extension uses the standard OAuth 2.0 device authorization flow. No redirect URIs, no client secrets — just a short user code and a tab on /account.

Why device flow?

The extension lives in a browser process that doesn't own a meaningful URL — it can't register a redirect URI, and a popup callback is fragile across browsers. The device flow side-steps the problem entirely: the extension just polls until you approve a code from a regular browser tab.

Endpoints

Method & pathWho calls itWhat it does
POST /auth/device/codeExtensionIssues a fresh device_code + user_code pair.
POST /auth/device/approveYou, signed into the browserBinds a user_code to your account.
POST /auth/device/pollExtensionTrades an approved device_code for an access + refresh token pair.

Walkthrough

1. Extension requests a code

Triggered when you click Connect to xgoose.org in the extension popup. The extension calls:

POST https://api.xgoose.org/auth/device/code
Content-Type: application/json

{ "client_id": "xgoose-safari" }

The response:

{
  "device_code": "…long opaque token…",
  "user_code": "A1B2-C3D4",
  "verification_uri": "https://xgoose.org/account",
  "verification_uri_complete": "https://xgoose.org/account?device=A1B2-C3D4",
  "expires_in": 600,
  "interval": 5
}

The extension stores device_code in memory (never on disk), displays the user_code, and opens verification_uri_complete in a new tab.

2. You approve it in the browser

The /account page reads the ?device query param (pre-fills the input) and asks you to confirm. Submitting calls:

POST /auth/device/approve
Authorization: Bearer <your access token>
Content-Type: application/json

{ "user_code": "A1B2-C3D4" }

The worker matches the user code, records approved_at and user_id on the row, and returns { "ok": true }. From here, the device code is bound to you for the remainder of its 10-minute window.

3. Extension polls and gets tokens

Meanwhile, the extension is calling poll roughly every interval seconds (5 by default):

POST /auth/device/poll
Content-Type: application/json

{ "device_code": "…", "client_id": "xgoose-safari" }

Possible responses:

  • 200 OK { access_token, refresh_token, access_token_expires_at, token_type: "Bearer" } — you approved; the device code is deleted server-side so the same poll can't replay.
  • 400 { "error": "authorization_pending" } — keep polling.
  • 429 slow_down — you polled faster than the advertised interval; back off.
  • 400 { "error": "expired_token" } — 10 minutes elapsed. Start over with a fresh /auth/device/code request.

Lifetimes

  • Device codes: 10 minutes from issue. Single-use; the row is deleted on successful poll.
  • Poll interval: 5 seconds. Earlier polls return slow_down.
  • Access token (issued): 15 minutes by default.
  • Refresh token (issued): 60 days; rotates on every use of POST /auth/refresh.

Security notes

  • The device_code is stored server-side only as a SHA-256 hash, so a leaked DB dump can't replay pairings.
  • client_id is enforced on poll: the same code can't be redeemed by a different client than the one that requested it.
  • Approving a user code requires a logged-in browser session, which itself requires a verified email. There is no public way to approve someone else's pairing.
  • The extension can revoke its own session at any time from /account Sessions.

Reference

See RFC 8628 for the canonical description of the flow. We follow it strictly modulo error-code spellings (we return expired_token and authorization_pending, both 400; and slow_down as 429).