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 & path | Who calls it | What it does |
|---|---|---|
POST /auth/device/code | Extension | Issues a fresh device_code + user_code pair. |
POST /auth/device/approve | You, signed into the browser | Binds a user_code to your account. |
POST /auth/device/poll | Extension | Trades 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/coderequest.
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_codeis stored server-side only as a SHA-256 hash, so a leaked DB dump can't replay pairings. client_idis 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).