You SSH into a remote box. You want to run an agent there that uses your GitHub identity. The standard OAuth flow opens a browser. The remote box has no browser, no display, and no reason to be holding a copy of your GitHub password.
The answer is RFC 8628, the OAuth 2.0 Device Authorization Grant. Most people just call it device code flow. Every serious identity provider supports it. Almost no integration guide uses it. This post is about why it exists, when it's the right call, and the operational quirks nobody documents in the SDK readme.
When you need it
Device code is the right flow when any of these are true:
- You're SSHed into a server with no GUI and don't want to set up X11 forwarding.
- The agent runs in a CI runner or container with terminal output but no browser.
- The machine being configured is on a network where opening a callback URL would be a pain (corporate firewall, NAT, Tailscale-only host).
- The user setting things up is at a different device than the one being authenticated.
The traditional alternative is "ssh -L, port-forward the OAuth callback to your laptop". That works once. It fails the moment the agent's refresh window expires overnight while you're not at your laptop. The other alternative is a long-lived PAT, which gives up OAuth's automatic refresh and per-scope consent.
Device code preserves both. The only cost is a 15-second one-time interaction on whatever device has a browser.
How it works
There are two parallel halves running at the same time. The agent runs the device side. You, on your phone or laptop, run the user side.
Concretely:
- The agent calls
/device/codewith itsclient_idand requested scopes. - The provider returns a
device_code(a long opaque string the agent keeps), auser_code(a short readable code likeWDJB-MJHT), and a verification URL. - The agent prints "Visit
https://github.com/login/deviceand enterWDJB-MJHT." - You, on a device that has a browser, follow those instructions and approve the scopes.
- Meanwhile, the agent polls the provider's
/tokenendpoint asking "is this approved yet?". It getsauthorization_pendinguntil you finish, then gets the actual access token plus a refresh token.
The agent never sees your password. You never type a credential into the remote box. The two halves are linked by the user_code you typed plus the provider's record of which device_code it issued for that user.
A bare HTTP example for GitHub
If you wanted to do this without any client library, just to see the wire format:
curl -X POST https://github.com/login/device/code \
-H "Accept: application/json" \
-d "client_id=YOUR_CLIENT_ID" \
-d "scope=repo read:user"
Response:
{
"device_code": "3584d83530557fdd1f46af8289938c8ef79f9dc5",
"user_code": "WDJB-MJHT",
"verification_uri": "https://github.com/login/device",
"expires_in": 900,
"interval": 5
}
Then poll the token endpoint every 5 seconds:
curl -X POST https://github.com/login/oauth/access_token \
-H "Accept: application/json" \
-d "client_id=YOUR_CLIENT_ID" \
-d "device_code=3584d83530557fdd1f46af8289938c8ef79f9dc5" \
-d "grant_type=urn:ietf:params:oauth:grant-type:device_code"
While the user hasn't approved:
{ "error": "authorization_pending" }
After approval:
{
"access_token": "ghu_...",
"token_type": "bearer",
"scope": "repo,read:user",
"refresh_token": "ghr_...",
"expires_in": 28800
}
What the RFC doesn't tell you
Things that bit me building agents on top of this flow.
The interval is a floor, not a target. interval: 5 means "do not poll more often than every 5 seconds, or I will slow you down." If you poll faster, the provider returns slow_down and adds penalty seconds to the cadence. Respect the initial interval and increase it whenever you see slow_down.
The expires_in is real. That 900 is 15 minutes. If the user takes longer than that, the device_code is dead and you have to start over with a fresh /device/code call. Build for this. Show the remaining time. Restart the flow on expiry rather than just looping forever.
verification_uri_complete saves a step. Some providers return a second URL that has the user_code pre-filled. The agent can render this as a QR code or a clickable link. GitHub, Microsoft, Auth0, and Okta support it. Google does not.
The user can narrow the scope at consent. When you approve in the browser, you can sometimes uncheck specific scopes the device asked for. The token you receive then reflects the scopes you actually approved, which may be a subset of what was requested. The agent must read scope in the response and not assume it got everything it asked for.
Refresh-token lifetimes vary by provider and even by scope. GitHub, Google, and Microsoft all have different rules, and within each provider the rules depend on the scope set and tenant policy. The pragmatic mitigation is to do a no-op refresh on a cadence shorter than the shortest revocation window of any provider you depend on. Daily is conservative; once a week is fine for most.
How authsome does it
The way I use this in practice on an SSH box: log in once with the flow, then everything else is the same as on my laptop.
authsome login github --flow device_code
--flow accepts pkce (the default, browser-based), device_code, dcr_pkce (Dynamic Client Registration variant for providers like Notion), and api_key. For providers that don't have an OAuth flow at all, authsome's API-key login on a headless machine falls back to masked terminal input via getpass so you can paste a key without it appearing in your shell history.
After --flow device_code completes, the CLI prints something like:
Visit https://github.com/login/device on any device with a browser
and enter the user code: WDJB-MJHT
Waiting for authorization...
Open the URL on your phone, paste the code, approve. The next poll lands the access token, the daemon stores it encrypted under your profile, and any subsequent authsome run -- <command> on this box uses the same credential. Refresh happens automatically.
To check whether a given provider supports device code at all:
authsome inspect github
# look for: supports_device_code: true
Most bundled providers do. Postiz is the only one I know of that defaults to device code (rather than PKCE) out of the box, but you can force it on any supported provider with --flow device_code.
When NOT to use device code
It's not always the right answer.
- On your dev laptop. Just use the browser PKCE flow. It's faster and the UX is better when you actually have a browser.
- For unattended CI runners. Device code requires a human to walk to a phone. CI is unattended by definition. Use a GitHub App's installation token, a service-account PAT in a secrets manager, or pre-bake credentials into the runner image. Device code is for "human one time, then headless" not "no human at all".
- For machine-to-machine traffic. Use the OAuth2 client credentials grant (
grant_type=client_credentials). Device code is specifically for a user identity on a device where a user can't conveniently type a password.
The mistake people make is treating "no browser on this box" as "fall back to a PAT". A PAT means giving up OAuth's automatic refresh and per-scope auditability. Device code preserves both at the cost of a one-time 15-second interaction on a different device.
Summary
Device code is OAuth's answer to "I need a real user identity on a box that has no browser". It works across every major identity provider. It preserves refresh and scope semantics. The only friction is the one-time 15-second user_code paste.
Where people get it wrong is operational: polling too fast, ignoring expiry, treating the user_code as static for the session. Build with those in mind and the flow becomes a permanent fixture in any headless-agent setup.
Next steps
Further reading
Top agent proxy tools in 2026: what each one does and what to know before picking one
Eight tools that sit between AI agents and the services they call. Not a comparison post. A walking tour of the category so you know what each is for, what it's good at, and where it'll bite you.
Read postMay 17, 2026What is MCP? A developer's primer on the Model Context Protocol
The protocol that connects AI agents to external tools. What MCP actually is, how the architecture works, what to build with it, and the auth questions nobody answers.
Read postMay 15, 2026AI agent security in 2026: the four threat models you actually need to think about
Prompt injection, credential exfiltration, runaway autonomy, supply chain. What each one looks like in practice, how attacks actually unfold, and which defenses work.
Read post