## Device flow The Device Authorization Grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) lets users authenticate on input-constrained devices — smart TVs, CLI tools, IoT devices, and set-top boxes — without typing credentials directly on the device. The device displays a short code and a URL; the user opens the URL on a phone or laptop, signs in, and approves access. The device polls in the background and receives tokens once the user approves. ### Prerequisites Prerequisites - A Frontegg workspace with OAuth enabled. - An OAuth application configured in your Frontegg dashboard. - Your `application ID`. - Your Frontegg domain (e.g. `https://app-xxxx.frontegg.com`). br All API calls in this guide use your Frontegg domain as the base URL: ``` https://{your-domain}/frontegg/oauth ``` ### How it works The device flow has three participants: - **Device app** (you build) — the TV, CLI, or IoT application that initiates the flow and polls for tokens. - **Verification page** (you build) — a page in your web application where the user signs in and approves or denies the request. - **Frontegg APIs** — the backend that manages device codes, user verification, and token issuance. ``` Device App Frontegg APIs Your Verification Page | | | | 1. POST /device/authorize | | |------------------------------->| | | { device_code, user_code, | | | verification_uri, ... } | | |<-------------------------------| | | | | | Display code + URL pointing | | | to YOUR verification page | | | | | | | 2. User opens your page | | |<-------------------------------| | | 3. User signs in | | |<-------------------------------| | | 4. GET /device?user_code= | | |<-------------------------------| | | { appName, scopes, status }| | |------------------------------->| | | 5. POST /device/verify | | | { approved: true } | | |<-------------------------------| | | | | 6. POST /token (polling) | | |------------------------------->| | | { access_token, id_token, | | | refresh_token } | | |<-------------------------------| | ``` br Important: you must build your own verification page The `verification_uri` returned by the API is **not** a user-facing web page, it points to a Frontegg JSON API endpoint. **Do not display it to users or encode it in a QR code.** Frontegg provides the backend APIs for the device flow but does not host a verification UI. You must build a verification page in your own application (see Step 2) and display its URL to the user instead. ### Step 1: Request device authorization The device initiates the flow by requesting a device code from the following public endpoint (no authentication required): ```http POST /frontegg/oauth/device/authorize Content-Type: application/json ``` br **Request body:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `client_id` | string | Yes | Your OAuth application client ID | | `scope` | string | No | Space-separated list of scopes | **Example:** ```bash curl -X POST https://{your-domain}/frontegg/oauth/device/authorize \ -H "Content-Type: application/json" \ -d '{"client_id": "your-client-id", "scope": "openid profile email"}' ``` br **Response (200 OK):** ```json { "device_code": "a1b2c3d4e5f6...", "user_code": "BCKF-DHLM", "verification_uri": "https://app-xxxx.frontegg.com/oauth/device", "verification_uri_complete": "https://app-xxxx.frontegg.com/oauth/device?user_code=BCKF-DHLM", "expires_in": 1800, "interval": 5 } ``` | Field | Description | | --- | --- | | `device_code` | Opaque token the device uses to poll for access tokens. Keep this secret — never expose it to the user. | | `user_code` | Short human-readable code (`XXXX-XXXX` format) displayed to the user for verification. | | `verification_uri` | **Do not use.** Points to a Frontegg JSON API endpoint, not a web page. See note below. | | `verification_uri_complete` | Same as `verification_uri`, with `user_code` appended. **Do not use.** | | `expires_in` | Seconds until the device code expires (default: 900 / 15 minutes). | | `interval` | Minimum seconds to wait between polling requests (default: 5). | Important: display your own verification page URL `verification_uri` and `verification_uri_complete` point to a Frontegg JSON API endpoint, not a user-facing page. Displaying them to users or encoding them in a QR code will not work. Instead, direct users to your own verification page (see Step 2) and pass the `user_code` as a query parameter: ``` Go to: https://myapp.com/device-activate Enter code: BCKF-DHLM ``` Or as a QR code URL: ``` https://myapp.com/device-activate?user_code=BCKF-DHLM ``` br After receiving the response: - Display `user_code` prominently on screen. - Direct users to your verification page URL (not `verification_uri`). - Store `device_code` locally — you'll need it to poll for tokens. - Use `interval` as the minimum delay between polling requests. ### Step 2: Build the verification page Host a page in your application where authenticated users can review and approve or deny device access requests. This page calls the Frontegg device APIs on behalf of the signed-in user. Using the Frontegg SDK The verification page can be a protected route in your application using a Frontegg SDK (`@frontegg/react`, `@frontegg/nextjs`, `@frontegg/angular`, `@frontegg/vue`). When the route is behind the SDK's authentication guard, unauthenticated users are automatically redirected to login and back — no manual token handling required. br Your verification page should follow this flow: 1. Host the page at a URL such as `/device-activate`. 2. Read `user_code` from the URL query string (`?user_code=BCKF-DHLM`), or prompt the user to enter it manually. 3. Authenticate the user. If using the Frontegg SDK on a protected route, this happens automatically. 4. Fetch device info to display the app name and requested scopes. 5. Show a confirmation UI with the app name, scopes, and user code, then prompt the user to approve or deny. 6. Submit the decision to Frontegg. 7. Show a result screen telling the user to return to their device. #### Get device info Retrieve details about the device request to display to the user before they approve. ```http GET /frontegg/oauth/device?user_code={user_code} Authorization: Bearer {access_token} Accept: application/json ``` **Response (200 OK):** ```json { "appName": "My TV App", "scopes": "openid profile email", "status": "pending" } ``` | Field | Description | | --- | --- | | `appName` | Name of the application requesting access. | | `scopes` | Requested scopes, or `undefined` if none were requested. | | `status` | `pending`, `approved`, or `denied`. Only show the approve/deny UI when `pending`. | #### Approve or deny ```http POST /frontegg/oauth/device/verify Authorization: Bearer {access_token} Content-Type: application/json ``` **Request body:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `user_code` | string | Yes | The code displayed on the device. | | `approved` | boolean | Yes | `true` to approve, `false` to deny. | **Example:** ```bash curl -X POST https://{your-domain}/frontegg/oauth/device/verify \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOi..." \ -d '{"user_code": "BCKF-DHLM", "approved": true}' ``` **Response:** `200 OK` with no body returned on success. #### Complete verification page example The following self-contained HTML/JS example demonstrates the full verification page flow. It includes a simple login form for illustration. In production, protect this route with the Frontegg SDK so authentication is handled automatically, and replace the manual login with the SDK's `getAccessToken()`. Replace `BASE_URL` with your Frontegg domain (e.g. `https://app-xxxx.frontegg.com`). ```html Authorize Device

Authorize Device

Loading...

``` ### Step 3: Poll for tokens (device side) After displaying the code, the device polls the token endpoint until the user approves, denies, or the code expires. This is a public endpoint — no authentication required. ```http POST /frontegg/oauth/token Content-Type: application/json ``` **Request body:** | Field | Type | Required | Description | | --- | --- | --- | --- | | `grant_type` | string | Yes | `urn:ietf:params:oauth:grant-type:device_code` | | `device_code` | string | Yes | The `device_code` received in Step 1 | | `client_id` | string | Yes | The same `client_id` used in Step 1 | **Example:** ```bash curl -X POST https://{your-domain}/frontegg/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "a1b2c3d4e5f6...", "client_id": "your-client-id" }' ``` **Success response (200 OK):** ```json { "access_token": "eyJhbGciOi...", "id_token": "eyJhbGciOi...", "refresh_token": "550e8400-e29b-41d4-a716-446655440000", "token_type": "Bearer", "expires_in": 86400 } ``` **Polling error responses (400):** While the user hasn't yet acted, the endpoint returns `400` with one of the following `error` values. Continue polling for `authorization_pending` and `slow_down`; stop for all others. | `error` | Meaning | Action | | --- | --- | --- | | `authorization_pending` | User hasn't acted yet | Wait `interval` seconds, then poll again | | `slow_down` | Polling too fast | Increase interval by 5 seconds, then retry | | `access_denied` | User denied the request | Stop polling — show a denied message | | `expired_token` | Device code has expired | Stop polling — restart the flow from the beginning | | `invalid_grant` | Client ID mismatch or code already used | Stop polling — the device code is invalid | Error response format: ```json { "error": "authorization_pending", "errors": ["authorization_pending"], "statusCode": 400 } ``` #### Polling implementation ```javascript async function pollForTokens(deviceCode, clientId, intervalSeconds) { let interval = intervalSeconds * 1000; while (true) { await sleep(interval); const response = await fetch('https://{your-domain}/frontegg/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: deviceCode, client_id: clientId, }), }); if (response.ok) { return await response.json(); // { access_token, id_token, refresh_token, ... } } const error = await response.json(); switch (error.error) { case 'authorization_pending': continue; case 'slow_down': interval += 5000; continue; case 'access_denied': throw new Error('User denied the request'); case 'expired_token': throw new Error('Device code expired — restart the flow'); default: throw new Error(`Unexpected error: ${error.error}`); } } } ``` ### Complete device-side example The following example shows the full device-side flow — requesting a device code and polling for tokens. The verification page (Step 2) runs separately in your web application. ```javascript const FRONTEGG_DOMAIN = 'https://app-xxxx.frontegg.com'; const CLIENT_ID = 'your-client-id'; //Your applcation ID const VERIFICATION_PAGE = 'https://myapp.com/device-activate'; async function deviceSignIn() { const authResponse = await fetch(`${FRONTEGG_DOMAIN}/frontegg/oauth/device/authorize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: CLIENT_ID }), }); const auth = await authResponse.json(); // Direct users to your verification page — not to verification_uri console.log(`Go to: ${VERIFICATION_PAGE}`); console.log(`Enter code: ${auth.user_code}`); const tokens = await pollForTokens(auth.device_code, CLIENT_ID, auth.interval); console.log('Access token:', tokens.access_token); console.log('Refresh token:', tokens.refresh_token); return tokens; } ``` ### OIDC discovery The device authorization endpoint is advertised in the OIDC discovery document: ```http GET /frontegg/oauth/.well-known/openid-configuration ``` Look for: - `device_authorization_endpoint` — the device authorization endpoint URL. - `grant_types_supported` — should include `urn:ietf:params:oauth:grant-type:device_code`. ### Refresh tokens Use the `refresh_token` from the token response with the standard OAuth refresh flow: ```bash curl -X POST https://{your-domain}/frontegg/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "refresh_token", "refresh_token": "550e8400-e29b-41d4-a716-446655440000", "client_id": "your-client-id" }' ``` ### Security notes - **`device_code` is secret.** Never expose it to the user or include it in URLs. Only `user_code` is shown to the user. - **Codes are short-lived.** Device codes expire after `expires_in` seconds (default: 30 minutes). - **Single use.** Once tokens are issued, the device code is invalidated. Any subsequent exchange attempt will fail. - **Client ID binding.** The `client_id` used when polling must match the one used in the initial request. - **Respect rate limits.** Polling faster than `interval` returns `slow_down`. Back off by 5 seconds each time you receive this error. ### Error handling summary | Scenario | HTTP status | Error | | --- | --- | --- | | Missing or empty `client_id` | 400 | Validation error | | Invalid `client_id` | 400 | `invalid_client_id` | | User hasn't acted yet (polling) | 400 | `authorization_pending` | | Polling too fast | 400 | `slow_down` | | User denied | 400 | `access_denied` | | Device code expired or not found | 400 | `expired_token` | | Client ID mismatch on poll | 400 | `invalid_grant` | | Missing auth on `GET /device` | 401 | Unauthorized | | Missing auth on `POST /device/verify` | 401 | Unauthorized | | OAuth not enabled for workspace | 404 | Feature not enabled |