Skip to content

OAuth code can be consumed before local credential persistence succeeds #36

Description

@jumski

Summary

The callback path in src/server/auth.ts exchanges the authorization code with the broker before proving that the resulting credentials can be persisted locally. If the local write fails, the flow rejects after the code has already been consumed.

This does not silently corrupt local state, but it produces a bad recovery path: auth succeeded upstream, the single-use code is burned, and the user has to start over.

Evidence

src/server/auth.ts

  • 155-164: exchangeCodeThroughBroker(...) consumes the code and returns tokens
  • 166-171: computes expiry and calls writeSavedAuth(input, { ... })
  • 173: resolves the pending auth only after the write succeeds
  • 184-198: any failure in this block rejects the auth flow and returns an error page

So the ordering is:

  1. exchange succeeds
  2. local persistence happens
  3. success is reported

If step 2 fails, the code from step 1 is already spent.

Reproduction

  1. Start Supabase OAuth.
  2. Before completing the callback, make the target auth store unwritable.
    • example: permissions problem on .opencode/
    • read-only filesystem
    • disk full / write failure
  3. Complete the browser callback.

Expected

Either:

  • persistence feasibility is checked before the browser flow begins, or
  • the failure path gives a clear recovery message with minimal wasted work

Actual

The callback fails after the code is exchanged, so the user must repeat the full authorization flow.

Impact

  • Bad recovery UX when local persistence fails.
  • Repeated browser auth flows for what is effectively a local filesystem problem.
  • Harder debugging because the visible failure appears during OAuth completion, but the root cause is local persistence.

Suggested Fix

Recommended layered fix:

  1. Preflight the auth store path before starting the browser flow.
    • verify parent directory can be created
    • verify target path is writable
  2. Use an atomic write strategy for the auth file.
  3. Improve the failure message to explicitly say:
    • authorization succeeded
    • credentials could not be saved locally
    • user should fix permissions/storage and retry

This issue may not be fully eliminable because the OAuth code is inherently single-use, but it can be made much less painful and much easier to understand.

Acceptance Criteria

  • Browser auth does not start if the store path is clearly unwritable up front.
  • Auth-file writes are atomic.
  • Persistence failure after exchange produces a targeted recovery message instead of a generic auth failure.
  • Regression coverage exists for write failure during callback persistence.

Notes

This is lower severity than the callback-context bug or refresh race, but it is still worth tracking because it creates an avoidable failure mode at the end of a successful OAuth flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p2Valid but lower urgencystatus:triagedReviewed and ranked

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions