Device Authorization
OAuth 2.0 device-code login (RFC 8628) for TVs, CLIs, and limited-input clients.
The device authorization plugin implements the OAuth 2.0 Device Authorization
Grant (RFC 8628). A device requests a pair of codes, shows the user a short
user_code to enter on a second screen, and polls until the user approves or
denies the request.
Installation
Add the plugin to your server
Pass device_authorization() to your Kernia config. verification_uri is where
you tell users to enter their code.
from kernia import KerniaOptions
from kernia.auth import init
from kernia.plugins.device_authorization import device_authorization
auth = init(KerniaOptions(
database=adapter,
secret=os.environ["KERNIA_SECRET"],
base_url=os.environ["KERNIA_BASE_URL"],
plugins=[
device_authorization(
expires_in="30m",
interval="5s",
verification_uri="https://app.example.com/device",
),
],
))Add the client plugin
Add the deviceAuthorization client plugin:
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
baseURL: "/api/auth",
plugins: [deviceAuthorizationClient()],
});Usage
Request device and user codes (on the device)
const { data } = await authClient.device.code({
client_id: "my-tv-app",
scope: "openid profile",
});
// data: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }Display user_code and verification_uri to the user.
Poll for the token (on the device)
const { data, error } = await authClient.device.token({
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
device_code: deviceCode,
client_id: "my-tv-app",
});Poll at the returned interval. Until approval you get AUTHORIZATION_PENDING;
back off on POLLING_TOO_FREQUENTLY.
Look up a code (on the approval screen)
const res = await authClient.device({ query: { user_code: userCode } });The user must be signed in. This claims the code for the current session so it can be approved or denied.
Approve or deny
await authClient.device.approve({ userCode });
await authClient.device.deny({ userCode });Once approved, the device's next poll returns a session.
Options
| Option | Type | Default | Description |
|---|---|---|---|
expires_in | str | int | "30m" | Code lifetime. Time string ("30m", "1h") or milliseconds. |
interval | str | int | "5s" | Minimum poll interval enforced server-side. |
user_code_length | int | 8 | Length of the human-entered code. |
device_code_length | int | 40 | Length of the device code. |
verification_uri | str | None | /device | Absolute URL or relative path users visit to enter the code. |
generate_device_code | () -> str | None | Custom device-code generator. |
generate_user_code | () -> str | None | Custom user-code generator. |
validate_client | (client_id) -> bool | None | Gate /device/code and /device/token by client id. |
on_device_auth_request | (client_id, scope) -> None | None | Side effect run when a device requests codes. |
Schema
The plugin adds a deviceCode table:
iddeviceCodeuserCodeuserIdexpiresAtstatuspollingIntervalclientIdscopelastPolledAtcreatedAtupdatedAtA code must be claimed by a signed-in session (via authClient.device(...))
before it can be approved or denied — approving a code you haven't looked up
returns DEVICE_CODE_NOT_CLAIMED.