Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clerk-js): Authenticate with popup #5239

Merged
merged 41 commits into from
Mar 25, 2025
Merged

feat(clerk-js): Authenticate with popup #5239

merged 41 commits into from
Mar 25, 2025

Conversation

dstaley
Copy link
Member

@dstaley dstaley commented Feb 26, 2025

Description

This PR implements the ability to authenticate with a popup instead of a redirect for external connections. This is useful in environments like Loveable and Bolt where we're unable to render pages like Google inside of an iframe.

It accomplishes this by adding a new prop to SignIn and SignUp called oauthFlow, with the values "redirect" | "popup" | "auto". The "auto" value uses a new utility, originPrefersPopup, to determine whether the given origin is one that likely needs to use the popup flow. This means that generative AI coding tools do not need to make any specific changes to their code for OAuth flows to work as expected. Users are, of course, able to opt into the "popup" flow specifically if they desire, however we maintain that the "redirect" option is a better UX.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

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

Copy link

vercel bot commented Feb 26, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
clerk-js-sandbox ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 25, 2025 9:30pm

@dstaley

This comment was marked as outdated.

@clerk-cookie

This comment was marked as outdated.

Copy link

changeset-bot bot commented Mar 3, 2025

🦋 Changeset detected

Latest commit: 367577b

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

This PR includes changesets to release 22 packages
Name Type
@clerk/clerk-js Minor
@clerk/types Minor
@clerk/chrome-extension Patch
@clerk/clerk-expo Patch
@clerk/agent-toolkit Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/elements Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/localizations Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/clerk-react Patch
@clerk/remix Patch
@clerk/shared Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/themes Patch
@clerk/vue 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

@dstaley

This comment was marked as outdated.

@dstaley dstaley force-pushed the ds.feat/auth-with-popup branch from 1cf0380 to 6b35b89 Compare March 3, 2025 19:37
@dstaley

This comment was marked as outdated.

@dstaley

This comment was marked as outdated.

@clerk-cookie

This comment was marked as outdated.

@dstaley

This comment was marked as outdated.

@clerk-cookie

This comment was marked as outdated.

@dstaley

This comment was marked as outdated.

@clerk-cookie

This comment was marked as outdated.

let shouldRemoveListener = false;

if (event.data.session) {
const existingSession = this.client?.sessions.find(x => x.id === event.data.session) || null;
Copy link
Member

Choose a reason for hiding this comment

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

[nit] we are already checking for existence of this.client on L1856 so no need for the optional chaining here

Copy link
Member

Choose a reason for hiding this comment

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

I think it's because this is an async function that can run with delay which may result to this.client not being defined.

Comment on lines +1475 to +1476
console.log('This can be safely ignored:');
console.error(err);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a significant reason to leave the logs ?

const r = new URL(redirectUrl);
r.searchParams.set('sign_in_force_redirect_url', params.redirectUrlComplete);
r.searchParams.set('sign_up_force_redirect_url', params.redirectUrlComplete);
// All URLs are decorated with the dev browser token in development mode since we're moving between AP and the app.
Copy link
Member

Choose a reason for hiding this comment

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

let shouldRemoveListener = false;

if (event.data.session) {
const existingSession = this.client?.sessions.find(x => x.id === event.data.session) || null;
Copy link
Member

Choose a reason for hiding this comment

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

I think it's because this is an async function that can run with delay which may result to this.client not being defined.

Comment on lines 1887 to 1893
if (!existingSession) {
try {
await this.client?.reload();
} catch (e) {
console.error(e);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this another attempt to recover if stale ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, I noticed in a few instances we were receiving a session ID that wasn't in the return value from this.client.session.

Comment on lines 1894 to 1897
await this.setActive({
session: event.data.session,
redirectUrl: params.redirectUrlComplete,
});
Copy link
Member

Choose a reason for hiding this comment

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

Is it on purpose that we existingSession may not exist, but we are still calling set active with the event data that was provided ?

Comment on lines +1 to +10
const POPUP_PREFERRED_ORIGINS = ['.lovable.app', '.lovableproject.com', '.webcontainer-api.io'];

/**
* Returns `true` if the current origin is one that is typically embedded via an iframe, which would benefit from the
* popup flow.
* @returns {boolean} Whether the current origin prefers the popup flow.
*/
export function originPrefersPopup(): boolean {
return POPUP_PREFERRED_ORIGINS.some(origin => window.location.origin.endsWith(origin));
}
Copy link
Member

Choose a reason for hiding this comment

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

potentially something that could be fetched from /environment, but this feels ok as well.

Copy link
Member

@mwickett mwickett Mar 21, 2025

Choose a reason for hiding this comment

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

Your suggesting we set the POPUP_PREFERRED_DOMAINS on the backend? (if so, I do kind of like that doing it from there would give us the ability to modify without releasing - I guess our hotloading kind of gives us the same capability though)

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, that's why I don't feel strongly about it. Only someone with a pinned version would not benefit from hotloading, but I assume users of these tools won't have a reason to do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yup, Bryce and I discussed this. Ultimately we also concurred that it's likely not that big of a deal where this logic lives. There was some hesitancy from my side on whether it's easier to cut a clerk-js release versus updating the return value from a FAPI endpoint.

Copy link
Member

Choose a reason for hiding this comment

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

We're also maintaining a very similar list of origins for AP frame ancestors.

Not blocking for this PR, but I'll capture the "store and reference in a central place" as an enhancement.


import type { Clerk } from '../core/clerk';

export async function _authenticateWithPopup(
Copy link
Member

Choose a reason for hiding this comment

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

Curious why we are exporting a function with a leading _ ?

Copy link
Member Author

Choose a reason for hiding this comment

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

In our SignIn and SignUp resources, we have a public authenticateWithPopup method available at signIn.authenticateWithPopup. These methods share code, so that's pulled out into the shared _authenticateWithPopup method. I would love a better name if you have any suggestions, but the underscore is to distinguish it from the actual public method.

Copy link
Member

@jacekradko jacekradko left a comment

Choose a reason for hiding this comment

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

No major concerns

@dstaley dstaley enabled auto-merge (squash) March 25, 2025 21:30
@dstaley dstaley merged commit f20dc15 into main Mar 25, 2025
30 checks passed
@dstaley dstaley deleted the ds.feat/auth-with-popup branch March 25, 2025 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants