Skip to content

Add OAuth 2.0 Device Authorization Grant (RFC 8628)#95

Merged
KrisSimon merged 3 commits intomainfrom
feature/device-grant-2026
Mar 7, 2026
Merged

Add OAuth 2.0 Device Authorization Grant (RFC 8628)#95
KrisSimon merged 3 commits intomainfrom
feature/device-grant-2026

Conversation

@KrisSimon
Copy link
Copy Markdown
Contributor

Summary

Implements the OAuth 2.0 Device Authorization Grant (RFC 8628) for input-constrained devices such as CLIs, smart TVs, and IoT devices that cannot open a browser directly.

  • The device requests a short user code and a verification URI from the authorization server
  • The user visits /activate on any browser, logs in, and enters the code
  • The device polls the /token endpoint until the grant is approved

Changes

Refactoring (Phase 1)

  • AuthSession refactored from a flat struct to a discriminated union enum (AuthSession.code, .refresh, .device) — each case carries only fields relevant to its flow
  • AuthSession.CodeType replaced by AuthSessionType enum throughout the codebase
  • Backward-compatible: existing Redis sessions ("type": "code" / "type": "refresh") decode without migration

Device Grant (Phase 2)

Area Detail
Endpoint POST /oauth/device_authorization — issues device_code, user_code, verification_uri, expires_in, interval
Activation GET /activate — enter user code; POST /activate — authenticate and approve
Token polling POST /token with grant_type=device_code — returns authorization_pending (428), slow_down (429), access_denied (400), or tokens (200)
Storage DeviceSession in AuthCodeStorage; secondary Redis key deviceuser~{userCode} for fast lookup by user code
Client config New device_grant_config field (expires_in, interval, verification_uri) on ClientSpec
Well-known device_authorization_endpoint added to OpenID Connect discovery document
UI New activate.leaf template; translations in en_EN, de_DE, pt_PT
Metrics 4 new Prometheus counters (device_flow_initiation, device_code_authorized, device_code_denied, device_code_expired)
CRD device_grant_config schema added to crd-clients.yaml
E2E DeviceGrant.spec.ts + DeviceApp tenant/client fixtures

Test plan

  • ./tooling.sh test — all unit tests pass
  • ./tooling.sh lint — no violations
  • curl -X POST .../oauth/device_authorization -d 'client_id=...' → valid JSON response
  • Navigate to /activate, enter user code, log in → success page
  • Poll /token with device_code → transitions from authorization_pending to 200 with tokens
  • curl .../.well-known/openid-configuration → contains device_authorization_endpoint
  • ./tooling.sh e2e --filter "DeviceGrant" — all e2e scenarios pass

The JavaScript provider's credentials object now includes the OAuth2                                                                                             grant_type (serialised as grant_type in JSON), giving provider scripts visibility into how the authentication was initiated.

Changes:
- JSInputCredentials gains a grantType: GrantTypes field (JSON key
    "grant_type") via a CodingKeys mapping
- LoginController derives the grant type from the request mode
    (interceptor → .interceptor, otherwise → .authorization_code) and
    passes it to the provider
- TokenController password-grant handler passes .password
- All unit tests and test fixtures updated accordingly

E2E (Ham application):
- UserLoginProvider updated to deny interceptor logins by checking
  credentials.grant_type and calling commit(false) when it equals
  'interceptor'
- New test GrantType/GrantTypeCheck.spec.ts covers both cases:
  happy path (authorization_code login succeeds with a code in the
  redirect) and error path (interceptor login is denied and the login
  page is shown again with a WRONG_CREDENTIALS error)
Implement the full device grant flow for input-constrained clients  (CLIs, smart TVs, IoT devices) that cannot open a browser directly.

Core changes:
- Refactor AuthSession into a discriminated union enum (CodeSession, RefreshSession, DeviceSession) replacing the flat struct; backward-compatible with existing Redis-stored sessions via "type" discriminator
- Add DeviceSession with status (pending/authorized/denied), userCode, lastPolledAt for rate limiting, and optional payload
- Add getDevice(byUserCode:) and updateDevice(...) to AuthCodeStorage; Redis impl maintains a secondary "deviceuser~{userCode}" index
- POST /oauth/device_authorization — issues device_code + XXXX-XXXX user_code, stores pending session, respects DeviceGrantConfig
- GET/POST /activate — cross-tenant activation page; supports cookie auto-auth and credential-based auth via JavaScript provider
- POST /token with grant_type=device_code — polls session status, enforces slow_down rate limiting (429), returns token pair on success
- Advertise device_authorization_endpoint in /.well-known/openid-configuration when at least one tenant client supports device_code
- Add device_grant_config to Client CRD schema and Helm template
- Add 4 Prometheus counters: deviceFlowInitiation, deviceFlowAuthorized, deviceFlowPending, deviceFlowSuccess
- Add ACTIVATE.UI translations (en, de, pt) and autofocus username fields
- Fix Traefik Helm values: remove secretResourceNames from rbac (schema removed in latest chart release)
@KrisSimon KrisSimon merged commit 8317133 into main Mar 7, 2026
17 checks passed
@KrisSimon KrisSimon deleted the feature/device-grant-2026 branch March 7, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant