Plugins

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.

auth.py
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:

auth-client.ts
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

OptionTypeDefaultDescription
expires_instr | int"30m"Code lifetime. Time string ("30m", "1h") or milliseconds.
intervalstr | int"5s"Minimum poll interval enforced server-side.
user_code_lengthint8Length of the human-entered code.
device_code_lengthint40Length of the device code.
verification_uristr | None/deviceAbsolute URL or relative path users visit to enter the code.
generate_device_code() -> strNoneCustom device-code generator.
generate_user_code() -> strNoneCustom user-code generator.
validate_client(client_id) -> boolNoneGate /device/code and /device/token by client id.
on_device_auth_request(client_id, scope) -> NoneNoneSide effect run when a device requests codes.

Schema

The plugin adds a deviceCode table:

deviceCode
id
stringrequired
Primary key.
deviceCode
stringrequired
Opaque code the device polls with.
userCode
stringrequired
Short code the user enters.
userId
stringoptional
Approving user, set after approval.
expiresAt
daterequired
When the codes expire.
status
stringrequired
pending | approved | denied.
pollingInterval
numberoptional
Per-code poll interval.
clientId
stringoptional
Requesting client id.
scope
stringoptional
Requested scope.
lastPolledAt
dateoptional
Last poll time, used for rate limiting.
createdAt
daterequired
Row creation time.
updatedAt
daterequired
Last update time.

A 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.