Authentication·OAuth 2.0·CLI/Headless

MCP server OAuth 2.0 device flow security: headless agent authentication without redirect URIs

CLI-driven MCP servers and background agents can't use the Authorization Code flow — there's no browser to redirect. OAuth 2.0 Device Authorization Grant (RFC 8628) is the right solution, but it introduces its own security patterns that differ significantly from web-flow OAuth. This page covers device flow security for MCP servers: polling hygiene, scope binding, token lifetime management, and the common mistakes that create persistent auth vulnerabilities.

Why device flow for MCP servers

Most OAuth documentation assumes a web browser is available: the authorization code flow redirects the user to an identity provider, the user authenticates, and the provider redirects back to your callback URL with an authorization code. MCP servers running in CLI tools (like Claude Code), background worker processes, or server-to-server agent pipelines have no browser and no user-facing callback URL.

The Device Authorization Grant (RFC 8628) solves this. Instead of a redirect, the device (your MCP server or CLI client) displays a short URL and a user code. The user opens the URL on any device with a browser, enters the code, and authorizes. The MCP server polls the authorization server until the user approves or the device code expires. The MCP server then receives an access token — no redirect URI required.

The device flow sequence

The grant has four phases, each with security implications:

  1. Device Authorization Request: Client posts to /device_authorization with its client_id and requested scope. Server returns device_code, user_code, verification_uri, expires_in, and interval (minimum poll interval).
  2. User Interaction: Client displays verification_uri and user_code. User opens the URL in a browser, authenticates, enters the code, and grants (or denies) the requested scopes.
  3. Access Token Polling: Client polls /token every interval seconds with the device_code. Server returns authorization_pending until the user acts, then returns the access token (or access_denied).
  4. Token Use: Client uses the access token as a Bearer token in MCP server requests. If a refresh_token was issued, client can refresh without re-running the device flow.

Device flow security: polling interval enforcement

The authorization server specifies a minimum polling interval (typically 5 seconds). A naive client implementation that ignores this interval and polls at maximum speed creates a brute-force-like load on the authorization server and may trigger rate limiting or account lockouts. More critically, a missing slow-down check on the server side means a compromised device code can be brute-forced if the server doesn't enforce back-off on authorization_pending responses.

Server-side: Track per-device-code poll count and enforce the interval. Return HTTP 429 (or RFC 8628's slow_down error with an incremented interval) if a client polls faster than the stated interval.

Client-side: Always honor the interval field from the device authorization response, not a hardcoded poll interval. Implement exponential back-off on slow_down responses by adding 5 seconds to the interval per error.

Device code scope binding

A critical mistake in device flow implementations: the scope requested at device authorization time is advisory, not binding, unless the server enforces it. If your MCP server requests scope=crm:read during device authorization but the issued token actually has scope=crm:read crm:write (because the server ignores the requested scope), your client has more authority than it asked for. This is ambient authority via overly-permissive token issuance.

Validate the token's scope claim on receipt. If the token has scopes your client didn't request, reject it and log the discrepancy — this indicates either an authorization server misconfiguration or an attempted scope escalation.

Token binding: preventing device code relay attacks

Device flow is vulnerable to a relay attack: an attacker social-engineers the user into entering the device code on the attacker's behalf (phishing the verification URL). RFC 8628 introduced verification_uri_complete — a URL that pre-fills the user code — which reduces friction but doesn't prevent phishing. OAuth 2.0 Demonstrating Proof of Possession (DPoP, RFC 9449) binds the access token to a specific client key pair, so a relayed token can't be used from a different device.

For MCP servers, DPoP implementation: the MCP client generates a key pair at startup, signs a DPoP proof on each request, and the authorization server issues tokens bound to the client's public key. Even if the access token is intercepted, it's useless without the private key.

Refresh token rotation for long-lived agent sessions

Background agent MCP servers often run for hours or days without user interaction. If the access token expires and there's no refresh token, the agent fails silently or interrupts with an unexpected re-auth flow. If the refresh token is long-lived without rotation, it becomes a persistent credential that survives far beyond the user's intent.

The correct pattern: issue a short-lived access token (15–60 minutes) with a refresh token that uses rotation (each refresh invalidates the old refresh token and issues a new one). If a refresh token is used twice (replay attack), invalidate the entire token family and require re-authorization. Store refresh tokens encrypted at rest — treat them as equivalent to a password.

What SkillAudit checks for device flow

SkillAudit's auth flow analysis flags these device flow patterns:

Audit your MCP server's OAuth 2.0 device flow → Paste a GitHub URL for a free check including auth flow analysis and credential handling