Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modal-with-signup-sso-callback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Fixed OAuth sign-ups from `openSignIn({ withSignUp: true })` landing on the sign-in page with an "External Account was not found" error instead of creating the account.
43 changes: 39 additions & 4 deletions integration/tests/oauth-flows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo
test.describe.configure({ mode: 'serial' });

let fakeUser: FakeUser;
let modalSignUpUser: FakeUser;

test.beforeAll(async () => {
// Create a clerkClient for the OAuth provider instance.
Expand All @@ -30,16 +31,22 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo
fakeUser = users.createFakeUser(test, {
withUsername: true,
});
modalSignUpUser = users.createFakeUser(test, {
withUsername: true,
});
// Create the user on the OAuth provider instance so we do not need to sign up twice.
await users.createBapiUser(fakeUser);
await users.createBapiUser(modalSignUpUser);
});

test.afterAll(async () => {
const u = createTestUtils({ app });
// Delete the user on the OAuth provider instance.
await fakeUser.deleteIfExists();
await modalSignUpUser.deleteIfExists();
// Delete the user on the app instance.
await u.services.users.deleteIfExists({ email: fakeUser.email });
await u.services.users.deleteIfExists({ email: modalSignUpUser.email });
await app.teardown();
});

Expand Down Expand Up @@ -103,6 +110,33 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('oauth flo
await u.po.expect.toBeSignedIn();
});

test('openSignIn with withSignUp signs up a new user with custom oauth provider', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/buttons');
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.page.evaluate(() => {
(window as any).Clerk.openSignIn({
withSignUp: true,
forceRedirectUrl: '/protected',
signUpForceRedirectUrl: '/protected',
});
});
await u.po.signIn.waitForModal();
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
await u.page.getByText('Sign in to oauth-provider').waitFor();

await u.po.signIn.setIdentifier(modalSignUpUser.email);
await u.po.signIn.continue();
await u.po.signIn.enterTestOtpCode();
await grantOAuthConsent(u.page);

await u.page.waitForAppUrl('/protected');
await u.po.expect.toBeSignedIn();
});

test('openSignIn OAuth uses ClerkProvider.signInUrl for sso-callback', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

Expand Down Expand Up @@ -239,7 +273,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('oauth
await app.teardown();
});

test('openSignIn OAuth in combined flow targets /sign-in#/create/sso-callback', async ({ page, context }) => {
test('openSignIn OAuth in combined flow targets /sign-in#/sso-callback', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });

await u.page.goToRelative('/buttons');
Expand All @@ -263,13 +297,14 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSignInOrUpFlow] })('oauth
expect(redirectUrl).toBeTruthy();

// Combined flow (CLERK_SIGN_UP_URL is unset in this env): the sso-callback must anchor to
// ClerkProvider.signInUrl and carry the combined-flow /create segment, since the
// create/sso-callback route is mounted under the SignIn tree — not SignUp.
// ClerkProvider.signInUrl. The modal cannot know whether the SignIn mounted at that URL has the
// combined-flow `create/*` routes, so it targets `sso-callback`, which every SignIn mounts and
// whose handler transfers new users into a sign-up itself.
const parsed = new URL(redirectUrl!);
const appOrigin = new URL(app.serverUrl).origin;
expect(parsed.origin).toBe(appOrigin);
expect(parsed.pathname).toBe('/sign-in');
expect(parsed.hash).toMatch(/^#\/create\/sso-callback/);
expect(parsed.hash).toMatch(/^#\/sso-callback/);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,37 @@ describe('SignInSocialButtons', () => {
openSpy.mockRestore();
});

it('with virtual routing and withSignUp, redirects back to the sign-in sso-callback route', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withSocialProvider({ provider: 'google' });
});
props.setProps({ routing: 'virtual', withSignUp: true, oauthFlow: 'redirect' } as any);
fixtures.signIn.authenticateWithRedirect.mockResolvedValue(undefined as any);

const { userEvent } = render(
<CardStateProvider>
<SignInSocialButtons
enableOAuthProviders
enableWeb3Providers={false}
enableAlternativePhoneCodeProviders={false}
/>
</CardStateProvider>,
{ wrapper },
);

await userEvent.click(screen.getByText('Continue with Google'));

await waitFor(() => {
expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalled();
});
expect(fixtures.signIn.authenticateWithRedirect).toHaveBeenCalledWith(
expect.objectContaining({
strategy: 'oauth_google',
redirectUrl: 'https://dashboard.clerk.com/sign-in#/sso-callback',
}),
);
});

it('with a transport registered, clears loading when authenticateWithRedirect rejects', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withSocialProvider({ provider: 'google' });
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/contexts/components/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ export const useSignInContext = (): SignInContextType => {
baseUrl: signInUrl,
authQueryString,
path: ctx.path,
endpoint: isCombinedFlow ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE,
endpoint:
isCombinedFlow && ctx.routing !== 'virtual' ? '/create' + SSO_CALLBACK_PATH_ROUTE : SSO_CALLBACK_PATH_ROUTE,
});

if (isCombinedFlow) {
Expand Down
Loading