Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/curly-cameras-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/clerk-js": patch
---

Fix Core 3 OAuth retry routing to the previously selected provider after an abandoned redirect.
12 changes: 5 additions & 7 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1145,13 +1145,11 @@ class SignInFuture implements SignInFutureResource {
routes.actionCompleteRedirectUrl = wrappedRoutes.redirectUrl;
}

if (!this.#resource.id) {
await this._create({
strategy,
...routes,
identifier,
});
}
await this._create({
strategy,
...routes,
identifier,
});

if (strategy === 'enterprise_sso') {
await this.#resource.__internal_basePost({
Expand Down
79 changes: 78 additions & 1 deletion packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,82 @@ describe('SignIn', () => {
});
});

it("creates a fresh sign-in on every sso() call, even when a prior provider's redirect is still pending", async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });

const mockPopup = { location: { href: '' } } as Window;
const mockBuildUrlWithAuth = vi.fn().mockImplementation(url => {
if (url.startsWith('/')) {
return 'https://example.com' + url;
}
return url;
});

SignIn.clerk = {
buildUrlWithAuth: mockBuildUrlWithAuth,
buildUrl: vi.fn().mockImplementation(path => 'https://example.com' + path),
frontendApi: 'clerk.example.com',
__internal_environment: {
displayConfig: {
captchaOauthBypass: [],
},
},
} as any;

const mockFetch = vi.fn();
mockFetch.mockResolvedValueOnce({
client: null,
response: {
id: 'signin_github',
first_factor_verification: {
status: 'unverified',
external_verification_redirect_url: 'https://github.com/login/oauth/authorize',
},
},
});
mockFetch.mockResolvedValueOnce({
client: null,
response: {
id: 'signin_github',
status: 'complete',
},
});
BaseResource._fetch = mockFetch;

vi.mocked(_futureAuthenticateWithPopup).mockImplementation((_clerk, params) => {
params.popup.location.href = params.externalVerificationRedirectURL.toString();
return Promise.resolve();
});

const signIn = new SignIn({
id: 'signin_google',
object: 'sign_in',
status: 'needs_first_factor',
first_factor_verification: {
status: 'unverified',
strategy: 'oauth_google',
external_verification_redirect_url: 'https://accounts.google.com/o/oauth2/auth',
},
} as any);

const result = await signIn.__internal_future.sso({
strategy: 'oauth_github',
redirectUrl: 'https://complete.example.com',
redirectCallbackUrl: '/sso-callback',
popup: mockPopup,
});

expect(result.error).toBeNull();
expect(mockFetch).toHaveBeenNthCalledWith(1, {
method: 'POST',
path: '/client/sign_ins',
body: expect.objectContaining({
strategy: 'oauth_github',
}),
});
expect(mockPopup.location.href).toBe('https://github.com/login/oauth/authorize');
});

it('uses popup when provided', async () => {
vi.stubGlobal('window', { location: { origin: 'https://example.com' } });

Expand Down Expand Up @@ -2198,9 +2274,10 @@ describe('SignIn', () => {
});
BaseResource._fetch = mockFetch;

vi.mocked(_futureAuthenticateWithPopup).mockImplementation(async (_clerk, params) => {
vi.mocked(_futureAuthenticateWithPopup).mockImplementation((_clerk, params) => {
// Simulate the actual behavior of setting popup href
params.popup.location.href = params.externalVerificationRedirectURL.toString();
return Promise.resolve();
});

const signIn = new SignIn();
Expand Down
Loading