Device and offline access

Use this API family to create an offline-access grant under an existing principal, collect the approved token package, refresh its bundle, and manage the resulting grant records.

This is not the public OAuth device-authorization start for a relying party. For that standards-facing flow, call POST /device_authorization on M7 SSO. The api.user routes below require an existing bearer token.

Authentication

Send a consumer-user bearer token:

Authorization: Bearer <ACCESS_TOKEN>

Issue, pickup, refresh, acknowledgement, search, and view also support the restricted personal owner-proxy machine profile described in the authorization guide. Organization application tokens are not accepted by this family, and revoke always requires a human consumer-user session.

Issue, pickup, refresh, and acknowledgement calls operate in the current principal context. Use the same principal that created the grant when polling, refreshing, acknowledging, viewing, or revoking its record.

Endpoint summary

Route Required fields Notes
/oauth/device/issue client_id Starts an offline-access grant under the current principal.
/oauth/device/pickup id; client_secret for a confidential client Polls the grant after approval.
/oauth/device/refresh/bundle Refresh bundle Refreshes or rotates a saved token bundle.
/oauth/device/acknowledge/refresh/bundle Pending bundle Activates an ACK-mode refresh package.
/oauth/device/revoke id; password when revoking a live server-backed refresh session Optional force for an eligible record that cannot be invalidated upstream. Machine credentials cannot revoke through this route.
/oauth/device/view id Returns a grant record. Client-credentials records use the corresponding issued-record view.
/oauth/device/search None Returns both current-user and accessible-application grant sets. Request filters are not applied.
/oauth/device/search/app None Grant records for an application-management view.
/oauth/device/search/user None Grant records for the current user; supports pagination.

Start an offline-access grant

Request fields

Field Required Notes
client_id Yes Public OAuth client ID.
client_secret Conditional Required for confidential clients.
scope No Requested scopes; prefer a space-delimited string.
aud No Requested audience, as a string or array of strings.
access_expires No Requested access-token lifetime in seconds.
refresh_expires No Requested refresh-token lifetime in seconds.
claims No Legacy claim object used when split claim fields are absent.
access_claims No Requested access-token claim object.
refresh_claims No Requested refresh-token claim object.
fingerprint No Optional binding value.
label No Display label stored with the offline-access record.
display_type No iframe (default) or popup. Use the returned verification URLs unchanged.

Requested token lifetime and claim values are subject to the OAuth application's configured policy.

Request example

curl -sS https://api.user.m7.org/api/v2/oauth/device/issue \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "client_id": "PUBLIC_CLIENT_ID",
    "scope": "openid profile offline_access",
    "aud": "https://api.example.m7.org",
    "access_expires": 3600,
    "refresh_expires": 2592000,
    "access_claims": {
      "installation": "cli"
    },
    "refresh_claims": {
      "grant_kind": "offline_access"
    },
    "label": "Example CLI",
    "display_type": "popup"
  }'

Success response

The result contains the information needed to render the user step and control polling. It returns display_type in both verification URLs.

{
  "status": 1,
  "data": {
    "id": "OFFLINE_ACCESS_ID",
    "status": "pending",
    "client_id": "PUBLIC_CLIENT_ID",
    "requested_scope": "openid profile offline_access",
    "requested_aud": ["https://api.example.m7.org"],
    "user_code": "ABCD-EFGH",
    "verification_uri": "https://sso.user.m7.org/device_login?display_type=popup",
    "verification_uri_complete": "https://sso.user.m7.org/device_login?user_code=ABCD-EFGH&display_type=popup",
    "expires_in": 600,
    "interval": 5
  }
}

Poll for pickup

Call /oauth/device/pickup after the user completes the returned verification URL. Poll no more frequently than interval and stop after expires_in.

curl -sS https://api.user.m7.org/api/v2/oauth/device/pickup \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "OFFLINE_ACCESS_ID"
  }'

Send client_secret again when the application is confidential.

A grant that is still awaiting approval is a successful API response with a pending grant state, not necessarily an HTTP error:

{
  "status": 1,
  "data": {
    "id": "OFFLINE_ACCESS_ID",
    "status": "pending",
    "interval": 5,
    "comment": "authorization pending"
  }
}

After approval, data contains the token package and grant metadata. A revoked, closed, or abandoned grant returns a grant state and invalid_grant details; do not continue polling it.

{
  "status": 1,
  "data": {
    "id": "OFFLINE_ACCESS_ID",
    "status": "success",
    "client_id": "PUBLIC_CLIENT_ID",
    "token_type": "bearer",
    "access_token": "ACCESS_TOKEN",
    "expires_in": 3600,
    "refresh_token": "REFRESH_TOKEN",
    "refresh_expires_in": 2592000,
    "binding_chain": "BINDING_CHAIN",
    "binding_link": "BINDING_LINK",
    "scope": "openid profile offline_access",
    "aud": ["https://api.example.m7.org"]
  }
}

Refresh a token bundle

/oauth/device/refresh/bundle refreshes a previously issued bundle. Send bundle as an object or JSON string. It may contain refresh_token, binding_chain, binding_link, and client_id. Top-level refresh_token, binding_chain, and binding_link override values in bundle.

Field Required Notes
bundle Yes, unless all bundle values are sent at top level Current bundle object or JSON string.
client_id Conditional Required unless it is already in bundle.client_id.
client_secret Conditional Required for confidential clients.
fingerprint No 64-character hexadecimal string.
new_fingerprint No 64-character hexadecimal string; defaults to fingerprint when omitted.
refresh_mode No Omit for the SSO default, strict_rotation. Allowed values: static, strict_rotation, grace_rotation, ack, ack_supersede_pending.

Refresh example

curl -sS https://api.user.m7.org/api/v2/oauth/device/refresh/bundle \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "client_id": "PUBLIC_CLIENT_ID",
    "client_secret": "CLIENT_SECRET",
    "refresh_mode": "ack",
    "bundle": {
      "client_id": "PUBLIC_CLIENT_ID",
      "refresh_token": "REFRESH_TOKEN",
      "binding_chain": "BINDING_CHAIN",
      "binding_link": "BINDING_LINK"
    }
  }'

A successful normal refresh returns data.success: 1, a replacement bundle, and token_state: "active". Replace stored credentials only when the returned token state is active.

An ACK refresh returns a successful pending package:

{
  "status": 1,
  "data": {
    "success": 1,
    "status": "pending",
    "token_state": "pending",
    "bundle": {
      "client_id": "PUBLIC_CLIENT_ID",
      "refresh_token": "REFRESH_TOKEN",
      "token_state": "pending",
      "activation": {
        "id": "ACTIVATION_ID",
        "secret": "ACTIVATION_SECRET"
      },
      "predecessor_bundle": {
        "client_id": "PUBLIC_CLIENT_ID",
        "refresh_token": "PREVIOUS_REFRESH_TOKEN"
      }
    }
  }
}

Keep this exact pending bundle until it is acknowledged. Do not replace the currently active stored bundle with it yet.

Acknowledge a pending refresh

Send the pending bundle from an ACK refresh to /oauth/device/acknowledge/refresh/bundle.

curl -sS https://api.user.m7.org/api/v2/oauth/device/acknowledge/refresh/bundle \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "bundle": {
      "client_id": "PUBLIC_CLIENT_ID",
      "token_state": "pending",
      "activation": {
        "id": "ACTIVATION_ID",
        "secret": "ACTIVATION_SECRET"
      }
    }
  }'

On success, the response changes token_state to active and removes activation and predecessor_bundle. If acknowledgement fails, retain the original pending package so it can be retried.

Search, view, and revoke

Route Request fields Notes
/oauth/device/search None Returns both data.user and data.app record sets. The user set is constrained to the current user's principal with principal_type: "user"; the app set is constrained to OAuth applications owned by that user.
/oauth/device/search/app None Returns device-grant records for OAuth applications owned by the current user.
/oauth/device/search/user Shared pagination only Returns the current user's device-grant records. The handler does not accept caller-selected id, status, client_id, or principal filters.
/oauth/device/view id Returns one record.
/oauth/device/revoke id; conditional password; optional force Closes or revokes the selected grant for an eligible user principal.

For a live record with a refresh-token session ID, also send password (or its pass alias) so the service can revoke the upstream session. If the record has refresh material but no revocable session ID, the request fails unless force: true; forced handling marks the local record abandoned without claiming upstream invalidation.

Use the shared pagination contract for list results.