Device Code RP Setup

This document describes the full device-code flow for another site / RP that wants to offer offline access.

Example user story:

  • user is on bigfs.m7.org
  • user clicks Get offline access
  • bigfs starts the device flow with SSO
  • user approves on sso.user.m7.org
  • bigfs redeems the device code and stores the returned token package

What this flow is for

Use this when:

  • the RP wants long-lived API access outside the normal browser session
  • the RP wants to issue a token package after the user explicitly approves it
  • the RP does not want to push browser login credentials through the RP itself

Client setup

The OAuth client used for this flow must:

  • be active
  • allow device_code in grant_types

Typical confidential RP setup:

  • token_endpoint_auth_method = client_secret_post
  • grant_types includes device_code

Typical public CLI setup:

  • token_endpoint_auth_method = none
  • grant_types includes device_code

High-level flow

  1. User clicks Get offline access on the RP.
  2. RP backend calls SSO POST /device_authorization.
  3. SSO returns device_code, user_code, and verification URLs.
  4. RP stores device_code.
  5. RP shows the user where to approve.
  6. User goes to sso.user.m7.org/device_login.
  7. User enters the code and approves.
  8. RP redeems device_code at POST /token.
  9. SSO returns the token package.
  10. RP stores the returned refresh material for later use.

Step 1: User clicks on the RP

Example UI:

  • button text: Get offline access
  • helper text: Approve a device token for CLI or background uploads

At this point, the RP should create a server-side action that starts the flow.

For a confidential client, do this on the RP backend, not in browser JavaScript.

Step 2: RP calls device authorization

Endpoint:

POST https://sso.user.m7.org/device_authorization

Content type:

application/x-www-form-urlencoded

Required parameters

Always send:

  • client_id

Usually send:

  • scope

Optional:

  • aud May be sent once for a single audience, or repeated to request multiple audiences.

Confidential client auth

Send one of:

  • client_id + client_secret
  • Authorization: Basic ...

depending on the configured token_endpoint_auth_method.

Public client auth

If the client is public:

  • send client_id
  • do not send a secret

Example request

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

client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&scope=openid%20profile%20email%20groups%20offline_access

Multi-audience example:

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

client_id=<CLIENT_ID>&aud=https%3A%2F%2Fuser.m7.org%2Fapi%2Fv2%2F&aud=https%3A%2F%2Ffiles.m7.org%2Fapi%2Fv1%2F

Step 3: SSO returns device authorization data

Current response shape:

{
  "device_code": "<DEVICE_CODE>",
  "user_code": "<USER_CODE>",
  "verification_uri": "https://sso.user.m7.org/device_login",
  "verification_uri_complete": "https://sso.user.m7.org/device_login?user_code=<USER_CODE>",
  "expires_in": 600,
  "interval": 5
}

Step 4: RP stores device state

The RP should store at least:

  • device_code
  • user_code
  • verification_uri
  • verification_uri_complete
  • expires_in
  • interval
  • local status like pending

For a confidential RP:

  • keep device_code on the server side
  • do not rely on the browser to preserve it

Step 5: RP tells the user how to approve

The RP should show:

  • the user_code
  • a clickable approval URL

Best UX:

  • show verification_uri_complete as a button
  • also show the plain user_code in case the user types it manually

Example text:

  • Go to sso.user.m7.org/device_login and enter code ABCD-EFGH

Step 6: User approves on SSO

User goes to:

  • https://sso.user.m7.org/device_login

Current SSO approval behavior:

  • user enters user_code
  • user enters password
  • email may be left blank if the user already has an SSO session and SSO can resolve the account from that session

The RP does not receive the password.

Step 7: RP waits for approval

The RP has two reasonable choices:

  • poll /token
  • wait for the user to click a local Continue button, then try /token

Current SSO behavior supports polling.

If the approval has not happened yet, /token returns:

  • authorization_pending

Use the returned interval from device authorization as the minimum polling interval.

Step 8: RP redeems the device code

Endpoint:

POST https://sso.user.m7.org/token

Required form fields:

  • grant_type=device_code
  • device_code=<DEVICE_CODE>

Confidential clients also send:

  • client_id
  • client_secret

or use Basic auth.

Example request

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

grant_type=device_code&device_code=<DEVICE_CODE>&client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>

Step 9: SSO returns the token package

On success, SSO returns:

{
  "token_type": "bearer",
  "access_token": "<ACCESS_TOKEN>",
  "expires_in": 900,
  "refresh_token": "<REFRESH_TOKEN>",
  "refresh_expires_in": 2592000,
  "binding_chain": "<BINDING_CHAIN>",
  "binding_link": "<BINDING_LINK>",
  "scope": "openid profile email groups offline_access"
}

This is the one-time pickup response for that device_code.

Step 10: RP stores the returned token material

The RP should store:

  • refresh_token
  • refresh_expires_in
  • binding_chain
  • binding_link
  • granted scope

The access token may be used immediately, but the important long-lived material is the refresh-side package.

After initial pickup

Later refreshes go through the normal /token refresh path, not the device-code path.

That means the RP later sends:

  • grant_type=refresh_token
  • refresh_token
  • binding_chain
  • binding_link

plus normal client authentication for that client.

One-time behavior

The device_code pickup is one-time.

After a successful /token response:

  • the device authorization is consumed
  • the same device_code should not be expected to work again

Recommended RP state machine

Suggested local states:

  • pending
  • approved_waiting_pickup
  • redeemed
  • expired
  • failed

Suggested transitions:

  • after /device_authorization -> pending
  • after user approval is expected -> still pending locally, until /token succeeds
  • after successful /token -> redeemed
  • after timeout / expiry -> expired

Common errors

Device not approved yet

Response:

{
  "error": "authorization_pending",
  "error_description": "device authorization is still pending"
}

Wrong client redeems the code

Response:

  • invalid_grant

Expired or already-used device code

Response:

  • invalid_grant

Client not allowed for device flow

Response:

  • unauthorized_client

Simple RP implementation recipe

  1. Add an RP backend action called something like startOfflineAccess().
  2. In that action, call POST /device_authorization.
  3. Save the returned device_code server-side.
  4. Show user_code and verification_uri_complete to the user.
  5. Poll POST /token every interval seconds.
  6. Stop polling when SSO returns the token package.
  7. Store the returned refresh-side material.
  8. Use normal refresh flow from then on.

Confidential RP rule

If the RP is a confidential client:

  • browser starts the action
  • RP backend talks to SSO
  • browser shows status
  • RP backend redeems the token

The browser should not hold the client secret and should not be the final /token caller.

1