Skip to content

fix(oauth): repair the OpenClaw client registration and make redirect URIs editable - #1629

Merged
krishagel merged 1 commit into
devfrom
fix/oauth-client-edit-and-repair
Aug 11, 2026
Merged

fix(oauth): repair the OpenClaw client registration and make redirect URIs editable#1629
krishagel merged 1 commit into
devfrom
fix/oauth-client-edit-and-repair

Conversation

@krishagel

Copy link
Copy Markdown
Member

Agent-connect has been broken in prod for every user, and the admin surface had no way to fix it. This closes both halves.

Migration 176 — repair the row

The PSD OpenClaw client (7e8646f4…) carried a dev-only http://localhost:3000/agent-connect-aistudio/callback alongside two correct production URIs. Its application_type is native, and the policy rejects an HTTP redirect whose host isn't a literal loopback address — LOOPBACK_HOSTS is {"127.0.0.1", "[::1]"}, and localhost isn't in it (RFC 8252 §7.3).

Redirect validation is all-or-nothing, so that single entry made loadClient() return undefined and oidc-provider answer invalid_client. The two valid URIs never got a chance. The prod errors at 19:35:28 / 19:36:25 / 19:39:43 UTC are one user's three attempts — but the client was dead for everyone.

The row was also revoked at 23:18 UTC — reasonably, since revoke was the only control the UI offered for a client that plainly needed changing. The migration restores is_active and corrects the URI, guarded on the exact current redirect array and the public-PKCE native profile so an administrator-modified or unrelated client is never overwritten.

Verified before committing, against a throwaway postgres:

check result
applies once UPDATE 1
re-run is a no-op UPDATE 0
resulting array validates VALID: true, ERRORS: []
tokens affected by reactivation 0 refresh / 0 access / 0 codes — nothing was ever issued

updateOAuthClientRedirectUris — remove the dead end

Admin actions were create/list/revoke only, and the actions column rendered nothing once isActive went false. A client revoked because its redirect list was wrong was unrecoverable without a migration and a deploy — exactly the situation migration 176 exists to undo.

The action is deliberately narrow: redirect URIs and active state, nothing else. applicationType, tokenEndpointAuthMethod, requirePkce, the secret and the scopes define the security profile, and changing those on a live registration silently alters what an already-issued grant means — those still require a new client.

New URIs are validated against the stored application type, never a caller-supplied one, so a native client can't be relaxed into web rules by passing a type alongside the URIs. A zero-row update fails rather than reporting a false success, matching revokeOAuthClient (REV-COR-055).

The dialog stays available on a revoked client and offers reactivation in the same save. Validation failures are shown verbatim — the admin typed the URI and can't correct it from a generic error. That's the opposite of the adapter's log path (#1628), which redacts: this text goes to an authenticated administrator looking at their own input, not to CloudWatch.

Tests

Action (5): admin role required before any query · the real localhost repair with reactivation · a native-forbidden URI that must not reach the UPDATE · validation against the stored type while ignoring a caller-supplied one · no-false-success on zero rows

Dialog (4): present and pre-checked on a revoked client · corrected list + reactivation in one call · verbatim error display · blank-line trimming

Also mocked the new dialog in oauth-clients-admin-trust.test.tsx, which renders the page client and doesn't mock @/components/ui/dialog — the same treatment it already gives ClientFormSheet.

112 tests pass across the OAuth suites; eslint clean; typecheck clean.

After deploy

The migration fixes the row automatically. No manual admin step needed — and if a redirect URI ever goes wrong again, it's now editable in the UI without a deploy.

… URIs editable

Agent-connect has been broken in prod for every user, and the admin surface had
no way to fix it. This closes both halves.

Migration 176 — repair the row.

The PSD OpenClaw client (7e8646f4-4091-4a34-a6b9-0d3721e8a126) carried a
dev-only `http://localhost:3000/agent-connect-aistudio/callback` alongside two
correct production URIs. Its application_type is `native`, and
lib/oauth/redirect-uri-policy.ts rejects an HTTP redirect whose host is not a
literal loopback address — LOOPBACK_HOSTS is {"127.0.0.1", "[::1]"} and
`localhost` is not in it (RFC 8252 section 7.3). Redirect validation in
DrizzleOidcAdapter is all-or-nothing, so that single entry made loadClient()
return undefined and oidc-provider answer `invalid_client`; the two valid URIs
never got a chance. Prod errors 2026-08-10 19:35:28 / 19:36:25 / 19:39:43 UTC
are one user's three attempts, but the client was dead for everyone.

The row was also revoked at 23:18 UTC — reasonably, since revoke was the only
control the admin UI offered for a client that plainly needed changing. The
migration restores is_active and corrects the URI to 127.0.0.1, guarded on the
exact current redirect array and the public-PKCE native profile so an
administrator-modified or unrelated client is never overwritten.

Verified against a throwaway postgres before committing: applies once
(UPDATE 1), re-runs as a clean no-op (UPDATE 0), and the resulting three-URI
array passes validateOAuthRedirectUris("native", …) with VALID: true, ERRORS: [].
Also verified the client had 0 refresh tokens, 0 access tokens and 0
authorization codes, so nothing was ever issued against it and reactivating
grants nothing retroactively.

updateOAuthClientRedirectUris — remove the dead end.

The admin actions were create/list/revoke only, and the table's actions column
rendered nothing at all once isActive went false. A client revoked *because*
its redirect list was wrong was therefore unrecoverable without a migration and
a deploy, which is exactly the situation this migration exists to undo.

The new action is deliberately narrow: redirect URIs and active state, nothing
else. applicationType, tokenEndpointAuthMethod, requirePkce, the secret and the
scopes define the client's security profile, and changing those on a live
registration silently alters what an already-issued grant means — those still
require registering a new client. New URIs are validated against the STORED
application type, never a caller-supplied one, so a native client cannot be
relaxed into web rules by passing a type alongside the URIs. A zero-row update
fails rather than reporting a false success, matching revokeOAuthClient
(REV-COR-055).

The dialog stays available on a revoked client and offers reactivation in the
same save. Validation failures are surfaced verbatim to the admin: they typed
the URI and cannot correct it from a generic error. That is the opposite of the
adapter's LOG path, which redacts — the difference is that this text goes to an
authenticated administrator looking at their own input, not to CloudWatch.

Tests: 5 action cases (admin-role required before any query, the real localhost
repair with reactivation, a rejected native-forbidden URI that must not reach
the UPDATE, validation against the stored type while ignoring a caller-supplied
one, and no-false-success on zero rows) and 4 dialog cases (present and
pre-checked on a revoked client, corrected list plus reactivation in one call,
verbatim error display, blank-line trimming). Also mocked the new dialog in
oauth-clients-admin-trust.test.tsx, which renders the page client and does not
mock @/components/ui/dialog — same treatment it already gives ClientFormSheet.

112 tests pass across the OAuth suites; eslint clean; typecheck clean.
Copilot AI lite review requested due to automatic review settings August 11, 2026 00:13

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review

Reviewed the diff (actions/oauth/oauth-client.actions.ts, the new EditRedirectUrisDialog, oauth-clients-page-client.tsx, migration 176, and the test files).

Summary

Solid, well-scoped fix. The narrow "redirect URIs + active state only" surface, validating against the stored applicationType rather than a caller-supplied one, and the zero-row-update-is-a-failure pattern (matching revokeOAuthClient/REV-COR-055) are all the right calls. Migration 176's guard (pinning the exact current redirect_uris array + the full public-PKCE native profile) makes it safely re-runnable and scoped to exactly the row it's meant to fix. Test coverage is good — both the action's validation/authorization paths and the dialog's UI behavior (verbatim error display, blank-line trimming, reactivation) are exercised.

Minor / non-blocking

  1. TOCTOU between load and update (oauth-client.actions.ts:374-424) — the applicationType is read in one query and the redirect URIs are validated against it, then written in a separate query with no transaction wrapping the two. If the application type could ever change between those two calls, the write would use a validation result computed against a now-stale type. In practice there's no UI path to change applicationType on an existing client today, so this is theoretical, but wrapping both in executeTransaction would remove the assumption entirely and match the "never nest db.transaction() inside executeQuery()" guidance in CLAUDE.md — using executeTransaction directly would be the idiomatic fix if you want to close this off.

  2. Redundant type cast (oauth-client.actions.ts:390) — existing.applicationType as OAuthApplicationType is unnecessary; the Drizzle column is already declared .$type<OAuthApplicationType>() in lib/db/schema/tables/oauth-clients.ts:24-27, so existing.applicationType is already typed correctly. Harmless, but worth dropping since the codebase otherwise avoids type assertions.

  3. flex on TableCell (oauth-clients-page-client.tsx:100) — changing the actions <TableCell> to className="flex items-center gap-1" turns a <td> into a flex container. Worth a quick visual check that this doesn't fight with the table component's own padding/vertical-align defaults, especially since it's the last column and has w-24 on the header — two icon buttons plus gap might be tight.

  4. Silent no-op dedupvalidateOAuthRedirectUris rejects duplicate URIs as a validation error rather than silently deduping them (good), but it's worth double-checking this is surfaced clearly to the admin in the dialog's verbatim error text (it should be, given the ${uri}: duplicate redirect URI message format) — just flagging since it's not directly covered by one of the four dialog tests.

Security

No concerns. Admin-role gate is present, redirect URI validation is applied server-side against the trusted stored value (not a client-supplied applicationType), errors returned to the admin don't leak anything beyond what they themselves typed, and the migration only touches one specific row via a fully-pinned WHERE clause.

Test coverage

Good — the 5 action tests and 4 dialog tests cover the meaningful branches (auth gate, the actual repair scenario, native-forbidden URI rejection, stored-vs-supplied-type isolation, zero-row failure, revoked-client availability, reactivation, verbatim errors, blank-line trimming). Nothing major missing.


🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c6606f0c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{/* Editing stays available on a REVOKED client. This column used to
render nothing once isActive went false, which left a client that
was revoked because its redirect list was wrong with no way back. */}
<EditRedirectUrisDialog client={client} onSaved={onSaved} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add authenticated E2E coverage for the edit flow

This introduces a new admin-facing edit/reactivation workflow, but the commit adds only unit tests with the dialog or server-action dependencies mocked and adds no spec under tests/e2e. An authenticated Playwright test should exercise opening this control for a revoked client, submitting the real server action, and observing the refreshed status so role enforcement, action serialization, dialog wiring, and reactivation are covered together.

AGENTS.md reference: AGENTS.md:L34-L34

Useful? React with 👍 / 👎.


UPDATE oauth_clients
SET
redirect_uris = '["http://127.0.0.1:3000/agent-connect-aistudio/callback", "https://dev.aistudio.psd401.ai/agent-connect-aistudio/callback", "https://aistudio.psd401.ai/agent-connect-aistudio/callback"]'::jsonb,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the local callback aligned with the registered URI

After a local db:reset, migration 139 inserts this client and migration 176 changes its only local callback to http://127.0.0.1:3000/..., while .env.example:2 still sets AUTH_URL=http://localhost:3000 and actions/agent-aistudio.actions.ts:22-23 constructs the authorization and token-exchange redirect_uri from that issuer. OAuth redirect matching is exact, so the supported local agent-connect flow will now submit the unregistered localhost URI and be rejected; either generate the loopback-literal URI locally or align the local issuer configuration.

AGENTS.md reference: AGENTS.md:L191-L193

Useful? React with 👍 / 👎.

Comment on lines +403 to +407
throw ErrorFactories.invalidInput(
"redirectUris",
validated.redirectUris,
uriValidation.errors.join("; ")
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return the redirect validation reason to the dialog

When an administrator submits an invalid URI, ErrorFactories.invalidInput stores this joined validator text only as the field's constraint; its userMessage is always "The provided input is invalid", and handleError returns that generic value as result.message. The dialog reads only result.message, so the real server action never provides the offending URI or reason that the new UI says it will show, leaving administrators unable to diagnose failures such as the localhost case; set a safe explicit userMessage or return the validation errors in the action contract.

Useful? React with 👍 / 👎.

@krishagel
krishagel merged commit 389534f into dev Aug 11, 2026
11 checks passed
krishagel added a commit that referenced this pull request Aug 11, 2026
dev took 176 for the OpenClaw OAuth client redirect repair (#1629) while this
branch was open. Renumbers the migration file and every reference to it so the
numbering stays collision-free and migrationFiles can hold both.
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.

2 participants