Skip to content

feat(electron): add http loopback redirect strategy for native OAuth#9044

Draft
nicolas-angelo wants to merge 2 commits into
mainfrom
feat/electron-loopback-oauth-transport
Draft

feat(electron): add http loopback redirect strategy for native OAuth#9044
nicolas-angelo wants to merge 2 commits into
mainfrom
feat/electron-loopback-oauth-transport

Conversation

@nicolas-angelo

@nicolas-angelo nicolas-angelo commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

This builds off @wobsoriano's work; the loopback OAuth approach in the loopback branch of his clerk-electron-example.

Adds an http loopback redirect strategy for native OAuth in @clerk/electron (alongside the existing, default custom-scheme deep link) — and fixes the browser-side completion so the OAuth flow's hosted callback page resolves instead of hanging.

Completing the OAuth handshake (the key fix)

Example:
After attempting sign-in with Vercel, the OAuth callback page would hang in a perpetual "waiting" state in the system browser — even though the Electron app had already signed the user in.

The native transport set only redirect_url to the transport callback and left action_complete_redirect_url pointing at the in-app URL (the clerk://app/ custom scheme). The system browser can't navigate to a custom scheme, so Clerk's final (action-complete) redirect never resolved — the flow never signaled completion in the browser, leaving the tab stalled.

This PR backs both redirect_url and action_complete_redirect_url with the transport's redirect URL, so the provider's action-complete redirect lands on a reachable listener and the browser tab resolves to a real completion page.

New http loopback strategy

Receives the callback on a short-lived http://127.0.0.1:<port> server (RFC 8252 §7.3) and serves a completion page (or 302s to your own URL). Needs no OS protocol registration. Selected via a discriminated oauth.redirect union, with httpRedirectStrategy() / deepLinkRedirectStrategy() helpers. The custom-scheme deep link remains the default.

Usage

import { createClerkBridge, httpRedirectStrategy } from '@clerk/electron';
import { storage } from '@clerk/electron/storage';

// Default — custom-scheme deep link (unchanged)
createClerkBridge({
  storage: storage(),
  renderer: { scheme: 'my-app', host: 'app' },
});

// Opt into the http loopback strategy
createClerkBridge({
  storage: storage(),
  renderer: { scheme: 'my-app', host: 'app' },
  oauth: {
    redirect: httpRedirectStrategy({
      port: 45789,                               // optional (default 45789)
      successUrl: 'https://myapp.com/signed-in', // optional: 302 to your own page
      // successHtml: '<h1>Signed in</h1>',       // optional alternative (mutually exclusive)
    }),
  },
});

For the http strategy, add http://127.0.0.1:<port>/sso-callback to your instance's allowed redirect URLs.

Testing: sign in with an OAuth provider from the Electron app and confirm the browser tab resolves to the completion page (no "waiting" hang) and the app ends up signed in — with both the default deep link and httpRedirectStrategy().

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • JSDoc comments have been added/updated for the new exports.
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 1, 2026 10:35pm
swingset Ready Ready Preview, Comment Jul 1, 2026 10:35pm

Request Review

@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 838b9cd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@clerk/electron Minor
@clerk/clerk-js Patch
@clerk/chrome-extension Patch
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: bf842f5a-0bbe-48ea-aea5-6a58027b5259

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an HTTP loopback redirect strategy (http://127.0.0.1:<port>/sso-callback) as an alternative to the existing deep-link custom-scheme strategy for native OAuth in @clerk/electron. Refactors the OAuth transport behind a RedirectHandler interface with arm/disarm/cleanup lifecycle methods. Fixes @clerk/clerk-js to pass the transport redirect URL as both redirect_url and action_complete_redirect_url.

Changes

HTTP Loopback OAuth Redirect Strategy

Layer / File(s) Summary
OAuth redirect types and strategy builders
packages/electron/src/shared/types.ts, packages/electron/src/main/oauth-redirect.ts, packages/electron/src/index.ts
Adds OAuthRedirectStrategy union type (http loopback / deep-link), OAuthOptions, updates CreateClerkBridgeOptions; exports httpRedirectStrategy and deepLinkRedirectStrategy factory functions; expands public module exports.
OAuth transport RedirectHandler refactor
packages/electron/src/main/oauth-transport.ts
Introduces RedirectHandler interface with redirectUrl, arm, disarm, cleanup; adds createDeepLinkHandler and createHttpHandler (loopback HTTP server serving completion page or redirecting to successUrl); refactors setupOAuthTransportIpcHandlers to select handler by strategy and use settle()-driven flow control.
createClerkBridge wiring and clerk-js fix
packages/electron/src/main/create-clerk-bridge.ts, packages/clerk-js/src/utils/authenticateWithTransport.ts, packages/clerk-js/src/utils/__tests__/authenticateWithTransport.test.ts
Passes oauth options into setupOAuthTransportIpcHandlers; fixes _authenticateWithTransport to set redirectUrlComplete to the transport redirect URL so action-complete callbacks return to the loopback listener.
Tests and changeset
packages/electron/src/main/__tests__/oauth-transport.test.ts, packages/electron/src/main/__tests__/create-clerk-bridge.test.ts, .changeset/loopback-oauth-transport.md
New oauth-transport.test.ts covers HTTP loopback scenarios (default page, successHtml, successUrl, protocol rejection, concurrency guard); expanded create-clerk-bridge tests cover protocol registration, loopback URL derivation, and http strategy behavior; changeset documents the feature.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hop hop, no more deep-link fright,
A loopback server springs to light!
127.0.0.1 awaits the call,
/sso-callback catches all.
OAuth flows now settle neat,
With arm and disarm, the trick's complete! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% 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
Title check ✅ Passed The title clearly and concisely describes the main change: adding an HTTP loopback redirect strategy for native OAuth in Electron.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9044

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9044

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9044

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9044

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9044

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9044

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9044

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9044

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9044

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9044

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9044

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9044

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9044

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9044

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9044

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9044

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9044

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9044

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9044

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9044

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9044

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9044

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9044

commit: 838b9cd

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-30T14:37:38.227Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on c6e7c36.

@nicolas-angelo nicolas-angelo marked this pull request as draft June 30, 2026 14:38

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/electron/src/main/__tests__/oauth-transport.test.ts (1)

44-50: 🩺 Stability & Availability | 🔵 Trivial | ⚖️ Poor tradeoff

Hardcoded loopback ports may cause flaky CI runs.

Each test binds the loopback server to a fixed port (4765447658). If any of these ports is already in use on the CI host, the server bind will fail and the test will become non-deterministically flaky. The handler derives redirectUrl synchronously from the configured port, so an ephemeral port (0) isn't directly usable here, but consider acquiring a free port at runtime (e.g. via a transient net.Server listen-on-0 + address().port) before constructing the bridge to remove the collision risk.

Also applies to: 66-72, 84-90, 108-112

🤖 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 `@packages/electron/src/main/__tests__/oauth-transport.test.ts` around lines 44
- 50, The OAuth transport tests are using fixed loopback ports, which can
collide on shared CI hosts and make the suite flaky. Update the affected cases
in oauth-transport.test.ts that call setupOAuthTransportIpcHandlers,
getHandlers, and openExternal to obtain an available port at runtime instead of
hardcoding values like 47654–47658; use a transient net.Server bound to port 0
to read back the assigned port, then pass that port into the redirect config
before asserting getRedirectUrl and exercising open.
🤖 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 `@packages/electron/src/main/oauth-transport.ts`:
- Around line 226-238: The error handler on the server created in
oauth-transport is not being removed correctly because removeListener is using a
different function reference than the one passed to once('error', ...). Update
the createServer/listen flow to assign the error callback to a named const and
use that same reference for both attaching and removing it so the listener is
actually detached after listen succeeds, and keep the server state cleanup logic
intact in the next server setup.

---

Nitpick comments:
In `@packages/electron/src/main/__tests__/oauth-transport.test.ts`:
- Around line 44-50: The OAuth transport tests are using fixed loopback ports,
which can collide on shared CI hosts and make the suite flaky. Update the
affected cases in oauth-transport.test.ts that call
setupOAuthTransportIpcHandlers, getHandlers, and openExternal to obtain an
available port at runtime instead of hardcoding values like 47654–47658; use a
transient net.Server bound to port 0 to read back the assigned port, then pass
that port into the redirect config before asserting getRedirectUrl and
exercising open.
🪄 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 YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 912cc93f-9931-4f20-980a-c9a52bd525ed

📥 Commits

Reviewing files that changed from the base of the PR and between be5588b and c6e7c36.

📒 Files selected for processing (10)
  • .changeset/loopback-oauth-transport.md
  • packages/clerk-js/src/utils/__tests__/authenticateWithTransport.test.ts
  • packages/clerk-js/src/utils/authenticateWithTransport.ts
  • packages/electron/src/index.ts
  • packages/electron/src/main/__tests__/create-clerk-bridge.test.ts
  • packages/electron/src/main/__tests__/oauth-transport.test.ts
  • packages/electron/src/main/create-clerk-bridge.ts
  • packages/electron/src/main/oauth-redirect.ts
  • packages/electron/src/main/oauth-transport.ts
  • packages/electron/src/shared/types.ts

Comment on lines +226 to +238
const next = createServer(requestListener);

next.once('error', error => {
server = null;
reject(error);
});

next.listen(port, host, () => {
next.removeListener('error', reject);
server = next;
resolve();
});
}),

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file slice ---\n'
sed -n '180,270p' packages/electron/src/main/oauth-transport.ts | cat -n

printf '\n--- search once(error) patterns ---\n'
rg -n "once\\('error'|removeListener\\('error'" packages/electron/src/main -S

printf '\n--- node eventemitter probe ---\n'
node - <<'JS'
const { EventEmitter } = require('events');
const ee = new EventEmitter();

function reject(err) { console.log('reject', err.message); }
const onceHandler = err => console.log('once', err.message);

ee.once('error', onceHandler);
console.log('listeners after once', ee.listeners('error').length);
console.log('raw listeners after once', ee.rawListeners('error').length);
console.log('listener identity matches original?', ee.listeners('error')[0] === onceHandler);
console.log('raw listener identity matches original?', ee.rawListeners('error')[0] === onceHandler);
ee.removeListener('error', reject);
console.log('listeners after removeListener(reject)', ee.listeners('error').length);
console.log('raw listeners after removeListener(reject)', ee.rawListeners('error').length);
ee.removeListener('error', onceHandler);
console.log('listeners after removeListener(onceHandler)', ee.listeners('error').length);
console.log('raw listeners after removeListener(onceHandler)', ee.rawListeners('error').length);
JS

Repository: clerk/javascript

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file outline ---'
ast-grep outline packages/electron/src/main/oauth-transport.ts --view expanded || true

echo
echo '--- wider slice around the relevant helper ---'
sed -n '1,340p' packages/electron/src/main/oauth-transport.ts | cat -n

echo
echo '--- EventEmitter listener-removal probe ---'
node - <<'JS'
const { EventEmitter } = require('events');
const ee = new EventEmitter();

function reject(err) { console.log('reject', err.message); }
const onError = err => console.log('onError', err.message);

ee.once('error', onError);
console.log('listeners length after once:', ee.listeners('error').length);
console.log('rawListeners length after once:', ee.rawListeners('error').length);
console.log('listeners[0] === onError:', ee.listeners('error')[0] === onError);
console.log('rawListeners[0] === onError:', ee.rawListeners('error')[0] === onError);

ee.removeListener('error', reject);
console.log('after removeListener(reject): listeners=', ee.listeners('error').length, 'rawListeners=', ee.rawListeners('error').length);

ee.removeListener('error', onError);
console.log('after removeListener(onError): listeners=', ee.listeners('error').length, 'rawListeners=', ee.rawListeners('error').length);
JS

Repository: clerk/javascript

Length of output: 12783


removeListener('error', reject) is a no-op here. The once('error', …) handler stays attached after listen succeeds, so a later server error can still clear server and leave cleanup unable to close it. Store the callback in a named const and remove that same reference.

🤖 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 `@packages/electron/src/main/oauth-transport.ts` around lines 226 - 238, The
error handler on the server created in oauth-transport is not being removed
correctly because removeListener is using a different function reference than
the one passed to once('error', ...). Update the createServer/listen flow to
assign the error callback to a named const and use that same reference for both
attaching and removing it so the listener is actually detached after listen
succeeds, and keep the server state cleanup logic intact in the next server
setup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants