Skip to content

Support device-flow sign-in for local instances with persistent credentials #157

Description

@lavaman131

Support device-flow sign-in for local instances with persistent credentials

Context

Chopin already provides the browser workspace and an MCP interface for external coding agents. A local integration can start Chopin and direct users to the existing browser UI without duplicating either interface.

The remaining authentication friction is that each local deployment currently needs a GitHub App client secret. Reusing a shared App registration across independently operated local instances would require distributing that secret, while separate App registrations add onboarding steps for each deployment.

GitHub Apps support device authorization using a public client ID. Users approve a code on GitHub, and the local process receives user-scoped access and refresh tokens. Device-issued tokens can also be refreshed without the App client secret. App installation, organization policies, and the signed-in user's repository permissions still apply.

Some local environments do not have a usable operating-system credential store. Local sign-in should prefer secure storage, but its absence should not make sign-in impossible. This revises the original proposal's prohibition on plaintext fallback: allow a local plaintext credential file only after explaining the risk and receiving explicit consent. A warning alone is not consent.

Current behavior

  • apps/server/src/auth/config.ts requires GITHUB_APP_CLIENT_SECRET.
  • apps/server/src/auth/routes.ts implements browser authorization-code login and passes the client secret to token exchange and refresh.
  • apps/server/src/auth/session.ts keeps browser cookie verifiers and GitHub credentials in process memory.
  • Startup clears browser-session registry rows and Planner ownership after acquiring the database writer lease.

The restart behavior is an explicit security choice in #56. Enabling local authentication mode would introduce a credential-persistence boundary without changing hosted defaults. Within local mode, persistence is part of sign-in, not a separate user opt-in.

Proposal

Add an explicit local authentication mode in which device authorization and credential persistence form one sign-in flow:

  1. Device-flow browser sign-in. Show GitHub's verification URL and user code in Chopin, handle the authorization lifecycle, and validate identity and admission.
  2. Persist credentials before completing sign-in. Prefer the operating system's credential store. If it cannot be used, offer plaintext storage with the warning and consent behavior below. Create a normal Chopin session only after credentials are saved successfully.

There is no remember checkbox or separate session-only option in local mode. Declining or dismissing the plaintext-storage prompt cancels sign-in; it must not silently continue with an in-memory-only login. The user can retry after making secure storage available or choose plaintext storage on a new attempt.

The existing web OAuth flow and process-local credential behavior remain the default. App identifiers remain operator-configurable. This does not hardcode a particular App or coding-agent product into Chopin.

Device authorization

  • Enable device flow on the configured GitHub App. Local device mode requires its public client ID, not a client secret. Hosted authorization-code configuration remains unchanged.
  • The server requests a grant from POST /login/device/code and retains the private device_code. The browser receives only the user-facing code, verification URL, and safe attempt status. GitHub App permissions come from the App registration and installation, not a broad OAuth scope list.
  • Display the code and a clickable verification URL immediately. Start server-side polling independently of whether the user opens GitHub from Chopin. Approval in another browser or device must work.
  • Offer a "Copy code and open GitHub" action. Keep manual instructions visible if clipboard access or opening a new tab fails. Presentation failures must not cancel authorization.
  • Poll POST /login/oauth/access_token with the client ID, device code, and urn:ietf:params:oauth:grant-type:device_code grant type. Never poll faster than GitHub's advertised interval. On slow_down, respect the increased minimum interval, including the required five-second increase; any backoff cap must not undercut it.
  • Continue on authorization_pending. Treat denial and provider-reported expiry as terminal rather than polling until a generic timeout. Bound network requests and retries by the grant's expiry. Report malformed replies and unrecoverable provider errors without exposing secrets.
  • Cancel stops polling and makes a late response unusable. Retry creates a fresh attempt and code; it does not reuse a cancelled or expired grant.
  • Validate the authorized identity and admission before activating the account. Finish credential persistence before reporting local sign-in complete.

Use these status and instruction strings:

Waiting for device code...

Waiting for authorization...
Enter one-time code: {userCode} at {verificationUri}

Manual fallback messages:

Failed to open browser. Please visit {verificationUri} and enter the code {userCode} manually.

Failed to copy to clipboard. Please visit {verificationUri} and enter the code {userCode} manually.

For failures, show Error during sign-in: {safeErrorMessage} with Retry and Cancel controls. Local expiry uses Device code expired before authorization completed. Browser controls should not rely on terminal-style "press any key" instructions.

Credential persistence and storage fallback

The default storage preference is macOS Keychain, Windows Credential Manager, or a supported Linux secret service/keyring. Bound a credential-store attempt to five seconds so an unavailable or locked service cannot hang sign-in indefinitely.

Outcome Required behavior
Secure storage succeeds Complete local sign-in without writing a plaintext copy.
Secure storage is missing, inaccessible, disabled, fails, or times out Keep the pending credential server-side and request explicit plaintext consent. Do not activate the new account yet.
User accepts plaintext storage Save the credential in the local config file, then complete sign-in.
User declines or dismisses the prompt Discard the pending credential and cancel this sign-in without changing the active account or its persisted state.
Plaintext persistence fails Report the failure. Do not report successful sign-in or issue a new session.
A previously consented plaintext credential is restored Read it without repeating the storage-consent prompt, then revalidate or refresh it before issuing a session.

Use this warning and choice text verbatim, with visible warning styling:

System vault not available

The recommended secure storage (keychain, keyring, or credential manager) could not be found or accessed. You may need to install or configure one.

Storing the token in the config file saves it as plain text, which is insecure. If you decline, sign-in will be cancelled and no account state will be changed.

Store token in plain text config file?

Yes, store in plain text (insecure)
No, cancel sign-in

Show the actual local file path alongside the warning. Acceptance authorizes plaintext persistence for this login; it does not silently enable a global preference to store future accounts in plaintext. Missing interaction, a closed tab, or a cancelled prompt is not approval.

Store only the credential material needed for restoration, including refresh tokens and expiry metadata for expiring GitHub App tokens. Use atomic replacement and restrictive file access, including 0600 files and 0700 newly created directories on Unix, with appropriate access controls on other supported platforms. These restrictions do not encrypt the file or make plaintext equivalent to an OS vault. Do not put it in the repository or a shared application data directory.

Track the selected storage backend for refresh and cleanup. A later secure-store failure must not silently move credentials into plaintext. Logout must invalidate restoration and remove the relevant persisted credentials; stale copies or a delayed write must not revive the login.

Boundaries to preserve

  • Local mode is restricted to a loopback-bound server with an exact loopback origin. It is not an alternative authentication mode for an exposed multi-user deployment.
  • Device authorization and storage consent are bound to the initiating browser with an unpredictable, HttpOnly attempt cookie and origin checks. Learning an attempt ID is not sufficient to claim a grant or authorize persistence.
  • Persisted credentials are scoped to the local installation, origin, GitHub App, account, and browser binding. Finding a stored account is not sufficient to authenticate an arbitrary browser. Browser binding remains a security requirement even though there is no remember checkbox.
  • GitHub tokens do not enter PostgreSQL, browser storage, JavaScript-readable cookies, logs, or MCP responses. The only new disk-storage exception is the explicitly consented local credential file.
  • Token rotation, server crashes, concurrent requests, logout, revocation, and storage failures must not restore an invalidated login. Persist replacement credentials before reporting a successful durable rotation. Unrecoverable interrupted-refresh recovery must return to sign-in rather than reuse a stale refresh token.
  • Restored credentials still require current identity/admission checks and normal repository-installation/role checks. Restoring browser access does not reclaim Planner ownership or replay interrupted model work.
  • Existing MCP bearer authentication stays separate. No token-export endpoint or implicit conversion of MCP credentials into a browser session is proposed.

Expected user experience

First local sign-in
  Open Chopin → approve a code on GitHub
  → save credentials in the OS credential store → enter the workspace

Secure storage unavailable
  Show the warning and plaintext-storage choice
  → accept: save locally, then enter the workspace
  → decline or dismiss: cancel this sign-in; leave existing account state unchanged

After restart
  The returning browser proves its binding → credentials are revalidated/refreshed
  → issue a new normal Chopin session

After logout, revocation, or unrecoverable credential expiry/refresh failure
  Show sign-in again; do not silently recover an invalidated login

Validation expectations

  • Existing OAuth, MCP, repository authorization, and hosted restart tests continue to pass unchanged in default mode.
  • Device-flow tests cover pending authorization, polling intervals, repeated slow_down, denial, provider and local expiry, cancellation, retry, malformed replies, transient network failures, and late responses from superseded attempts.
  • Browser tests cover code/URL presentation, manual authorization, clipboard and tab-opening failures, and cross-browser/cross-origin attempts to claim a grant or approve storage.
  • Storage tests cover a working, missing, locked, failing, and timed-out OS credential store; exact warning and choice text; acceptance, decline, dismissal, and abandoned consent; failed plaintext writes; and no plaintext write before consent.
  • Local sign-in tests prove that successful persistence is required before activating the new account or issuing a session, and that no remember checkbox or session-only choice is offered. Declining or dismissing plaintext consent must cancel sign-in without activating the new account or falling back to an in-memory-only login.
  • Persistence and restoration tests cover a real server restart using each backend, refresh-token rotation, concurrent refreshes, logout races, interrupted persistence, delayed credential-store completion, and installation/origin/App/account/browser isolation.
  • Verify file permissions or access controls, token redaction, persisted-credential cleanup, and the absence of silent secure-to-plaintext migration.
  • Browser tests use production auth/session handlers with a fake GitHub network boundary. Native credential-store checks are distinguished from mocks and run on each claimed supported platform.

Scope and alternatives

This proposal does not include local service provisioning, a coding-agent startup hook, changes to the editor, an alternative database, automatic repository installation, or a new hosted authentication broker. Credential persistence is part of the local sign-in contract, not a separate optional feature.

Alternatives are retaining one App registration per deployment, requiring sign-in after every local restart, or operating a central OAuth service. The proposed mode avoids distributing a shared client secret while keeping authentication inside Chopin and allowing an informed storage choice on machines without a usable OS vault.

Related work and references

#151 mentions onboarding work. #56 establishes the current credential boundary; #80 establishes the separate MCP bearer path.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions