AUTH4D-58 per-application device settings and branding

The device flow from AUTH4D-16 (repository file docs/api-spec/auth4d-16.md) now applies each native application’s device settings and branding, configured through AUTH4D-56 (repository file docs/api-spec/auth4d-56.md). Values are resolved per tenant and client (resolveApplicationPresentation); unset fields take DEFAULT_DEVICE_SETTINGS, so unconfigured applications behave exactly as before. Databases without migration 0008 also use the defaults.

Start a device authorization

POST /device/authorization?tenant_id={tenantId} is unchanged except for the response values. The response validates against ConfiguredDeviceAuthorizationResponseSchema:

Field Source Default
user_code device_settings.user_code_length (8–12) 8
expires_in device_settings.code_ttl_seconds (120–1800) 600
interval device_settings.poll_interval_seconds (5–60) 5
{
  "device_code": "dc_…",
  "user_code": "7K2Q9ZP4XM",
  "verification_uri": "https://auth.auth4.dev/device/verify?tenant_id=ten_…",
  "verification_uri_complete": "https://auth.auth4.dev/device/verify?tenant_id=ten_…&user_code=7K2Q9ZP4XM",
  "expires_in": 300,
  "interval": 15
}

The effective settings are stored on the device-code record when the code is issued. Changing an application’s settings affects only codes issued afterwards; an in-flight code keeps its lifetime and polling interval.

Polling

POST /oauth/token with the device-code grant enforces the interval returned at issue time. A poll earlier than the interval returns 400 slow_down and adds five seconds to that code’s interval (RFC 8628 §3.5), up to 600 seconds. The code expires with expired_token after its configured lifetime.

Verification data

GET /device/verify?tenant_id={tenantId}&client_id={clientId}&user_code={userCode} with Accept: application/json returns DeviceVerifyResponseSchema. branding now holds the application’s configured branding (only the fields that are set; {} when none are). A stored accent color that no longer meets 4.5:1 contrast against white is omitted.

{
  "tenant": { "tenant_id": "ten_…", "display_name": "Studio" },
  "application": { "client_id": "cli_…", "display_name": "My game" },
  "branding": {
    "accent_color": "#1d4ed8",
    "support_url": "https://support.example.com/help",
    "privacy_policy_url": "https://example.com/privacy"
  },
  "user_code": "7K2Q9ZP4XM",
  "scopes": ["openid", "profile"],
  "browser_flow_id": "flw_…",
  "csrf_token": "…"
}

user_code is accepted case-insensitively at any configurable length (8–12, matching ConfiguredDeviceUserCodeSchema). POST /device/approval and POST /device/denial accept the same lengths (ConfiguredDeviceApprovalRequestSchema). A code only matches the record it was issued for, so accepting longer codes does not widen what a guess can reach. The device CSRF cookie’s Max-Age is now the code’s remaining lifetime instead of a fixed 600 seconds.

The response keeps Cache-Control: no-store. Branding is public presentation data and contains no secrets.

Source: docs/api-spec/auth4d-58.md