Skip to main content

Desktop GitHub browser sign-in

Linux and macOS local setup sign in through GitHub's OAuth device authorization flow before starting the local backend. Initial setup asks the user to open GitHub; Change GitHub account requests a fresh authorization even if another account was previously saved. No GitHub CLI, terminal emulator, global gh credentials, or running local API is needed for GitHub sign-in. The terminal-based propr login flow remains supported. Agent registry authentication is a separate operation and retains its existing handoff.

Provisioning (required)​

The distributor must register a ProPR-owned GitHub OAuth app, enable Device Flow, and supply its public client ID to the desktop build as PROPR_DESKTOP_GITHUB_CLIENT_ID. Use a dedicated OAuth app. Both token modes are supported: GitHub enables Expire user access tokens by default for new OAuth apps, and desktop stores and rotates the refresh token (see Expiring user tokens); apps with expiration disabled issue non-expiring tokens and need no refresh. Disabling expiration is no longer required. Do not supply a client secret, reuse GitHub CLI's client ID, or substitute a GitHub App registration. The requested scopes remain repo read:org, matching the existing enrollment login contract. This does not install the ProPR GitHub App or select additional repositories.

The client ID is public configuration, not a credential. An owner must provision the OAuth app and set the repository Actions variable PROPR_DESKTOP_GITHUB_CLIENT_ID for the desktop release and Linux preview workflows. Local package builders pass the same environment variable to npm run desktop:package (or the desktop make command). The main-process Vite build validates and embeds this public value; missing or malformed configuration fails even unsigned builds. No client secret is accepted or embedded by this configuration path. Syntactic validation cannot establish registration ownership or device-flow enablement: those remain owner responsibilities.

Packaged apps read the built constant, ignoring the launch environment, so Finder and application-menu launches need no shell configuration. Unpackaged development uses PROPR_DESKTOP_GITHUB_CLIENT_ID from its environment. There is no terminal fallback. No OAuth registration or account access is modified by these changes.

The configuration regression builds a small inspection entry through the actual main Vite configuration using an explicitly synthetic public ID. It checks missing-input failure and emitted configuration only; it is not a packaged Linux/macOS execution or live OAuth proof. Do not ship fixture IDs. After owner provisioning, inspect the actual unsigned package's main bundle for the configured public ID and perform the separate native acceptance below.

Protocol reference: GitHub's official device flow documentation. Device flow does not need a client secret. GitHub's authorization-code exchange still requires a secret, so desktop does not embed one. Existing ProPR Connect instance-login redemption requires an already-enrolled relay token; existing desktop pairing requires a running API. Neither is suitable for pre-enrollment sign-in.

User journey​

  1. In the native desktop sign-in dialog, optionally choose Open GitHub, or open https://github.com/login/device in another browser yourself. Enter the displayed one-time user verification code. Polling starts while the initial dialog is open; pressing Open GitHub is not required. The code remains visible until approval, cancellation, or expiry. Both dialogs offer Copy code, using the native clipboard only when requested.
  2. Check the account GitHub displays. To use another account, switch accounts on GitHub before approving. No account is inferred from the renderer or the operator's CLI session.
  3. Return to ProPR. Desktop independently fetches /user with the issued token and asks Continue as @…?. Confirm only the intended account. If wrong, cancel, retry sign-in, and switch accounts in GitHub. For issue #3008 live acceptance the authorized account is propr-ultrafix.
  4. Choose an installation from the refreshed, access-scoped discovery results. The existing separate enrollment permission check still applies. Installation is always a separate explicit action.

Choose Cancel sign-in in the native dialog while it is open. The modal dialog blocks interaction with the setup window; after it closes, setup cancellation remains available in the app. A refusal, browser failure, expiration, or connection error leaves an actionable retry outcome. An attempt has a maximum 15-minute lifetime (or GitHub's shorter expiration). Relaunch interrupts in-flight authorization; retry creates new codes. Previously saved credentials are replaced only after identity confirmation. No global gh credentials are read or written.

Failed account change​

If Change GitHub account does not complete (declined, cancelled, expired, unconfirmed, or a connection or provider failure), the installation chooser says that changing account did not complete and gives the safe reason. The account shown is labelled Previously saved GitHub account. ProPR did not replace it, and it is not used automatically. Installations cannot be selected, and nothing is enrolled, until the user either chooses Keep @… to continue with the saved account or Try changing account again. If the saved credential itself is no longer usable (for example, its refresh grant was revoked), setup stops with a sign-in recovery instead of showing the old identity.

Expiring user tokens​

GitHub's device flow returns either a non-expiring token or, when the OAuth app has Expire user access tokens enabled, an access token that lasts eight hours plus a single-use refresh token that lasts six months (GitHub documentation). Desktop validates either shape strictly. A partial or malformed expiring grant is rejected before identity confirmation, and nothing is stored.

  • Storage. After the user confirms their account, the access token, its absolute expiry, the refresh token, and the refresh expiry are saved together as one atomic replacement in the private desktop-local CLI config, the same storage used for non-expiring tokens. Replacing or clearing the token always removes the earlier refresh metadata, so an earlier account's refresh token can never be paired with a newly confirmed identity. propr config output never includes refresh tokens.
  • Use and refresh. Before relay discovery or enrollment, desktop refreshes a token that expires within five minutes. Refresh uses GitHub's device-flow refresh grant, which needs no client secret. Concurrent callers share one refresh because GitHub invalidates the old pair once it is used. The rotated pair is saved before it is used. If that save fails, the rotated pair is discarded and the user must sign in again. If the relay rejects an expiring token with 401 before its stated expiry, desktop refreshes once and retries. A refresh result from before an account change completed is discarded.
  • Revocation and expiry. If GitHub reports the refresh token as invalid or revoked, or the refresh token has expired, desktop deletes the saved credential and shows "Saved GitHub sign-in expired or was revoked". Setup then asks for a new browser sign-in. Transient refresh failures keep the saved credential and show fixed retry copy.
  • Restart. Expiry metadata persists, so a relaunched app refreshes or reuses the saved credential as appropriate.
  • Visual preview credential. CLI setup does not copy an expiring token into the backend as a visual-preview upload credential, because the backend could not renew it.

Refresh requests reject redirects. Provider error bodies and descriptions are dropped: only fixed codes (bad_refresh_token, invalid_grant, unauthorized_client, incorrect_client_credentials) are recognized.

Trust boundary​

The main process keeps the device secret, access token, and any refresh token in memory during authorization. Refresh tokens and expiry details never enter renderer state, IPC snapshots, logs, or CI evidence. The short user verification code is shown only in native UI (and copied to the system clipboard only on the native Copy code action), never in the renderer, IPC snapshots, logs, or URLs. Do not capture the native code dialog or GitHub authorization page in screenshots or recordings. The approved token uses the existing private desktop-local CLI config contract under the desktop user-data directory; it is not written into global CLI configuration.

Each attempt has a random nonce, its own cancellation signal, and a fresh GitHub device secret. GitHub's secret-bound device exchange provides the protocol's exchange protection; there is no authorization-code redirect and no PKCE verifier in this flow. Desktop accepts no callback, HTTP listener, or deep-link completion for it. Existing strict deep-link and external URL protections remain in place. Only the fixed HTTPS GitHub verification URL can be opened. Provider requests reject redirects, polling honors the server interval and cumulative slow_down, and expiry/deadline abort both requests and native UI. Late or superseded results cannot complete setup.

Acceptance still required​

Unit/host/UI fixtures establish protocol and lifecycle behavior, not live OAuth acceptance. A new source-bound Linux package and a native macOS package must exercise initial sign-in, changing an existing account to propr-ultrafix, verified account confirmation, installation discovery and separately authorized enrollment, cancellation, retry, browser-open failure, and relaunch. For issue #3025, validate the packaged Linux flow with an actual non-default browser: leave the initial native dialog open, use its displayed code in that browser, approve, and verify that the code dialog closes and the verified account confirmation appears without pressing Open GitHub. Also repeat after Copy code and check default-browser opening, cancellation, expiry, retry, and shutdown. This live check is required before release qualification. Run without gh or a terminal on PATH. Check keyboard use, narrow layouts, tablet/phone widths and 200% zoom; do not record codes or credentials. Use the suggested propr-ultrafix/propr-desktop-acceptance-20260909 fixture only after discovering its accessibility with the authorized identity.

No OAuth client was provisioned and no live or native packaged browser acceptance was performed as part of this implementation. The historical installed 0.9.0 source checkpoint does not establish acceptance for this change.

PR #3026 follow-up qualification​

A signed-preview observation reached Recovery without a saved desktop-local GitHub token after GitHub reported that the device was connected. That observation does not establish whether approval was late, OAuth failed separately, or the verified account was not confirmed. Browser approval alone is not evidence of completed desktop sign-in or relay enrollment.

The follow-up integration fixtures run the real browser login, native presenter lifecycle, desktop host actions, isolated config persistence, relay HTTP client, and setup controller. Approval is exercised both while the initial native dialog remains open and after advancing to Waiting for GitHub. Fixtures verify confirmation before persistence, persisted credentials before discovery, explicit installation selection before enrollment, and desktop retry after account rejection, OAuth denial, expiry, transport failure, and config-save failure. Network and native dialogs are simulated; unrelated Docker/backend actions are fixtures. Public snapshots and logs are checked for secret exclusion. These checks do not qualify a live package.

Requalify a newly built source-bound signed Linux package containing this follow-up. Record the package's source revision and digest using the existing release tooling. With a fresh isolated desktop config, repeat both approval while the initial dialog remains open in a non-default browser and approval after Open GitHub advances to Waiting for GitHub. Observe native account confirmation, confirm the intended account, verify only the boolean presence of a saved desktop-local credential, then discover and explicitly select an accessible installation and verify successful relay enrollment. Also reject account confirmation and retry from Recovery using Retry setup; verify the specific safe reason and successful completion on retry. Do not dump config, request bodies, headers, token values, device codes, or native authorization screens into diagnostics or capture evidence. A browser success page alone must not be recorded as a passing result.

This follow-up has not performed live signed-package requalification; it remains required before release qualification.

Issue #3036 expiring-token qualification​

Unit and integration fixtures cover strict parsing of expiring grants, refresh rotation, restart persistence, single-flight refresh, a relay 401 retry, revoked and expired refresh grants, refresh save failure, an account change during an in-flight refresh, and the failed-account-change chooser state. Before release, requalify the exact signed installed RPM and the macOS package against an OAuth app with Expire user access tokens enabled. Check initial sign-in, changing to propr-ultrafix, a failed change that leaves the previous @integry identity labelled as not replaced until Keep is chosen, and relaunch. Where practical, also check renewal after expiry (or a revoked authorization in GitHub settings followed by recovery). Record only boolean credential presence; never record token values, expiry timestamps tied to accounts, or provider responses. No live or packaged acceptance was performed as part of this change.