Skip to content

Commit e8d816a

Browse files
authored
fix(clerk-js): Fix SSO callback for after-auth custom flows (#6430)
1 parent 8c7e5bb commit e8d816a

File tree

7 files changed

+141
-13
lines changed

7 files changed

+141
-13
lines changed

.changeset/metal-geese-double.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/types': patch
3+
---
4+
5+
Fix `UseSessionReturn['session']` JSDocs to not mention active status, since pending sessions are also returned

.changeset/yummy-ghosts-share.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Fix SSO callback for after-auth custom flows

packages/clerk-js/src/core/__tests__/clerk.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,118 @@ describe('Clerk singleton', () => {
923923
mockEnvironmentFetch.mockReset();
924924
});
925925

926+
describe('with after-auth flows', () => {
927+
beforeEach(() => {
928+
mockClientFetch.mockReset();
929+
mockEnvironmentFetch.mockReturnValue(
930+
Promise.resolve({
931+
userSettings: mockUserSettings,
932+
displayConfig: mockDisplayConfig,
933+
isSingleSession: () => false,
934+
isProduction: () => false,
935+
isDevelopmentOrStaging: () => true,
936+
organizationSettings: {
937+
forceOrganizationSelection: true,
938+
},
939+
}),
940+
);
941+
});
942+
943+
it('redirects to pending task', async () => {
944+
const mockSession = {
945+
id: '1',
946+
status: 'pending',
947+
user: {},
948+
tasks: [{ key: 'select-organization' }],
949+
currentTask: { key: 'select-organization', __internal_getUrl: () => 'https://sut/tasks/select-organization' },
950+
lastActiveToken: { getRawString: () => 'mocked-token' },
951+
};
952+
953+
const mockResource = {
954+
...mockSession,
955+
remove: jest.fn(),
956+
touch: jest.fn(() => Promise.resolve()),
957+
getToken: jest.fn(),
958+
reload: jest.fn(() => Promise.resolve(mockSession)),
959+
};
960+
961+
mockResource.touch.mockReturnValueOnce(Promise.resolve());
962+
mockClientFetch.mockReturnValue(
963+
Promise.resolve({
964+
signedInSessions: [mockResource],
965+
signIn: new SignIn(null),
966+
signUp: new SignUp({
967+
status: 'complete',
968+
} as any as SignUpJSON),
969+
}),
970+
);
971+
972+
const mockSetActive = jest.fn();
973+
const mockSignUpCreate = jest
974+
.fn()
975+
.mockReturnValue(Promise.resolve({ status: 'complete', createdSessionId: '123' }));
976+
977+
const sut = new Clerk(productionPublishableKey);
978+
await sut.load(mockedLoadOptions);
979+
if (!sut.client) {
980+
fail('we should always have a client');
981+
}
982+
sut.client.signUp.create = mockSignUpCreate;
983+
sut.setActive = mockSetActive;
984+
985+
await sut.handleRedirectCallback();
986+
987+
await waitFor(() => {
988+
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/tasks/select-organization');
989+
});
990+
});
991+
992+
it('redirects to after sign-in URL when task has been resolved', async () => {
993+
const mockSession = {
994+
id: '1',
995+
status: 'active',
996+
user: {},
997+
lastActiveToken: { getRawString: () => 'mocked-token' },
998+
};
999+
1000+
const mockResource = {
1001+
...mockSession,
1002+
remove: jest.fn(),
1003+
touch: jest.fn(() => Promise.resolve()),
1004+
getToken: jest.fn(),
1005+
reload: jest.fn(() => Promise.resolve(mockSession)),
1006+
};
1007+
1008+
mockResource.touch.mockReturnValueOnce(Promise.resolve());
1009+
mockClientFetch.mockReturnValue(
1010+
Promise.resolve({
1011+
signedInSessions: [mockResource],
1012+
signIn: new SignIn(null),
1013+
signUp: new SignUp(null),
1014+
}),
1015+
);
1016+
1017+
const mockSetActive = jest.fn();
1018+
const mockSignUpCreate = jest
1019+
.fn()
1020+
.mockReturnValue(Promise.resolve({ status: 'complete', createdSessionId: '123' }));
1021+
1022+
const sut = new Clerk(productionPublishableKey);
1023+
await sut.load(mockedLoadOptions);
1024+
if (!sut.client) {
1025+
fail('we should always have a client');
1026+
}
1027+
sut.client.signUp.create = mockSignUpCreate;
1028+
sut.setActive = mockSetActive;
1029+
1030+
await sut.handleRedirectCallback();
1031+
1032+
await waitFor(() => {
1033+
expect(mockNavigate.mock.calls[0][0]).toBe('/');
1034+
});
1035+
});
1036+
});
1037+
9261038
it('creates a new user and calls setActive if the user was not found during sso signup', async () => {
9271039
mockEnvironmentFetch.mockReturnValue(
9281040
Promise.resolve({

packages/clerk-js/src/core/clerk.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1879,10 +1879,11 @@ export class Clerk implements ClerkInterface {
18791879
};
18801880

18811881
if (si.status === 'complete') {
1882-
return this.setActive({
1882+
await this.setActive({
18831883
session: si.sessionId,
18841884
redirectUrl: redirectUrls.getAfterSignInUrl(),
18851885
});
1886+
return this.__internal_navigateToTaskIfAvailable();
18861887
}
18871888

18881889
const userExistsButNeedsToSignIn =
@@ -1892,10 +1893,11 @@ export class Clerk implements ClerkInterface {
18921893
const res = await signIn.create({ transfer: true });
18931894
switch (res.status) {
18941895
case 'complete':
1895-
return this.setActive({
1896+
await this.setActive({
18961897
session: res.createdSessionId,
18971898
redirectUrl: redirectUrls.getAfterSignInUrl(),
18981899
});
1900+
return this.__internal_navigateToTaskIfAvailable();
18991901
case 'needs_first_factor':
19001902
return navigateToFactorOne();
19011903
case 'needs_second_factor':
@@ -1941,10 +1943,11 @@ export class Clerk implements ClerkInterface {
19411943
const res = await signUp.create({ transfer: true });
19421944
switch (res.status) {
19431945
case 'complete':
1944-
return this.setActive({
1946+
await this.setActive({
19451947
session: res.createdSessionId,
19461948
redirectUrl: redirectUrls.getAfterSignUpUrl(),
19471949
});
1950+
return this.__internal_navigateToTaskIfAvailable();
19481951
case 'missing_requirements':
19491952
return navigateToNextStepSignUp({ missingFields: res.missingFields });
19501953
default:
@@ -1953,10 +1956,11 @@ export class Clerk implements ClerkInterface {
19531956
}
19541957

19551958
if (su.status === 'complete') {
1956-
return this.setActive({
1959+
await this.setActive({
19571960
session: su.sessionId,
19581961
redirectUrl: redirectUrls.getAfterSignUpUrl(),
19591962
});
1963+
return this.__internal_navigateToTaskIfAvailable();
19601964
}
19611965

19621966
if (si.status === 'needs_second_factor') {
@@ -1992,6 +1996,10 @@ export class Clerk implements ClerkInterface {
19921996
return navigateToNextStepSignUp({ missingFields: signUp.missingFields });
19931997
}
19941998

1999+
if (this.__internal_hasAfterAuthFlows) {
2000+
return this.__internal_navigateToTaskIfAvailable({ redirectUrlComplete: redirectUrls.getAfterSignInUrl() });
2001+
}
2002+
19952003
return navigateToSignIn();
19962004
};
19972005

packages/clerk-js/src/core/resources/SignIn.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type {
4141
} from '@clerk/types';
4242

4343
import {
44+
buildURL,
4445
generateSignatureWithCoinbaseWallet,
4546
generateSignatureWithMetamask,
4647
generateSignatureWithOKXWallet,
@@ -239,7 +240,10 @@ export class SignIn extends BaseResource implements SignInResource {
239240
// This ensures organization selection tasks are displayed after sign-in,
240241
// rather than redirecting to potentially unprotected pages while the session is pending.
241242
const actionCompleteRedirectUrl = SignIn.clerk.__internal_hasAfterAuthFlows
242-
? redirectUrlWithAuthToken
243+
? buildURL({
244+
base: redirectUrlWithAuthToken,
245+
search: `?redirect_url=${redirectUrlComplete}`,
246+
}).toString()
243247
: redirectUrlComplete;
244248

245249
if (!this.id || !continueSignIn) {

packages/clerk-js/src/ui/common/SSOCallback.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,13 @@ export const SSOCallback = withCardStateProvider<HandleOAuthCallbackParams | Han
1919
});
2020

2121
export const SSOCallbackCard = (props: HandleOAuthCallbackParams | HandleSamlCallbackParams) => {
22-
const { handleRedirectCallback, __internal_setActiveInProgress, __internal_navigateToTaskIfAvailable, session } =
23-
useClerk();
22+
const { handleRedirectCallback, __internal_setActiveInProgress } = useClerk();
2423
const { navigate } = useRouter();
2524
const card = useCardState();
2625

2726
React.useEffect(() => {
2827
let timeoutId: ReturnType<typeof setTimeout>;
2928
if (__internal_setActiveInProgress !== true) {
30-
if (session?.currentTask) {
31-
void __internal_navigateToTaskIfAvailable();
32-
return;
33-
}
34-
3529
const intent = new URLSearchParams(window.location.search).get('intent');
3630
const reloadResource = intent === 'signIn' || intent === 'signUp' ? intent : undefined;
3731
handleRedirectCallback({ ...props, reloadResource }, navigate).catch(e => {

packages/types/src/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export type UseSessionReturn =
180180
*/
181181
isSignedIn: undefined;
182182
/**
183-
* The current active session for the user.
183+
* The current session for the user.
184184
*/
185185
session: undefined;
186186
}

0 commit comments

Comments
 (0)