Skip to content

Add OAuth2 (PKCE + device code) login alongside personal access tokens#28

Open
AlexVanderbist wants to merge 2 commits into
mainfrom
oauth
Open

Add OAuth2 (PKCE + device code) login alongside personal access tokens#28
AlexVanderbist wants to merge 2 commits into
mainfrom
oauth

Conversation

@AlexVanderbist
Copy link
Copy Markdown
Member

Summary

  • flare login now defaults to a browser PKCE flow against Passport. --device runs the device-code flow for headless terminals (and is the auto-fallback when stdin is non-interactive). --token preserves the legacy paste-a-PAT UX.
  • Stored OAuth records refresh transparently before each API call via CredentialStore::getAccessToken(). spatie/laravel-openapi-cli@1.3.0's new retryOn hook acts as a 401 safety net via CredentialStore::forceRefresh().
  • Per-host credential storage now accepts both legacy strings and OAuth record objects. Existing PAT users keep working until they choose to flare login again — no migration needed.
  • flare logout --all wipes every host in one go.

What landed

  • app/Services/OAuth/: TokenRecord (value object), PkceCodes (RFC 7636 verifier/challenge/state), OAuthEndpoints, OAuthHttpClient, TokenRefresher, LocalCallbackServer (stream_socket_server on 127.0.0.1:0 with inline success/error HTML — PHAR-safe), PkceLoginFlow, DeviceLoginFlow, DeviceAuthorization + DevicePollResult DTOs, OAuthException.
  • config/flare.php: client ID + scopes config. Default client UUID is the dev seed value — see launch blocker below.
  • CredentialStore: extended with getRecord() / setRecord() / getAccessToken() / forceRefresh() / flushAll(), with flock-guarded read-modify-write around refresh.
  • AppServiceProvider: bound the new OAuth services and wired ->auth() to getAccessToken() and ->retryOn() to forceRefresh().
  • LoginCommand refactored: dispatches by mode flag, falls back to device flow on non-interactive stdin, warns when --token replaces an existing OAuth session.
  • 93 Pest tests (+46 new), all passing. PHPStan clean. Pint clean.

⚠️ Launch blocker — production OAuth client UUID

config/flare.php ships the dev Flare CLI Passport client UUID (9d000000-0000-4000-8000-000000000001) as the default for FLARE_OAUTH_CLIENT_ID. The production client UUID must be substituted there before tagging a release. The CLI honors the env var at runtime so staging/local can keep overriding via FLARE_OAUTH_CLIENT_ID=… — but the baked-in default is what end users will hit.

Grep for 9d000000 in config/flare.php to spot it. Worth considering a CI grep that fails on a release branch if the placeholder is still present.

Server-side dependencies

  • Passport Flare CLI client must be seeded with loopback redirect URIs (http://127.0.0.1/callback, http://[::1]/callback, flare://oauth/callback) — landed on passport-oauth branch.
  • ApproveAuthorizationController and DenyAuthorizationController must convert Inertia redirects to Inertia::location() (otherwise the XHR can't cross HTTPS → HTTP loopback). Landed on passport-oauth branch.

Test plan

  • OAuth storage shape (mixed with legacy strings) — CredentialStoreTest
  • PKCE flow happy path (state CSRF, code exchange, scope/URL params) — PkceLoginFlowTest
  • Local callback server: real socket, request parsing, success/error HTML — LocalCallbackServerTest
  • Device flow: pending → success, slow_down increases interval, fatal aborts, expiry throws — DeviceLoginFlowTest
  • Token exchange + refresh + refresh-token rotation — OAuthHttpClientTest
  • Proactive refresh threshold — TokenRefresherTest
  • CredentialStore::getAccessToken() refresh + write-back + no-op skip — CredentialStoreTest
  • retryOn(401 → forceRefresh) wiring — OpenApiRegistrationTest
  • LoginCommand: --token, PKCE, non-TTY fallback to device, error paths — LoginCommandTest
  • LogoutCommand: per-host + --allLogoutCommandTest
  • Manual: PKCE end-to-end against passport-oauth.test
  • Manual: proactive refresh (forced expires_at) and retryOn 401 (mangled access_token) against real Passport
  • Manual: --token with both modern (Passport PAT) and legacy SHA-256 tokens
  • Manual: device flow end-to-end (verified at the unit/integration boundary; physical browser approval not yet exercised)
  • Manual: non-TTY auto-fallback to device flow end-to-end (same reason)

`flare login` now defaults to a browser-based PKCE flow against Passport,
storing refreshable OAuth records per host. `--device` runs the device-code
flow for headless terminals (auto-fallback when stdin is non-interactive).
`--token` preserves the legacy paste-a-token UX. Stored OAuth tokens refresh
transparently before each API call, with a `retryOn`-driven safety net for
401s using the openapi-cli 1.3.0 hook. `flare logout --all` wipes every host.
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