Device authorization flow security: risks and mitigations
The RFC 8628 device authorization flow — the “enter this code on another device” pattern from smart TVs — is the most practical OAuth flow for command-line tools and agents. It works in headless environments, needs no browser on the client, and keeps the user’s approval one step away from the machine doing the work. That convenience comes with a distinct set of attack surfaces, and most of them are invisible until something goes wrong.
How the flow works
The client requests a device code and a user code from the provider, shows the user a URL and the code, then polls the token endpoint until the user approves on another device. Approval yields an access token and a refresh token. Everything after approval is a token lifecycle problem; everything before it is a trust problem.
The risks
User-code phishing. An attacker runs their own device flow against the same provider and shows the victim their URL and code — “log in here to authorize the tool.” The victim approves the attacker’s session, and the attacker’s client receives the tokens. The victim believes they authorized your tool.
Low-entropy codes and polling abuse. User codes are short by design so humans can type them. Short codes mean brute force is a question of rate limits, not mathematics. A client that polls aggressively, or an attacker who guesses codes at scale, can probe the authorization server. Providers rate-limit and return slow_down to honest clients; attackers do not care.
Token theft at the client. The flow ends with tokens on a machine that may not be trustworthy. If that machine is an agent workspace, tokens sitting in a file or a shell history are readable by the agent — the same leak you were trying to avoid by using OAuth at all.
Refresh-token standing access. The refresh token is the dangerous half of the exchange. Reused, a stolen copy stays valid for the lifetime of the consent; rotated on every use, it dies at the next refresh.
Mitigations that hold up
Bind the code to the context. Never display a verification URL the tool did not request. A broker CLI that controls the whole flow — requesting the code, rendering the URL, polling — gives the user one canonical URL to check, and the provider’s real domain is visible in it.
Respect the polling protocol. Follow the RFC’s timing: honor slow_down intervals, back off on errors, and stop polling once the user cancels.
Keep tokens off the agent’s disk. Store tokens in the credential store with locked-down permissions, and resolve them to fresh access tokens only at use time. Never let the refresh token appear in command arguments, environment dumps, or agent context.
Rotate refresh tokens as single-use. Treat every refresh as consuming the old token. Combined with short-lived access tokens, a token that leaks from any layer has a short half-life and no standing value.
Surface reauth, don’t retry it. A revoked refresh token returns invalid_grant. The correct behavior is to stop, report reauth_required, and ask the user to re-approve — not to retry the same failing call in a loop, which turns a revocation into a retry storm.
Where this lands in practice
A credential broker that implements the flow end to end — request, poll, store, refresh, status — removes most of the manual steps where these risks live. Its status command reports valid, expired, or reauth_required, refresh tokens are single-use and rotated, invalid_grant is never retried, and every refresh or failure lands in an append-only audit log that never contains token values. That is the shape of the mitigation set — control the flow, contain the tokens, make revocation visible — and it is the shape trustless implements.
Have you ever audited where your device-flow tokens actually live after login — and who could read them?