Skip to content

feat(api): single bearer auth, /v1/me, RFC 8628 device flow endpoints#527

Merged
DorianZheng merged 2 commits into
mainfrom
feat/auth-single-bearer-impl
May 14, 2026
Merged

feat(api): single bearer auth, /v1/me, RFC 8628 device flow endpoints#527
DorianZheng merged 2 commits into
mainfrom
feat/auth-single-bearer-impl

Conversation

@DorianZheng

Copy link
Copy Markdown
Member

Summary

OpenAPI spec change only. Three logical edits ship together because the wire contract is one coherent story:

  1. Replace dual securitySchemes with single BearerAuth (drops the OAuth2 client_credentials facade that collapsed to "client_secret IS the API key" — RFC 6749 §2.3.1 non-compliant). Description documents the four token sources the validation pipeline accepts: BoxLite-issued API keys, BoxLite-issued OAuth tokens, federated SSO JWTs, customer-issued gateway tokens.
  2. Add GET /v1/me + Principal schema (sub, principal_type, email, display_name, prefix, scopes, expires_at).
  3. Add /v1/oauth/device_code / /v1/oauth/token / /v1/oauth/revoke for RFC 8628 device flow + RFC 7009 revocation.

Device flow over loopback PKCE: BoxLite users run the CLI inside containers / SSH / remote dev pods where binding 127.0.0.1 fails. Vercel switched for the same reason in Sept 2025.

Stacked PRs

This is one of four PRs that together replace the original combined branch:

  • PR 1 (this one): spec only — feat/auth-single-bearer-impl
  • PR 2: Rust SDK + FFI bindings — feat/auth-rest-credential
  • PR 3: CLI auth login --webfeat/auth-cli-device-flow (stacks on PR 2)
  • PR 4: Axum / Python reference servers + NestJS controllers — feat/auth-server-stubs

Each PR is independently reviewable; only PR 3 has a hard merge dependency (on PR 2).

Test plan

  • Spec lints (spectral, openapi-cli validate)
  • Rendered docs preview shows the four token sources description
  • SDK / server PRs reference these endpoints by path + operationId

Aligns rest-sandbox-open-api.yaml with how comparable single-host SaaS
REST APIs actually publish bearer auth (Stripe / Resend / OpenAI /
GitHub PAT pattern). The OAuth2 client_credentials wrapper carried zero
information once the dashboard collapsed to a single secret, and the
spec's bearerFormat: JWT misdescribed the long-lived opaque key the
SDK actually sends.

Spec changes:
- Replace dual securitySchemes (BearerAuth JWT + OAuth2) with single
  BearerAuth (opaque dashboard key, no bearerFormat).
- Delete POST /oauth/tokens and its TokenRequest / TokenResponse
  schemas.
- Add GET /v1/me returning Principal { sub, principal_type, email,
  display_name, prefix, scopes, expires_at }. principal_type is an
  enum [user, service_account] so the discriminator is type-driven.
- Update info.description Authentication section + Authentication tag
  description.

Research backing (full reports under ~/.claude/plans/):
- 15-spec dual-auth survey: when an API serves both opaque-bearer and
  OAuth2 bearer, every observed case uses two distinct securitySchemes
  (Linode, Datadog, Cloudflare). bearerFormat: JWT with opaque keys
  has zero precedents.
- 9-impl OAuth2 client_id survey: RFC 6749 \xc2\xa72.3.1 + Auth0 / Okta /
  Keycloak / Cognito / Azure / Ory / HubSpot all require client_id.
  No major service collapses API key -> client_secret inside
  /oauth/token. Stripe / GitHub PAT / Postmark put the opaque key
  directly in Authorization: Bearer.
- 22-API identity-endpoint survey: /v1/me is principal-agnostic
  (matches Auth0 My Account, Spotify) and lives at the API root, above
  the {prefix} tenant segment.
- 15-repo OpenAPI organization survey: monolithic dominates below ~5k
  LOC; closest peer e2b (3344 LOC) is monolithic. Keep this file
  monolithic; revisit at ~3500 LOC or when SDK codegen lands.

Follow-ups (gated on backend):
- Backend implements GET /v1/me + accepts opaque key as Bearer on
  resource routes (tracked in project memory).
- SDK / CLI cleanup removes Credentials::ClientCredentials and
  --client-id / --client-secret-stdin once /me is live.
Adds the OAuth 2.0 device authorization grant + token revocation
endpoints to the BoxLite REST spec:
- POST /v1/oauth/device_code (RFC 8628 §3.1)
- POST /v1/oauth/token (RFC 6749 §4.4 + RFC 8628 §3.4)
- POST /v1/oauth/revoke (RFC 7009)

Device flow is preferred over RFC 8252 loopback PKCE for the dev-
workstation audience: BoxLite users routinely run the CLI inside
containers, SSH sessions, or remote dev pods where binding 127.0.0.1
for a callback fails (Vercel switched to device flow Sept 2025 for
this reason).

The BearerAuth scheme remains single (no bearerFormat) — its description
now documents the four token sources the validation pipeline accepts:
BoxLite-issued API keys, BoxLite-issued OAuth tokens, federated SSO
JWTs (per-deployment), and customer-issued gateway tokens (per-
deployment).

Spec-only PR. SDK / CLI / server implementations ship in follow-ups:
- feat/auth-rest-credential — Rust SDK + Python/Node FFI
- feat/auth-cli-device-flow — boxlite auth login --web
- feat/auth-server-stubs — Axum + Python reference servers + NestJS

References:
- RFC 8628 Device Authorization Grant
- RFC 7009 Token Revocation
- RFC 6750 §2.1 Bearer Token Usage
@DorianZheng DorianZheng merged commit b8fed57 into main May 14, 2026
10 checks passed
@DorianZheng DorianZheng deleted the feat/auth-single-bearer-impl branch May 14, 2026 15:37
DorianZheng added a commit that referenced this pull request May 15, 2026
PR #527 added /v1/oauth/device_code, /v1/oauth/token, /v1/oauth/revoke
endpoints + their request/response schemas to the spec. On review, those
spec entries turn out to be redundant and contract-drifting:

Zero code consumers in the BoxLite tree read the OAuth schemas:
- Python/Node SDKs expose only api_key (PyBoxliteRestOptions /
  JsBoxliteRestOptions). Go/C SDKs have no REST surface at all.
- The Rust SDK's refresh_oauth() in src/boxlite/src/rest/client.rs
  posts hand-coded RFC 8628 forms — never reads the spec.
- The CLI's commands::auth::device::login() hand-codes RFC 8628 forms.

Industry alignment — none of these put OAuth endpoints in their spec:
Stripe, GitHub, DigitalOcean, Anthropic, OpenAI. Only identity-provider
products (Auth0, Okta) co-locate. Their business is auth; ours isn't.

The wire format is fully defined by published IETF RFCs:
  - RFC 8628 §3.1 — device authorization request/response
  - RFC 6749 §4.4 / §6 — token exchange + refresh
  - RFC 7009         — token revocation

Restating those in OpenAPI adds maintenance churn with no precision
gain — implementers follow the RFC either way.

Contract-drift fix: PR #530's BoxliteOAuthController in apps/api returns
503 temporarily_unavailable on every /v1/oauth/* route because the real
OAuth server isn't built. With the endpoints out of the spec, the
gateway no longer promises something it can't deliver. When the
@node-oauth/node-oauth2-server backend lands, that work will add both
the controller and the spec paths in one coherent commit.

Spec changes:
- Remove /v1/oauth/device_code, /v1/oauth/token, /v1/oauth/revoke paths
- Remove DeviceAuthorizationRequest/Response, OAuthTokenRequest/Response,
  OAuthRevokeRequest, OAuthError schemas
- Update info.description to point at the IETF RFCs for the wire format
- BearerAuth.description: keep the four-token-source list; soften
  blo_/blr_ prefix descriptions to reference the RFCs not specific paths
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