Skip to main content

TV Sign-In with the Device Flow

Typing an email address and a password with a TV remote is slow and error-prone. The device flow (RFC 8628) moves the typing to the member's phone:

  1. The TV asks Subrite for a code, then shows a QR code, a short URL and the code.
  2. The member points their phone camera at the QR code, or opens the URL and types the code.
  3. On the phone, the member signs in if they aren't already, sees your app's name and the code, and approves.
  4. The TV polls Subrite every few seconds while this happens. The first poll after the member approves returns the tokens.

The TV gets the same tokens as the web flow: same issuer, same API audience, same sub. A token from the TV works everywhere a web token does.


Before You Start​

Get the Device Flow Enabled for Your Client​

The device flow is enabled per OIDC client. For now, ask your Subrite contact to enable it. We will enable it on stage first so you can test, and then on production.

A TV app is set up as a public client: it sends its client_id and no client_secret. Anyone can extract a secret from an app installed on a TV, so a secret would prove nothing (RFC 8628 §5.6). A public client that sends a client_secret anyway is refused with invalid_client.

If your platform can only authenticate with a secret, tell us, and we will keep your client confidential. Then send client_id and client_secret on every request on this page, the same way as in the web flow.

Endpoints and Discovery​

Every tenant has its own issuer on its own domain. The examples on this page use https://example.com; replace it with your tenant's domain. Request the code, poll for tokens, refresh and revoke on the same domain: a code from one tenant does not work on another.

Read the endpoints from the tenant's discovery document instead of hardcoding them:

GET https://example.com/.well-known/openid-configuration
{
"issuer": "https://example.com/api/oidc",
"device_authorization_endpoint": "https://example.com/api/oidc/device/auth",
"token_endpoint": "https://example.com/api/oidc/token",
"revocation_endpoint": "https://example.com/api/oidc/token/revocation"
}

The response is shortened here. device_authorization_endpoint and revocation_endpoint are only present once the device flow is enabled for a client on the tenant. If they are missing, the device flow isn't enabled yet.


1. Request a Code​

Endpoint​

POST /api/oidc/device/auth

Headers​

Content-Type: application/x-www-form-urlencoded
Accept: application/json

Parameters​

ParameterRequiredTypeDescription
client_idYesstringYour Client ID.
scopeYesstringopenid offline_access
device_nameNostringA name for the device, for example Living room TV. At most 64 characters.
device_modelNostringThe device's model, for example Samsung QE55Q80. At most 64 characters.
Request offline_access

Without offline_access, the TV's tokens are tied to the member's browser session on the phone. They stop working when that session ends, for example when the member signs out on the phone.

The member sees device_name and device_model on the approve screen, marked as reported by the device. A value longer than 64 characters, or one containing control characters or invisible characters, is refused with invalid_request. Don't send the device's IP address or location: Subrite observes those itself. Any other parameter is ignored.

Example Request​

curl --request POST 'https://example.com/api/oidc/device/auth' \
--header 'Accept: application/json' \
--data-urlencode 'client_id=your-client-id' \
--data-urlencode 'scope=openid offline_access' \
--data-urlencode 'device_name=Living room TV' \
--data-urlencode 'device_model=Samsung QE55Q80'

Response​

{
"device_code": "Sx8mJ0p2lW7bqC3vK9tYc1fN6uR4hZ5aE2dG8iL0oPq",
"user_code": "482 913",
"verification_uri": "https://example.com/activate",
"verification_uri_complete": "https://example.com/activate?user_code=482%20913",
"expires_in": 900,
"interval": 5
}
FieldDescription
device_codeThe TV's secret for this sign-in. Send it when polling. Never show it, never put it in a URL and never log it.
user_codeThe code the member enters on the phone: 6 digits, shown as 482 913. Show it exactly as you received it.
verification_uriThe short URL where the member types the code.
verification_uri_completeThe same URL with the code included. This is what the QR code contains.
expires_inSeconds until the code expires: 15 minutes. After that, request a new code.
intervalSeconds to wait between polls of the token endpoint.

Errors​

errorMeaningWhat to do
invalid_clientThe client_id is unknown, or a public client sent a client_secret.Check the client ID, and send no secret.
invalid_requestThe device flow isn't enabled for your client, or a parameter is missing or invalid. error_description says which.Ask us to enable the device flow, or fix the parameter.
server_errorSomething failed on our side.Safe to retry: request a new code.

If no client on the tenant has the device flow enabled, the endpoint does not exist and answers 404.


2. Show the Code on the TV​

The TV sign-in screen: a QR code on the left; the instruction, the short URL and the code on the right

  • Render verification_uri_complete as a QR code. It is a plain https:// URL; put it in the QR code exactly as you received it. Don't wrap it in a custom URL scheme, a link shortener or a redirect of your own.
  • Show verification_uri and user_code as text next to the QR code, large enough to read from the sofa. Typing the code is a first-class path, not a fallback: not every member will scan.
  • Write "Point your phone camera at the code". Never "Scan with a QR app". The phone's own camera opens the link directly. Third-party QR scanner apps have intercepted sign-in flows like this one with paywalls and look-alike pages.
  • Show the code exactly as user_code gives it. The phone shows the same code on the approve screen, and the member compares the two before approving. That comparison is what stops a member from approving someone else's TV, so the code must stay on screen even when the member scans.
  • When the code expires, request a new one and show it, or offer a button to do so.

3. Poll for the Tokens​

While the code is on screen, poll the token endpoint with the device_code.

Endpoint​

POST /api/oidc/token

Headers​

Content-Type: application/x-www-form-urlencoded
Accept: application/json

Parameters​

ParameterRequiredTypeDescription
grant_typeYesstringurn:ietf:params:oauth:grant-type:device_code
device_codeYesstringThe device_code from step 1.
client_idYesstringYour Client ID.

Example Request​

curl --request POST 'https://example.com/api/oidc/token' \
--header 'Accept: application/json' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
--data-urlencode 'device_code=Sx8mJ0p2lW7bqC3vK9tYc1fN6uR4hZ5aE2dG8iL0oPq' \
--data-urlencode 'client_id=your-client-id'

The Polling Contract​

Wait interval seconds before the first poll and between polls. Send one poll at a time: wait for the answer before you start the next wait. Until the member has approved, the token endpoint answers 400 with one of these errors:

errorMeaningWhat to do
authorization_pendingThe member hasn't approved yet.Keep polling.
slow_downYou polled before the interval was up.Add 5 seconds to your interval, for this poll and all later ones, and keep polling.
expired_tokenThe code expired before the member approved.Stop polling. Request a new code and show it.
access_deniedThe member rejected the sign-in on the phone.Stop polling. Tell the member the sign-in was declined, and let them start again.
invalid_grantThe device code is unknown, belongs to another client, or was already used.Stop polling. Request a new code and show it.

If an answer isn't JSON, or has status 429 or 5xx, treat it as temporary: double your interval and keep polling until the code expires.

Stop polling once you have the tokens

A device code can be exchanged for tokens once. Polling again with the same device code after success is refused with invalid_grant, and it also ends the sign-in: the refresh token you just received stops working, and the TV is signed out when its access token expires.

Response​

The first poll after the member approves returns the tokens:

{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
"expires_in": 14400,
"id_token": "eyJhbGciOiJSUzI1NiJ9...",
"refresh_token": "aR7kQ2nV9xL4mB8cT1wJ5yH3pF6sD0gE...",
"scope": "openid offline_access",
"token_type": "Bearer"
}

4. Use the Tokens​

From here the TV is an ordinary OIDC client.

Refresh the Access Token​

Refresh the same way as in the web flow, described in Get Refresh Token: POST /api/oidc/token with grant_type=refresh_token, refresh_token and client_id. A public client sends no client_secret.

Refresh tokens rotate on every use

Every refresh response contains a new refresh token, and the one you sent stops working. Store the refresh token from every token response and discard the previous one.

If an old refresh token is used again, Subrite treats it as stolen: it revokes the whole sign-in and answers invalid_grant. The TV is signed out, and the member has to sign in again.

  • Save the new refresh token before you do anything else with the response.
  • Never refresh twice in parallel with the same refresh token, for example from two threads or two app processes.

When a refresh answers invalid_grant, the sign-in has ended: it was revoked, it expired, or an old refresh token was reused. Delete the stored tokens and start the device flow again from step 1.


5. Sign Out​

To sign the TV out, revoke its refresh token. This ends the sign-in on Subrite, so the refresh token can't be used again.

Endpoint​

POST /api/oidc/token/revocation

Headers​

Content-Type: application/x-www-form-urlencoded

Parameters​

ParameterRequiredTypeDescription
tokenYesstringThe current refresh token.
token_type_hintNostringrefresh_token
client_idYesstringYour Client ID.

Example Request​

curl --request POST 'https://example.com/api/oidc/token/revocation' \
--data-urlencode 'token=aR7kQ2nV9xL4mB8cT1wJ5yH3pF6sD0gE...' \
--data-urlencode 'token_type_hint=refresh_token' \
--data-urlencode 'client_id=your-client-id'

The endpoint answers 200 with an empty body, also when the token was already invalid.

Then delete all stored tokens on the TV. The access token is a JWT and stays valid until it expires (at most 4 hours), so the TV must stop using it itself.

The browser logout endpoint, /api/oidc/session/end, does not apply to a TV: there is no browser session to end.


Checklist​

  • The device flow is enabled for your client (ask us), on stage before production.
  • Endpoints come from /.well-known/openid-configuration.
  • The code request includes scope=openid offline_access, and no client_secret.
  • The TV shows the QR code, the URL and the code, and says "Point your phone camera at the code".
  • Polling waits interval seconds, adds 5 seconds on slow_down, and stops on success or on any error other than authorization_pending and slow_down.
  • Every new refresh token is stored, and the previous one is discarded.
  • Signing out revokes the refresh token and deletes all tokens on the TV.