Skip to content

[codex-proposal] Add managed dev Google OAuth clients#2545

Open
stopachka wants to merge 3 commits intomainfrom
test2
Open

[codex-proposal] Add managed dev Google OAuth clients#2545
stopachka wants to merge 3 commits intomainfrom
test2

Conversation

@stopachka
Copy link
Copy Markdown
Contributor

Summary

This implements a Clerk-style managed development credential mode for Google OAuth redirect clients.

The core UX is inside the existing Google client form rather than as a separate admin-page action:

  • Web Google clients now default to Use Instant dev credentials
  • When enabled, the dashboard hides custom client ID, client secret, and custom redirect URL inputs
  • Existing managed-dev clients render as managed in the dashboard and make the limitation explicit
  • Switching an existing client between managed-dev and custom-credential mode is intentionally rejected: delete and recreate instead

Product / DX shape

The implementation is intentionally narrow.

What is supported:

  • Google OAuth web redirect clients
  • Localhost origins
  • Preview origins already modeled by our authorized-origin system (vercel, netlify)
  • Custom app schemes already modeled by our authorized-origin system

What is not supported:

  • Google Button / One Tap style clients
  • Native Google client types (ios, android)
  • Production web domains on managed credentials
  • Custom redirect URLs while managed credentials are enabled
  • In-place mutation of an existing client from managed-dev to custom credentials, or vice versa

That scope is deliberate. Google redirect OAuth is a server-mediated flow where shared dev credentials are practical. Google Button / One Tap relies on Google client-side setup and origin registration, so trying to hide that behind shared backend credentials would be misleading and brittle.

Backend changes

Config

Added an optional managed-dev Google credential source in server config:

  • GOOGLE_OAUTH_CLIENT_ID_DEV
  • GOOGLE_OAUTH_CLIENT_SECRET_DEV
  • optional config map fallback under :google-oauth-client-dev

OAuth client resolution

When an OAuth client is:

  • provider: Google
  • app type: web
  • meta.useDevCredentials = true

we now ignore stored app-specific credentials and instantiate the runtime OAuth client from the server-managed dev credentials plus Google's discovery document.

If the server is missing those managed credentials, we fail with a validation error rather than silently producing a broken flow.

Dashboard validation

Added explicit validation so that managed-dev mode:

  • only works for Google
  • only works for Google web redirect clients
  • requires client ID and secret to be blank
  • rejects custom redirect URLs

I also tightened the update path so JSON body handling is robust for string-vs-keyword body keys and so managed-dev mode cannot be toggled in place. The delete-and-recreate requirement is now enforced server-side, not just implied by the UI.

Runtime validation

At OAuth start time, managed-dev Google clients now reject production-style origins with a clear error. The runtime allows only:

  • localhost generic origins
  • preview origins matched through existing vercel / netlify origin types
  • custom app schemes

This matters because even if someone manually creates a managed-dev client through the API, they still should not be able to use Instant's shared Google credentials against a production domain.

Frontend changes

The Google dashboard UI now:

  • defaults web clients into managed-dev mode
  • renders a checkbox to opt out into custom credentials
  • hides irrelevant inputs when managed-dev mode is active
  • shows a persistent explanation of the scope and limitations
  • avoids showing client ID / redirect editing affordances that do not apply to managed-dev clients

This keeps the happy path simple for local development while still letting someone opt into their own production-ready Google credentials from the same form.

Tests

Added targeted end-to-end-ish server coverage in two places.

Runtime flow test

instant.runtime.routes-test/google-managed-dev-oauth-test

Covers:

  • production-origin rejection for managed-dev Google clients
  • full /runtime/oauth/start -> /runtime/oauth/callback -> /runtime/oauth/token flow
  • token exchange using managed server credentials
  • user creation / refresh token issuance

This test stubs Google network edges but exercises the real route flow and state/cookie plumbing.

Dashboard validation test

instant.dash.routes-test/google-managed-dev-oauth-client-validation

Covers:

  • creating a managed-dev Google web client without secrets
  • rejecting custom redirect URLs in managed-dev mode
  • rejecting custom secrets in managed-dev mode
  • rejecting in-place managed-dev mode changes on update

Manual verification

I did not complete a live browser-based Google sign-in against Google's real consent screen in this environment. That would require an interactive browser login. I did verify the full server flow under test coverage and validated the dashboard behavior and server-side guards around the new mode.

Why this shape

This is the smallest implementation that improves DX without creating an ambiguous production story.

A separate admin button would have made the model harder to reason about. Folding the behavior into the existing Google client form means the object the user creates is still just a Google OAuth client, with one explicit mode switch.

The runtime guard is also important. Without it, managed credentials would be too easy to accidentally carry into production setups.

Follow-ups I would consider separately

  • Apply the same managed-dev pattern to GitHub redirect OAuth if we want parity for another common dev provider
  • Add docs copy that explicitly separates Google redirect OAuth from Google Button / One Tap
  • Consider whether the dashboard should show a stronger warning badge when a managed-dev client exists alongside production origins

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

📝 Walkthrough

Walkthrough

Adds Google "Instant dev credentials" support: frontend UI toggle and conditional client creation, server config accessors and schema, validation in dash and runtime routes, model resolution loading dev creds for Google web clients, and corresponding unit/integration tests.

Changes

Cohort / File(s) Summary
Client UI
client/www/components/dash/auth/Google.tsx
Adds useDevCredentials checkbox/UI for appType === 'web'; omits clientId/clientSecret/redirectTo on submit when enabled; hides ID/redirect editors and shows dev-credentials informational snippet.
Server config & schema
server/src/instant/config.clj, server/src/instant/config_edn.clj
Adds get-google-oauth-client-dev to resolve dev creds from env or config (decrypts/obfuscates secret) and registers ::google-oauth-client-dev in config spec.
Dash API routes & validation
server/src/instant/dash/routes.clj
Introduces optional-body helpers (sentinel for missing vs explicit nil), oauth-client-errors validator, normalizes :meta, enforces managed-dev creation/update constraints and validation on create/update.
OAuth model resolution
server/src/instant/model/app_oauth_client.clj
Adds meta-value accessor and google-managed-dev-client?; new ->OAuthClient branch loads dev creds via config and constructs OAuth client from Google discovery when managed-dev is active.
Runtime OAuth flow validation
server/src/instant/runtime/routes.clj
Adds localhost-origin? and managed-dev-origin? helpers; oauth-start rejects disallowed redirect origins and rejects non-nil redirect_to for Google managed-dev clients.
Dash tests
server/test/instant/dash/routes_test.clj
Adds google-managed-dev-oauth-client-validation covering successful create/update and negative cases (custom redirect, non-blank secret, wrong discovery endpoint, toggling mode, missing client).
Runtime tests & test helpers
server/test/instant/runtime/routes_test.clj
Refactors test request helpers (raw request builder, JSON handling), adds JWT/encoding helpers and google-managed-dev-oauth-test that mocks discovery/dev-creds/token exchange and exercises start→callback→token flows with allowed/rejected origins.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Client as Web Client
    participant API as Instant API
    participant Config as Config/Credentials
    participant Google as Google OAuth

    User->>Client: Enable "Instant dev credentials"
    Client->>API: POST /dash/apps/:id/oauth_clients (meta.useDevCredentials=true, no client_id/secret)
    API->>API: Validate and store client (meta flag)

    rect rgba(100,150,200,0.5)
    Note over User,Google: OAuth Flow using managed dev credentials
    User->>Client: Click "Sign in with Google"
    Client->>API: GET /runtime/oauth/start (oauth_client_id, redirect_uri)
    API->>Config: get-google-oauth-client-dev()
    Config-->>API: Return dev client_id + client_secret
    API->>API: Validate redirect_uri allowed for managed-dev
    API->>Google: Redirect user to Google auth (using dev client_id)
    Google-->>User: Consent screen
    User->>Google: Approve -> Redirect to callback (code + state)
    Client->>API: GET /runtime/oauth/callback (code, state)
    API->>Google: POST token endpoint (code, dev client credentials)
    Google-->>API: Return id_token + refresh_token
    API-->>Client: Token response / user session
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • nezaj
🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding managed dev Google OAuth clients. It is specific enough to convey the primary feature without being overly verbose.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering product scope, backend/frontend changes, testing, and implementation rationale.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test2

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 and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

View Vercel preview at instant-www-js-test2-jsv.vercel.app.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/instant/config.clj`:
- Around line 237-247: The current get-google-oauth-client-dev can mix one env
value with the other fallback, producing mismatched credentials; update it to
resolve the pair atomically: read both env vars (GOOGLE_OAUTH_CLIENT_ID_DEV and
GOOGLE_OAUTH_CLIENT_SECRET_DEV) and normalize blank strings to nil (e.g. via
not-empty), then if both env values are present use them together; else if
neither env is present fall back to the EDN pair from `@config-map`
(:google-oauth-client-dev :client-id and :client-secret via
crypt-util/secret-value); otherwise return nil (no partial pair). Keep
crypt-util/obfuscate for the returned :client-secret and locate changes in
get-google-oauth-client-dev.

In `@server/src/instant/dash/routes.clj`:
- Around line 719-723: The route reads current-client but never asserts it
exists before calling app-oauth-client-model/update!, which lets a bad :id
create a new partial client; modify the handler that fetches current-client (the
binding named current-client where app-oauth-client-model/get-by-id is called)
to check for nil and return an appropriate error response (e.g. 404 or
validation error) immediately when current-client is missing instead of
proceeding to call app-oauth-client-model/update!; apply the same nil-existence
check and early return for the other occurrence around the code block at the
757-762 region to prevent update! from synthesizing a new record.
- Around line 687-703: The validation currently skips checking
discovery_endpoint for non-GitHub providers, which allows callers to register a
Google client with an arbitrary OIDC discovery URL when useDevCredentials is
enabled; update the validation around the ex/assert-valid! / discovery-endpoint
logic so that when (:provider_name provider) is "google" and the request
indicates useDevCredentials, discovery_endpoint is validated (or forced) to
equal Google’s canonical discovery document URL (the same literal used in
app_oauth_client.clj to detect managed-dev) — i.e., read useDevCredentials from
req/body, and if true and provider is "google" ensure discovery-endpoint is the
exact Google discovery URL (or fail validation via oauth-client-errors) so
managed-dev clients are pinned to the expected document.

In `@server/src/instant/runtime/routes.clj`:
- Around line 187-197: The localhost check in localhost-origin? only matches
"localhost" and "127.0.0.1", so IPv6 loopback addresses like "::1" are
incorrectly rejected by managed-dev-origin? (used in managed-dev auth flows);
update the host set in localhost-origin? to include "::1" and apply the same
change to the other identical occurrence of this logic (the second
implementation referenced in the diff) so IPv6 loopback redirects are accepted
for "generic" managed-dev service.

In `@server/test/instant/runtime/routes_test.clj`:
- Line 33: Remove the unused require for lambdaisland.uri by deleting the vector
entry `[lambdaisland.uri :as uri]` from the ns require list in the
routes_test.clj namespace (the unused alias `uri` is the symbol to target);
ensure the remaining requires remain properly comma/spacing separated and run
tests to confirm no references to `uri` remain.
🪄 Autofix (Beta)

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

Run ID: 8ac8ae3c-e18a-434c-b8a7-2897e22bb9ea

📥 Commits

Reviewing files that changed from the base of the PR and between 9d6d1cf and 444d86f.

📒 Files selected for processing (8)
  • client/www/components/dash/auth/Google.tsx
  • server/src/instant/config.clj
  • server/src/instant/config_edn.clj
  • server/src/instant/dash/routes.clj
  • server/src/instant/model/app_oauth_client.clj
  • server/src/instant/runtime/routes.clj
  • server/test/instant/dash/routes_test.clj
  • server/test/instant/runtime/routes_test.clj

Comment thread server/src/instant/config.clj
Comment thread server/src/instant/dash/routes.clj Outdated
Comment thread server/src/instant/dash/routes.clj Outdated
Comment thread server/src/instant/runtime/routes.clj
Comment thread server/test/instant/runtime/routes_test.clj Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/instant/dash/routes.clj`:
- Around line 754-758: Current condition blocks all meta updates when the
request includes :meta because it checks (or
(app-oauth-client-model/uses-dev-credentials? current-client) ...), preventing
any non-useDevCredentials changes on managed-dev clients; change the when to
only error when the client already uses dev credentials AND the request
explicitly tries to change or clear that flag: replace the existing (when (and
(body-contains? req :meta) (or (app-oauth-client-model/uses-dev-credentials?
current-client) (contains? (or meta {}) "useDevCredentials") (contains? (or meta
{}) :useDevCredentials) (nil? meta))) ...) with (when (and (body-contains? req
:meta) (app-oauth-client-model/uses-dev-credentials? current-client) (or
(contains? (or meta {}) "useDevCredentials") (contains? (or meta {})
:useDevCredentials) (nil? meta))) ...), so updates that leave useDevCredentials
unchanged (e.g., merged-meta preserving the flag) are allowed.
🪄 Autofix (Beta)

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

Run ID: e88ff626-5dd5-4c6a-98a9-2654fca96981

📥 Commits

Reviewing files that changed from the base of the PR and between 444d86f and b831feb.

📒 Files selected for processing (5)
  • server/src/instant/config.clj
  • server/src/instant/dash/routes.clj
  • server/src/instant/runtime/routes.clj
  • server/test/instant/dash/routes_test.clj
  • server/test/instant/runtime/routes_test.clj
✅ Files skipped from review due to trivial changes (1)
  • server/src/instant/runtime/routes.clj
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/test/instant/runtime/routes_test.clj

Comment thread server/src/instant/dash/routes.clj Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/src/instant/dash/routes.clj`:
- Around line 745-747: The merged-meta computation uses (merge (:meta
current-client) meta) which preserves old values when the request explicitly
sends :meta nil; change the cond in the merged-meta binding to treat any
presence of :meta in the request as the new meta value itself: (cond
(body-contains? req :meta) meta :else (:meta current-client)), so that
downstream validation (e.g. oauth-client-errors) operates on the actual
post-update meta state (apply the same change for the other analogous branches
handling 757-769 and 770-774).
- Around line 677-678: The code calls app-oauth-service-provider-model/get-by-id
to set provider and then uses (:provider_name provider) later; add an explicit
nil-check right after obtaining provider and reject unknown provider_id with a
clear error response before any field validation or reading provider keys. In
practice, inside the same route handler where provider is set, if provider is
nil return the appropriate 4xx/validation error (e.g., "unknown provider_id")
and stop further processing; update the same logic paths that later read
:provider_name (and the non-GitHub credential/discovery handling) so they only
run when provider is non-nil.
🪄 Autofix (Beta)

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

Run ID: 7b2be008-8d65-4bf7-b173-6f44b6a52190

📥 Commits

Reviewing files that changed from the base of the PR and between b831feb and cc54e98.

📒 Files selected for processing (2)
  • server/src/instant/dash/routes.clj
  • server/test/instant/dash/routes_test.clj
✅ Files skipped from review due to trivial changes (1)
  • server/test/instant/dash/routes_test.clj

Comment on lines +677 to +678
provider (app-oauth-service-provider-model/get-by-id {:app-id app-id
:id provider-id})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Reject unknown provider_ids before field validation.

If app-oauth-service-provider-model/get-by-id returns nil, this path falls into the generic non-GitHub branch on Line 684 and can report unrelated discovery_endpoint / credential errors instead of failing on the missing provider. Please assert the provider record before reading :provider_name.

Also applies to: 684-685

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/instant/dash/routes.clj` around lines 677 - 678, The code calls
app-oauth-service-provider-model/get-by-id to set provider and then uses
(:provider_name provider) later; add an explicit nil-check right after obtaining
provider and reject unknown provider_id with a clear error response before any
field validation or reading provider keys. In practice, inside the same route
handler where provider is set, if provider is nil return the appropriate
4xx/validation error (e.g., "unknown provider_id") and stop further processing;
update the same logic paths that later read :provider_name (and the non-GitHub
credential/discovery handling) so they only run when provider is non-nil.

Comment on lines +745 to +747
merged-meta (cond
(body-contains? req :meta) (merge (:meta current-client) meta)
:else (:meta current-client))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the same meta state that gets written.

When the request sends :meta nil, Line 757 already treats that as a real mutation, but Line 746 still leaves merged-meta at the old value while Line 773 writes nil. That lets oauth-client-errors validate stale appType / managed-dev flags instead of the post-update state.

🛠️ Suggested fix
-        merged-meta (cond
-                      (body-contains? req :meta) (merge (:meta current-client) meta)
-                      :else (:meta current-client))
+        merged-meta (cond
+                      (not (body-contains? req :meta)) (:meta current-client)
+                      (nil? meta) nil
+                      :else (merge (:meta current-client) meta))

Also applies to: 757-769, 770-774

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/instant/dash/routes.clj` around lines 745 - 747, The merged-meta
computation uses (merge (:meta current-client) meta) which preserves old values
when the request explicitly sends :meta nil; change the cond in the merged-meta
binding to treat any presence of :meta in the request as the new meta value
itself: (cond (body-contains? req :meta) meta :else (:meta current-client)), so
that downstream validation (e.g. oauth-client-errors) operates on the actual
post-update meta state (apply the same change for the other analogous branches
handling 757-769 and 770-774).

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