Skip to content

fix(oauth): recover missing client registrations - #135

Open
Waishnav wants to merge 8 commits into
mainfrom
codex/oauth-client-registration-recovery
Open

fix(oauth): recover missing client registrations#135
Waishnav wants to merge 8 commits into
mainfrom
codex/oauth-client-registration-recovery

Conversation

@Waishnav

@Waishnav Waishnav commented Aug 6, 2026

Copy link
Copy Markdown
Owner

ChatGPT reconnect can restart OAuth with its cached client ID. If DevSpace loses the corresponding SQLite client row during local development, authorization stops at invalid_client, forcing the connector to be deleted and configured again.

New public client registrations now use an authenticated, self-contained client ID backed by a private signing key. A missing registration is reconstructed transiently, checked against the current redirect allowlist, and persisted only after successful Owner password approval. Access and refresh tokens remain opaque server-side state. Registrations created before this change need one normal re-registration before this recovery path applies.

Summary by CodeRabbit

  • New Features

    • OAuth client registrations can be recovered after database loss or redirect revalidation with Owner approval.
    • Valid signed registrations automatically restore unknown clients.
    • New setups generate stable registration keys, while existing setups retain compatibility.
  • Security

    • Client identifiers are protected against tampering, excessive size, and unsupported client types.
    • Access and refresh tokens remain opaque, server-side, and revocable.
  • Documentation

    • Added guidance for registration keys, recovery requirements, and compatibility behavior.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 48f127c6-b258-4f2e-8e9b-d12a71fd0e19

📥 Commits

Reviewing files that changed from the base of the PR and between b315386 and e67e3c0.

📒 Files selected for processing (7)
  • docs/security.md
  • src/oauth-client-registration.test.ts
  • src/oauth-client-registration.ts
  • src/oauth-provider.ts
  • src/oauth-store.test.ts
  • src/oauth-store.ts
  • src/user-config.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/oauth-client-registration.test.ts
  • src/oauth-store.ts
  • src/oauth-provider.ts
  • docs/security.md
  • src/user-config.ts

📝 Walkthrough

Walkthrough

The change adds configurable OAuth client registration keys, signed recoverable client IDs, SQLite restoration for missing registrations, provider integration, and tests and documentation for recovery and legacy compatibility.

Changes

OAuth client recovery

Layer / File(s) Summary
Registration key configuration
src/user-config.ts, src/config.ts, src/cli.ts, src/config.test.ts, docs/configuration.md, docs/security.md
The configuration stores, derives, or generates the client registration key. It validates a minimum length and preserves legacy compatibility behavior.
Recoverable client ID format
src/oauth-client-registration.ts, src/oauth-client-registration.test.ts
Public client registrations use bounded, base64url-encoded payloads with HMAC-SHA256 signatures. Recovery validates the payload, signature, decoded data, and public-client fields.
Store and provider recovery flow
src/oauth-store.ts, src/oauth-provider.ts, src/oauth-store.test.ts, package.json
SQLite stores generate recoverable IDs with UUID fallback and restore missing clients after redirect-host validation. The provider restores unknown clients before authorization code issuance. Tests cover approval, rejection, persistence, and policy validation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OAuthProvider
  participant SqliteOAuthClientsStore
  participant recoverClientRegistration
  participant SqliteOAuthStore
  OAuthProvider->>SqliteOAuthClientsStore: authorize with client ID
  SqliteOAuthClientsStore->>recoverClientRegistration: recover client ID with key
  recoverClientRegistration-->>SqliteOAuthClientsStore: validated client registration
  SqliteOAuthClientsStore->>SqliteOAuthStore: restore client after redirect validation
  SqliteOAuthStore-->>OAuthProvider: available client registration
  OAuthProvider-->>OAuthProvider: issue authorization code
Loading

Poem

A rabbit signs the client ID,
Then finds lost records where they hide.
Keys persist and tokens stay,
Tests check every recovery way.
SQLite restores the trail,
OAuth codes then pass the rail.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary OAuth client registration recovery change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/oauth-client-registration-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds HMAC-authenticated, self-contained identifiers for public OAuth clients so missing SQLite registrations can be recovered after owner approval.

  • Generates, persists, and configures a dedicated client-registration signing key, with deterministic compatibility derivation for older configurations.
  • Reconstructs missing public-client registrations while rechecking the redirect-host allowlist.
  • Persists recovered registrations only after successful owner-password approval.
  • Adds recovery, tamper-resistance, configuration, and persistence tests plus security documentation.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete blocking or independently actionable non-blocking defects identified.

Recoverable registrations are authenticated, schema-validated, restricted to public clients, checked against the current redirect allowlist, and persisted only after the existing owner-approval gate succeeds.

Important Files Changed

Filename Overview
src/oauth-client-registration.ts Implements bounded HMAC signing, constant-time signature verification, schema validation, and reconstruction for public-client registrations.
src/oauth-store.ts Issues recoverable client IDs, reconstructs missing clients after signature and redirect-host checks, and adds idempotent persistence.
src/oauth-provider.ts Supplies the registration key to the client store and persists a recovered registration only after owner approval succeeds.
src/config.ts Resolves and validates the dedicated registration key, falling back to a deterministic key derived from the owner token.
src/cli.ts Extends initialization to preserve, derive, or generate the client-registration key as appropriate.
src/user-config.ts Adds secure random key generation and memory-hard compatibility derivation.
src/oauth-store.test.ts Covers transient recovery, failed and successful approval, persistence timing, and redirect-policy rejection.
src/oauth-client-registration.test.ts Covers signed-ID round trips, payload and signature tampering, wrong keys, length limits, and rejection of confidential clients.

Sequence Diagram

sequenceDiagram
    participant Client as OAuth Client
    participant Router as OAuth Router
    participant Store as Client Store
    participant Provider as OAuth Provider
    participant DB as SQLite

    Client->>Router: Authorization request with cached client_id
    Router->>Store: getClient(client_id)
    Store->>DB: Look up registration
    DB-->>Store: Missing
    Store->>Store: Verify HMAC and decode metadata
    Store->>Store: Check redirect hosts against allowlist
    Store-->>Router: Transient recovered registration
    Router->>Provider: authorize(client, parameters)
    Provider-->>Client: Owner approval form
    Client->>Provider: Submit owner password
    Provider->>Provider: Verify owner password
    Provider->>DB: Restore client registration
    Provider-->>Client: Redirect with authorization code
Loading

Reviews (1): Last reviewed commit: "fix(oauth): harden registration key deri..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/oauth-store.test.ts (1)

226-234: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the rejection status on the wrong-owner-token path.

authorizationResponse discards the status code, so this call proves only that no client row was written. Any early return or unrelated failure inside authorize produces the same result. Capture the status in the test double and assert 401, so the negative case pins the intended behavior.

💚 Suggested change
+    const rejected = authorizationResponse("wrong-owner-token");
     await recoveredProvider.authorize(
       recovered,
       params,
-      authorizationResponse("wrong-owner-token"),
+      rejected,
     );
+    assert.equal(statusOf(rejected), 401);

Record the value inside the helper:

 function authorizationResponse(
   ownerToken: string,
   onRedirect?: (location: string) => void,
 ): Response {
   const response = {
     req: { method: "POST", body: { owner_token: ownerToken } },
-    status() {
+    statusCode: 0,
+    status(code: number) {
+      response.statusCode = code;
       return response;
     },

and expose it with a small statusOf accessor, or assert on (rejected as unknown as { statusCode: number }).statusCode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth-store.test.ts` around lines 226 - 234, Update the wrong-owner-token
test around recoveredProvider.authorize to retain the authorization response
status and assert it is 401, using a statusOf accessor or direct statusCode
inspection on the captured response. Keep the existing assertion that no client
row is written.
src/oauth-client-registration.test.ts (1)

28-39: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add the legacy-ID and extra-segment negative cases.

The documented legacy path depends on recoverClientRegistration rejecting pre-change random client IDs. No assertion covers that. The extra !== undefined branch is also uncovered.

💚 Suggested additional assertions
 assert.equal(recoverClientRegistration(clientId, `${signingKey}-wrong`), undefined);
 assert.equal(recoverClientRegistration(`devspace-v1.${"x".repeat(5000)}.signature`, signingKey), undefined);
+
+// Legacy random client IDs are not recoverable.
+assert.equal(
+  recoverClientRegistration("devspace-0b3f9c1e-2d4a-4f77-9c0e-1a2b3c4d5e6f", signingKey),
+  undefined,
+);
+// A trailing segment invalidates the client ID even when the signature matches.
+assert.equal(recoverClientRegistration(`${clientId}.extra`, signingKey), undefined);
+// A different prefix is rejected.
+assert.equal(
+  recoverClientRegistration(`devspace-v2.${parts[1]}.${parts[2]}`, signingKey),
+  undefined,
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth-client-registration.test.ts` around lines 28 - 39, Add negative
assertions in the registration recovery tests around recoverClientRegistration:
verify a documented pre-change random/legacy client ID is rejected, and verify
an ID containing an additional segment beyond the expected three segments
returns undefined. Keep the existing malformed-part, wrong-signing-key, and
oversized-payload cases unchanged.
src/oauth-client-registration.ts (1)

27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the oversize rejection observable.

If the encoded client ID exceeds MAX_CLIENT_ID_LENGTH, createRecoverableClientId returns undefined and SqliteOAuthStore.registerClient falls back to a random UUID at src/oauth-store.ts lines 79-81. Recovery is then permanently unavailable for that client, and nothing records why.

Return a discriminated result or log at debug level so the reason is inspectable. A registration with many or long redirect_uris reaches this path.

As per coding guidelines: "Represent important behavior through schemas, types, checks, or explicit tool results rather than hidden prompt conventions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/oauth-client-registration.ts` at line 27, The oversize branch in
createRecoverableClientId currently returns undefined without exposing why,
while registerClient silently falls back to a UUID. Make the rejection
observable by returning a discriminated result or emitting a debug-level log
that identifies the MAX_CLIENT_ID_LENGTH overflow, and update
SqliteOAuthStore.registerClient to handle the chosen result without losing the
existing fallback behavior.

Source: Coding guidelines

src/user-config.ts (1)

107-113: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Make the client-registration derivation cost explicit and document password rotation.

Adding { N: 16_384, r: 8, p: 1 } keeps the current scrypt work factor but makes the cost inspectable and versioned with devspace-oauth-client-registration-v1. Add a short note at the derivation site that the derived registration key is bound to the ownerToken; if the owner password changes without a stored clientRegistrationKey, previously issued recoverable client IDs stop validating and affected clients must re-register.

♻️ Suggested change
 export function deriveClientRegistrationKey(ownerToken: string): string {
+  // Owner-token-bound: rotating the owner password without a stored
+  // clientRegistrationKey invalidates all previously issued recoverable client IDs.
   return scryptSync(
     ownerToken,
     "devspace-oauth-client-registration-v1",
     32,
+    { N: 16_384, r: 8, p: 1 },
   ).toString("base64url");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/user-config.ts` around lines 107 - 113, Update
deriveClientRegistrationKey to pass the explicit scrypt cost parameters N:
16_384, r: 8, and p: 1 while preserving the existing salt and output encoding.
Add a brief comment at the derivation site documenting that the key is bound to
ownerToken and that password changes without a stored clientRegistrationKey
invalidate previously issued recoverable client IDs, requiring client
re-registration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/security.md`:
- Around line 46-52: Revise the recovery claim in the security documentation to
state that repository verification is limited to the in-process
testClientRegistrationRecovery flow using SingleUserOAuthProvider and a mock
Express Response, without claiming validation of the SDK authorization router,
real redirect URI and PKCE checks, or a real ChatGPT/MCP-host reconnect. If a
real packaged npm/npx host reconnect was verified, document its scope and
restart, checkout, platform, tool, widget, and rendered-artifact coverage
instead.

In `@src/oauth-client-registration.ts`:
- Around line 22-27: Update recoverableClientId and
SqliteOAuthClientsStore.registerClient so the exact pre-parse registration is
used when creating the signed payload and persisted for recovery, rather than
OAuthClientInformationFullSchema.parse output. Preserve host-supplied RFC 7591
fields and extra properties, and ensure registration data remains restorable
even when SDK validation would normalize, reject, or drop fields.

In `@src/oauth-store.ts`:
- Around line 89-91: Update SqliteOAuthStore.restoreClient to validate the
restored client's redirect URIs against the current allowedRedirectHosts policy
before calling saveClient. Reject the client when any redirect URI has a
disallowed host, preserving the existing save behavior only for clients that
pass validation.

---

Nitpick comments:
In `@src/oauth-client-registration.test.ts`:
- Around line 28-39: Add negative assertions in the registration recovery tests
around recoverClientRegistration: verify a documented pre-change random/legacy
client ID is rejected, and verify an ID containing an additional segment beyond
the expected three segments returns undefined. Keep the existing malformed-part,
wrong-signing-key, and oversized-payload cases unchanged.

In `@src/oauth-client-registration.ts`:
- Line 27: The oversize branch in createRecoverableClientId currently returns
undefined without exposing why, while registerClient silently falls back to a
UUID. Make the rejection observable by returning a discriminated result or
emitting a debug-level log that identifies the MAX_CLIENT_ID_LENGTH overflow,
and update SqliteOAuthStore.registerClient to handle the chosen result without
losing the existing fallback behavior.

In `@src/oauth-store.test.ts`:
- Around line 226-234: Update the wrong-owner-token test around
recoveredProvider.authorize to retain the authorization response status and
assert it is 401, using a statusOf accessor or direct statusCode inspection on
the captured response. Keep the existing assertion that no client row is
written.

In `@src/user-config.ts`:
- Around line 107-113: Update deriveClientRegistrationKey to pass the explicit
scrypt cost parameters N: 16_384, r: 8, and p: 1 while preserving the existing
salt and output encoding. Add a brief comment at the derivation site documenting
that the key is bound to ownerToken and that password changes without a stored
clientRegistrationKey invalidate previously issued recoverable client IDs,
requiring client re-registration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a8d21b0a-7d1c-46e5-8afa-6524204dd1f0

📥 Commits

Reviewing files that changed from the base of the PR and between 3bd0378 and b315386.

📒 Files selected for processing (12)
  • docs/configuration.md
  • docs/security.md
  • package.json
  • src/cli.ts
  • src/config.test.ts
  • src/config.ts
  • src/oauth-client-registration.test.ts
  • src/oauth-client-registration.ts
  • src/oauth-provider.ts
  • src/oauth-store.test.ts
  • src/oauth-store.ts
  • src/user-config.ts

Comment thread docs/security.md
Comment thread src/oauth-client-registration.ts Outdated
Comment thread src/oauth-store.ts Outdated
@Waishnav

Waishnav commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

[GPT-5.6-THINKING] RESPONDING ON BEHALF OF WAISHNAV

The additional review findings are covered in 1aaf51b and e67e3c0: the rejected Owner-password path now asserts HTTP 401, legacy and extra-segment client IDs are rejected explicitly, oversized signed IDs produce an observable registration error rather than silently degrading to a non-recoverable ID, and the compatibility scrypt parameters and Owner-password binding are explicit. The full test suite and typecheck pass on e67e3c0.

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