Skip to main content

Overview

The device flow lets a program with no browser (a CLI, a headless agent, an MCP client) get a signed-in session. The program asks for a short code, the user approves it in a browser where they are already signed in, and the program receives a token.

Server setup

This mounts three endpoints under your TheAuth base path: Your /device page reads user_code from the URL, shows who is asking, and posts to the authorize endpoint with the user’s session cookie:

What is enforced

Breaking fix. /auth/device/authorize used to read user_id from the request body when called through module.handleRequest, so anyone could approve a code as any user. The user now always comes from the authenticated session (deviceAuth() plugin) or from resolveUser (the standalone module). A body user_id is ignored, and with no signed-in user the endpoint answers 401.
  • The device code is stored only as a SHA-256 hash, as is the user code index.
  • Each approving user gets 5 wrong guesses per 15 minutes (userCodeAttemptLimit, userCodeAttemptWindowSeconds); further attempts get 429.
  • slow_down is tracked in secondary storage, raises the interval by 5 seconds each time as RFC 8628 requires, and holds across instances.
  • A device code can be exchanged once. A second poll gets expired_token.
  • The approval endpoint requires Content-Type: application/json and refuses a cross-site Origin header, which blocks one-click approval from another site. Add origins with trustedOrigins.
  • Per-IP rate limits cover all three endpoints when you use rateLimit() (see Rate limiting).
With auth.session configured, the token is a TheAuth session token you can send as Authorization: Bearer .... To issue something else, pass issueToken(userId, { clientId, scope }).

The CLI

--server falls back to $THEAUTH_URL, then to the only server you are logged in to. --no-browser prints the code without opening a browser. Credentials are saved with file mode 0600 in a 0700 directory: Set THEAUTH_CREDENTIALS_FILE to use another path. logout revokes the session on the server (best effort) and deletes the local copy. On Windows the mode bits do not apply; the file sits in your per-user profile directory.

Use it from your own CLI or MCP client

loginWithDeviceFlow handles polling, slow_down, denial and expiry, and throws a DeviceFlowError with a code (access_denied, expired_token, aborted, server_error, no_token). Pass save: false to skip the credential cache, and an AbortSignal to cancel.

Standalone module

Without the plugin, use createDeviceAuthModule({ verificationUri, resolveUser, issueToken, storage }) and route requests through module.handleRequest(request).
Last modified on October 8, 2026