Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

OAuth 2.0 Device Authorization Grant lets a command-line program authenticate without requiring a redirect-capable browser on the same computer. The CLI requests a short-lived device code, displays a verification URL and user code, and polls the authorization server until the user approves the request on a phone or another computer. Use the server-provided expiry and polling interval, handle authorization_pending and slow_down correctly, and store the resulting tokens in the operating system’s credential store.

What OAuth device flow is

OAuth device flow is the OAuth 2.0 Device Authorization Grant defined by RFC 8628, an IETF Standards Track specification published in August 2019. It is designed for Internet-connected clients that cannot use a suitable browser locally or have limited input capabilities. The user reviews and approves the request on a secondary device while the CLI waits for the result.

The protocol is appropriate when a CLI runs on a headless server, a remote shell, a machine without a graphical session, or a device where entering credentials is impractical. It is not a general replacement for browser-based OAuth on capable native applications.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Minimum requirements

  • The CLI can make outbound HTTPS requests. Every device-flow request must use TLS.
  • The CLI can display or otherwise communicate a verification URI and a user code.
  • The user has a phone, computer, or other secondary device for approval.
  • The authorization server supports RFC 8628 and has issued the client an identifier.

The device-flow sequence

  1. Register the client. Create a client registration with the identity provider and obtain a client identifier. A CLI normally cannot keep a client secret confidential, so treat it as a public client.
  2. Request a device code. Send the client_id and, when needed, a space-delimited scope to the provider’s device authorization endpoint.
  3. Read the response. The server returns a device_code for polling, a shorter user_code for the human, a verification URI, expires_in, and a recommended polling interval. Some providers also return a complete verification URI containing the user code.
  4. Show clear instructions. Print the URI and code in a form that is easy to copy. You may offer to open the user’s browser when a graphical session is available, but the flow must still work when it is not.
  5. Poll the token endpoint. Submit grant_type=urn:ietf:params:oauth:grant-type:device_code, the device_code, and client_id at the server-specified interval.
  6. Process the result. Continue on authorization_pending; increase the delay after slow_down; stop with a useful message on denial or expiry; and, on success, securely store the access and refresh tokens.

Request and response fields

Stage Field Purpose
Device request client_id Identifies the registered CLI.
Device request scope Requests only the permissions the command actually needs.
Device response device_code Opaque value sent to the token endpoint; never show it to the user.
Device response user_code Short value the user enters on the verification page.
Device response verification URI Page where the user signs in and approves the request.
Device response expires_in Lifetime of the device authorization, in seconds.
Device response interval Minimum number of seconds between token requests.
Token response access_token Credential for API calls; protect it like a password.
Token response refresh_token Optional long-lived credential used to obtain later access tokens.
Token response token_type and expires_in Tell the client how to use and refresh the access token.

Endpoint URLs, accepted scopes, authentication methods, and response extensions are provider-specific. Use the values documented by the identity provider rather than assuming that another provider’s paths or defaults apply.

#1 Best Overall
Yubico - Security Key C NFC - Basic Compatibility - Multi-Factor authentication (MFA) Security Key and passkey, Connect via USB-C or NFC, FIDO Certified
  • POWERFUL SECURITY KEY: The Security Key C NFC is the essential physical passkey for protecting your digital life from phishing attacks. It ensures only you can access your accounts.
  • WORKS WITH 1000+ ACCOUNTS: Compatible with Google, Microsoft, and Apple. A single Security Key C NFC secures 100 of your favorite accounts, including email, password managers, and more.
  • FAST & CONVENIENT LOGIN: Plug in your Security Key C NFC via USB-C and tap it, or tap it against your phone (NFC) to authenticate. No batteries, no internet connection, and no extra fees required.
  • TRUSTED PASSKEY TECHNOLOGY: Uses the latest passkey standards (FIDO2/WebAuthn & FIDO U2F) but does not support One-Time Passwords. For complex needs, check out the YubiKey 5 Series.
  • BUILT TO LAST: Made from tough, waterproof, and crush-resistant materials. Manufactured in Sweden and programmed in the USA with the highest security standards.

A complete Python implementation

The following script uses the standard sequence and handles the protocol’s polling errors. Replace the endpoint and client values with those from your provider. It requires Python 3 and the requests package.

import time
import webbrowser
import requests

DEVICE_ENDPOINT = "https://YOUR_PROVIDER.example/device"
TOKEN_ENDPOINT = "https://YOUR_PROVIDER.example/token"
CLIENT_ID = "YOUR_CLIENT_ID"
SCOPE = "openid profile"

def device_login():
    device = requests.post(
        DEVICE_ENDPOINT,
        data={"client_id": CLIENT_ID, "scope": SCOPE},
        timeout=30,
    )
    device.raise_for_status()
    data = device.json()

    device_code = data["device_code"]
    user_code = data["user_code"]
    verification_uri = data.get("verification_uri") or data["verification_url"]
    expires_in = int(data["expires_in"])
    interval = int(data.get("interval", 5))

    print(f"Open: {verification_uri}")
    print(f"Enter code: {user_code}")
    try:
        answer = input("Press Enter to open the browser, or type n to skip: ")
        if answer.lower() != "n":
            webbrowser.open(verification_uri)
    except EOFError:
        pass

    deadline = time.monotonic() + expires_in
    while time.monotonic() < deadline:
        time.sleep(interval)
        token = requests.post(
            TOKEN_ENDPOINT,
            data={
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                "device_code": device_code,
                "client_id": CLIENT_ID,
            },
            timeout=30,
        )

        if token.status_code == 200:
            result = token.json()
            if "access_token" not in result:
                raise RuntimeError("Token response did not contain access_token")
            return result

        try:
            error = token.json().get("error")
        except ValueError:
            token.raise_for_status()
            raise RuntimeError("Non-JSON error from token endpoint")

        if error == "authorization_pending":
            continue
        if error == "slow_down":
            interval += 5
            continue
        if error in ("access_denied", "expired_token"):
            raise RuntimeError(f"Authorization ended: {error}")
        raise RuntimeError(f"Token request failed: {error}")

    raise TimeoutError("The device code expired before approval")

if __name__ == "__main__":
    tokens = device_login()
    print("Login succeeded; token type:", tokens.get("token_type", "unknown"))
    # Store tokens in the platform credential store, not in this terminal or a log file.

Do not print device_code, access tokens, refresh tokens, or complete authorization responses. In production, replace the final print statement with a call to the operating system's credential manager or a dedicated encrypted store.

Equivalent HTTP requests

cURL

First request a device code:

curl -X POST "https://YOUR_PROVIDER.example/device" 
  -d "client_id=YOUR_CLIENT_ID" 
  -d "scope=openid%20profile"

After displaying the returned code and waiting at least the returned interval, poll the token endpoint:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -X POST "https://YOUR_PROVIDER.example/token" 
  -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" 
  -d "device_code=DEVICE_CODE_FROM_RESPONSE" 
  -d "client_id=YOUR_CLIENT_ID"

Node.js

This Node.js 18+ example uses the built-in fetch API and follows the same error rules:

Rank #2
Yubico - YubiKey 5C NFC - Multi-Factor authentication (MFA) Security Key and passkey, Connect via USB-C or NFC, FIDO Certified - Protect Your Online Accounts
  • POWERFUL SECURITY KEY: The YubiKey 5C NFC is the most versatile physical passkey, protecting your digital life from phishing attacks. It ensures only you can access your accounts
  • WORKS WITH 1000+ ACCOUNTS: Compatible with popular accounts like Google, Microsoft, and Apple. A single YubiKey 5C NFC secures 100+ of your favorite accounts, including email, password managers, and more
  • FAST & CONVENIENT LOGIN: Plug in your YubiKey 5C NFC via USB and tap it, or tap it against your phone (NFC), to authenticate. No batteries, no internet connection, and no extra fees required
  • MOST SECURE PASSKEY: Supports FIDO2/WebAuthn, FIDO U2F, Yubico OTP, OATH-TOTP/HOTP, Smart card (PIV), and OpenPGP. That means it’s versatile, working almost anywhere you need it
  • PRIMARY & SPARE KEYS: Just like having a spare house key, we recommend buying two YubiKeys - one for daily use and one as a spare. That way you’ll never get locked out of your accounts
const deviceEndpoint = 'https://YOUR_PROVIDER.example/device';
const tokenEndpoint = 'https://YOUR_PROVIDER.example/token';
const clientId = 'YOUR_CLIENT_ID';

const deviceBody = new URLSearchParams({ client_id: clientId, scope: 'openid profile' });
const deviceResponse = await fetch(deviceEndpoint, { method: 'POST', body: deviceBody });
if (!deviceResponse.ok) throw new Error(`Device request failed: ${deviceResponse.status}`);
const device = await deviceResponse.json();

console.log(`Open ${device.verification_uri} and enter ${device.user_code}`);
let interval = Number(device.interval || 5);
const deadline = Date.now() + Number(device.expires_in) * 1000;
let tokens;
while (Date.now() < deadline) {
  await new Promise(resolve => setTimeout(resolve, interval * 1000));
  const body = new URLSearchParams({
    grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
    device_code: device.device_code,
    client_id: clientId
  });
  const response = await fetch(tokenEndpoint, { method: 'POST', body });
  const result = await response.json();
  if (response.ok) { tokens = result; break; }
  if (result.error === 'authorization_pending') continue;
  if (result.error === 'slow_down') { interval += 5; continue; }
  throw new Error(`Authorization failed: ${result.error}`);
}
if (!tokens) throw new Error('The device code expired before approval');
console.log('Login succeeded');

Provider timing and polling behavior

expires_in and interval are server instructions, not universal constants. For example, current Microsoft Entra device-code documentation uses a default 15-minute sign-in lifetime, while GitHub documents a 900-second validity window for its user code. Your client must honor the values in the response because providers can change them.

GitHub's documented flow asks the user to enter the code at https://github.com/login/device, then polls until authorization completes. GitHub explicitly warns that ignoring its minimum polling interval can trigger rate-limit errors. Sleeping before the first poll, rather than sending requests in a tight loop, avoids unnecessary load and makes the CLI behave consistently on slow networks.

Device flow versus authorization code with PKCE

Decision factor Device flow Authorization code with PKCE
Browser on CLI host Not required; approval occurs on a second device. Normally needs a browser and a redirect back to the application.
Redirect channel None; the CLI polls. Required, using an app or localhost redirect.
User-code exposure Visible in the terminal and typed on another device, so display it only for the active request. No device code is typed, but the authorization response must reach the redirect listener.
Rate limits Polling must follow the returned interval; handle slow_down. No polling loop, although token and authorization endpoints still have limits.
Public clients Works without a confidential client secret. PKCE protects the authorization-code exchange and is generally preferred when a capable browser is available.
Best fit Headless hosts, SSH sessions, consoles, and constrained input devices. Native apps and desktop environments that can open a browser and receive a redirect.

CLI utilities are generally public clients. GitHub's guidance says authorization code with PKCE is preferable when the primary concern is protecting a client secret; a CLI cannot reliably keep such a secret private. Choose device flow because the browser or redirect channel is unavailable or inconvenient, not as a shortcut around a better browser-based design.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Security and token-storage checklist

  • Request the minimum scopes needed for the command.
  • Show the client name and requested permissions before the user approves.
  • Use HTTPS for every request and reject invalid certificates.
  • Never log device codes, user codes, access tokens, refresh tokens, or authorization responses.
  • Keep the device code and user code in memory only until the flow ends.
  • Store refresh and access tokens in the platform credential store when one is available.
  • Provide a logout or revoke command that removes local credentials and uses the provider's revocation mechanism when supported.
  • Clear tokens from memory where the language and runtime make that practical.

Troubleshooting common failures

The verification page says the code is invalid

Check that the user entered the current user_code, not the opaque device_code, and that the code has not expired. Request a new device code after expiry rather than retrying the old one.

Rank #3
Yubico - YubiKey 5 NFC - Multi-Factor authentication (MFA) Security Key and passkey, Connect via USB-A or NFC, FIDO Certified - Protect Your Online Accounts
  • POWERFUL SECURITY KEY: The YubiKey 5 NFC is the most versatile physical passkey, protecting your digital life from phishing attacks. It ensures only you can access your accounts
  • WORKS WITH 1000+ ACCOUNTS: Compatible with popular accounts like Google, Microsoft, and Apple. A single YubiKey 5 NFC secures 100+ of your favorite accounts, including email, password managers, and more
  • FAST & CONVENIENT LOGIN: Plug in your YubiKey 5 NFC via USB and tap it, or tap it against your phone (NFC), to authenticate. No batteries, no internet connection, and no extra fees required
  • MOST SECURE PASSKEY: Supports FIDO2/WebAuthn, FIDO U2F, Yubico OTP, OATH-TOTP/HOTP, Smart card (PIV), and OpenPGP. That means it’s versatile, working almost anywhere you need it
  • PRIMARY & SPARE KEYS: Just like having a spare house key, we recommend buying two YubiKeys - one for daily use and one as a spare. That way you’ll never get locked out of your accounts

The token endpoint always returns authorization_pending

The user has not completed approval, or the CLI is polling a different tenant or environment than the verification page. Confirm that both endpoints belong to the same provider registration and continue only at the returned interval.

You receive slow_down or HTTP 429

Your client is polling too frequently. Increase the delay, retain that larger interval for subsequent requests, and ensure multiple CLI processes are not sharing one device code.

The request is rejected as an invalid client

Verify the exact client identifier, registration status, tenant, and required content type. Do not add a client secret unless the provider explicitly documents one for this flow.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The CLI works locally but not over SSH

Remove any assumption that a local browser exists. Always print a copyable verification URI and code, and avoid relying on webbrowser.open for correctness.

Rank #4
Yubico - Security Key NFC - Basic Compatibility - Multi-Factor Authentication (MFA) Key, Connect via USB-A or NFC, FIDO Certified
  • POWERFUL SECURITY KEY: The Security Key NFC is the essential physical passkey for protecting your digital life from phishing attacks. It ensures only you can access your accounts.
  • WORKS WITH 1000+ ACCOUNTS: Compatible with Google, Microsoft, and Apple. A single Security Key NFC secures 100 of your favorite accounts, including email, password managers, and more.
  • FAST & CONVENIENT LOGIN: Plug in your Security Key NFC via USB-A and tap it, or tap it against your phone (NFC) to authenticate. No batteries, no internet connection, and no extra fees required.
  • TRUSTED PASSKEY TECHNOLOGY: Uses the latest passkey standards (FIDO2/WebAuthn & FIDO U2F) but does not support One-Time Passwords. For complex needs, check out the YubiKey 5 Series.
  • BUILT TO LAST: Made from tough, waterproof, and crush-resistant materials. Manufactured in Sweden and programmed in the USA with the highest security standards.

Tokens disappear after restart

The implementation is probably storing them only in process memory or a plain temporary file. Use the operating system credential store and handle refresh-token rotation according to the provider's documentation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and operational notes

  • Latency: Approval time dominates the flow. A polling interval of several seconds is normal; reducing it rarely improves the user's experience.
  • Retries: Retry transient network failures with bounded exponential backoff, but do not bypass the provider's minimum interval.
  • Cancellation: Let the user press Ctrl-C to stop polling and clearly state that the device authorization remains valid until its server-provided expiry.
  • Clock handling: Use a monotonic local timer for the waiting deadline so a system clock adjustment does not extend the authorization window.
  • Concurrency: Associate one polling loop with one device code. Parallel loops can waste rate-limit budget and produce confusing results.
  • Cost: The protocol itself does not define a universal per-request price. Any quotas, throttling, or account charges come from the identity provider's plan.

Or skip the browser setup:

If a CLI workflow also needs a screenshot of a consent, documentation, or status page, ScreenshotNeo provides a single HTTPS request instead of requiring you to install and operate a local browser. It is separate from OAuth authentication: use your identity provider for login and ScreenshotNeo for page capture. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

For the full parameter list, see ScreenshotNeo's API documentation. A direct call looks like this:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

There is a free allowance of 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; Growth is $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is included on every plan. Create a free ScreenshotNeo account to get started.

Implementation takeaway

Device flow is a focused solution for public CLI clients that cannot depend on a local browser. Register the client, request a device code, show the verification instructions, poll at the returned interval, and stop cleanly on denial or expiry. Use PKCE with authorization code flow instead when the application can reliably open a browser and receive a redirect.

Best Value
Yubico - Security Key C NFC - Basic Compatibility - Multi-Factor authentication (MFA) Security Key and passkey, Connect via USB-C or NFC, FIDO Certified (Pack of 2)
  • The information below is per-pack only
  • POWERFUL SECURITY KEY: The Security Key C NFC is the essential physical passkey for protecting your digital life from phishing attacks. It ensures only you can access your accounts.
  • WORKS WITH 1000+ ACCOUNTS: Compatible with Google, Microsoft, and Apple. A single Security Key C NFC secures 100 of your favorite accounts, including email, password managers, and more.
  • FAST & CONVENIENT LOGIN: Plug in your Security Key C NFC via USB-C and tap it, or tap it against your phone (NFC) to authenticate. No batteries, no internet connection, and no extra fees required.
  • TRUSTED PASSKEY TECHNOLOGY: Uses the latest passkey standards (FIDO2/WebAuthn & FIDO U2F) but does not support One-Time Passwords. For complex needs, check out the YubiKey 5 Series.

Frequently Asked Questions

Can a CLI use device flow without any browser at all?

The CLI itself need not include a browser, but the user still needs a secondary device with a browser or provider-approved interface to approve the request.

Is the device code the same as the user code?

No. The user code is entered on the verification page; the device code is an opaque value sent only by the CLI to the token endpoint.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should a device-flow client use a client secret?

Usually not. CLI utilities are public clients, so a distributed secret cannot be kept confidential. Follow the provider's registration rules and use PKCE when choosing authorization code flow.

What happens if the user never approves the request?

The CLI keeps polling until the server-provided expiry, then reports expiration and requires a new device authorization.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.