Get started
API scopes
Integration guides
Features
Troubleshooting
Device code flow
The Device Authorization Grant (RFC 8628) lets a device that can't easily host a browser — a smart TV, a CLI, a printer's setup screen — sign a user in via a second device. The constrained device displays a short code and a verification URL; the user opens that URL on their phone or laptop, signs in to Kenni, and the device polls the token endpoint until Kenni hands it tokens.
The device flow is a paid feature on a higher plan tier. Get in touch and we'll talk you through pricing and enable it for your team.
Setup
Create a Device application in the developer portal. Device applications:
- have no
client_secret(they can't keep one), - don't use redirect URIs — verification happens on a separate device,
- only support the
urn:ietf:params:oauth:grant-type:device_codeandrefresh_tokengrants.
The application's Overview tab lists the device-authorization endpoint and the activation URL the user will visit.
Endpoints
Two endpoints in addition to the standard token endpoint:
- Device authorization —
https://idp.kenni.is/<team-domain>/oidc/device/auth. The constrained device POSTs here to start the flow. - Activation page —
https://idp.kenni.is/activate. The user opens this on a second device and enters theuser_code.
Both are advertised in the discovery document under device_authorization_endpoint and device_verification_uri.
Flow
1. Start the flow
The device POSTs to device_authorization_endpoint with its client_id and the requested scope:
curl -s -X POST https://idp.kenni.is/<team-domain>/oidc/device/auth \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=@<team-domain>/device' \
-d 'scope=openid offline_access' \
-d 'ui_locales=en'
Response:
{
"device_code": "xN7pJ...long-opaque-string",
"user_code": "ABCD-EFGH",
"verification_uri": "https://idp.kenni.is/activate",
"verification_uri_complete": "https://idp.kenni.is/activate?user_code=ABCD-EFGH",
"expires_in": 600,
"interval": 5
}
user_code— the short code the user types on the activation page.device_code— the opaque token the device polls with. Treat as a secret while the flow is active.verification_uri_complete— the URL with the code prefilled. Render it as a QR code if you can; the user scans it instead of typing.expires_in— total flow lifetime in seconds. Defaults to 10 minutes.interval— minimum poll cadence in seconds.
2. Show the user where to go
Display user_code and verification_uri (or a QR code linking to verification_uri_complete). The user opens it on a phone or laptop and signs in to Kenni — the same login UI as a normal authorization request, including any delegation or consent screens that apply.
3. Poll for the token
While the user is finishing on the second device, the constrained device polls the token endpoint at interval seconds:
curl -s -X POST https://idp.kenni.is/<team-domain>/oidc/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
-d 'device_code=xN7pJ...' \
-d 'client_id=@<team-domain>/device'
Polling responses follow RFC 8628 §3.5:
| HTTP status | error | What to do |
|---|---|---|
| 400 | authorization_pending | User hasn't approved yet. Wait interval and poll again. |
| 400 | slow_down | You're polling too fast. Increase interval by 5s. |
| 400 | expired_token | The 10-minute window elapsed. Start over from step 1. |
| 400 | access_denied | The user declined. Don't poll again. |
| 200 | none | You have access_token, id_token, and refresh_token. |
Refresh tokens are issued automatically for device clients — the offline_access scope is implied — so the device can stay signed in across reboots without making the user repeat the activation step.
Reference implementation
The test app includes a working device-code flow you can drive from a UI:
apps/test/src/app/api/device-code/route.ts— starts the flow.apps/test/src/app/api/device-code/poll/route.ts— polls the token endpoint with the returneddevice_code.
Both are short enough to copy into your own service as-is.