---
title: "Sign in devices without a keyboard through · Agent Wranglers"
url: https://agentwranglers.se/playbook/accounts/sign-in-methods/device-sign-in
updated: 2026-10-21
licence: "All rights reserved"
---

# Sign in devices without a keyboard through a second screen

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers). Updated 2026-10-21.

- Level: Optional
- Chapter: [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
- Rule: For TVs, command-line tools and other devices without a handy browser, use the OAuth 2.0 Device Authorization Grant from RFC 8628. Never ask for a password on the device.
- How: The device asks for a device code and a short user code, then shows the code and a short address, or a QR code. The person opens the address on a phone, signs in there with a passkey or second factor, and confirms the code. Meanwhile the device polls the token endpoint. RFC 8628 says it waits the given `interval`, or 5 seconds if none is given, and adds 5 seconds after each `slow_down`. Make user codes such as `WDJB-MJHT` from 20 consonants. Rate limit code entry and give codes a short life, because RFC 8628 warns about remote phishing with them. Show the device name on the confirm page. Each device then appears in [list every signed-in device](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/session-list).
- Check: A test starts a device sign-in, polls before the interval and gets `slow_down`. After the person confirms on the second screen, the next poll returns tokens. A wrong user code entered 5 times in a row is blocked.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
const DEFAULT_INTERVAL_SECONDS = 5;
const SLOW_DOWN_STEP_SECONDS = 5;
const sleep = seconds => new Promise(resolve => setTimeout(resolve, seconds * 1000));

export async function waitForDeviceApproval(tokenUrl, clientId, { device_code, interval = DEFAULT_INTERVAL_SECONDS }) {
  let waitSeconds = interval;
  for (;;) {
    await sleep(waitSeconds);
    const response = await fetch(tokenUrl, {
      method: 'POST',
      body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code, client_id: clientId }),
    });
    const body = await response.json();
    if (response.ok) return body;
    if (body.error === 'slow_down') waitSeconds += SLOW_DOWN_STEP_SECONDS;
    else if (body.error !== 'authorization_pending') throw new Error(body.error);
  }
}
```

### Python example

```python
import time
import httpx

DEFAULT_INTERVAL_SECONDS = 5
SLOW_DOWN_STEP_SECONDS = 5

def wait_for_device_approval(token_url: str, client_id: str, device: dict) -> dict:
    wait_seconds = device.get("interval", DEFAULT_INTERVAL_SECONDS)
    while True:
        time.sleep(wait_seconds)
        response = httpx.post(token_url, data={
            "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
            "device_code": device["device_code"],
            "client_id": client_id,
        })
        body = response.json()
        if response.is_success:
            return body
        if body["error"] == "slow_down":
            wait_seconds += SLOW_DOWN_STEP_SECONDS
        elif body["error"] != "authorization_pending":
            raise RuntimeError(body["error"])
```

## Sources

- [RFC 8628 OAuth 2.0 Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628)

## Related rules

- [List every signed-in device](https://agentwranglers.se/playbook/accounts/sessions-and-security-activity/session-list) in Accounts

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/accounts/sign-in-methods/device-sign-in">Agent Wranglers accounts playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Weigh email links before making them the only way in](https://agentwranglers.se/playbook/accounts/sign-in-methods/email-sign-in-trade-offs)
- Next: [Never remove the last way to sign in](https://agentwranglers.se/playbook/accounts/sign-in-methods/last-sign-in-method)
- [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
