diff --git a/.changeset/tiny-planes-lead.md b/.changeset/tiny-planes-lead.md new file mode 100644 index 00000000000..57e7bc84327 --- /dev/null +++ b/.changeset/tiny-planes-lead.md @@ -0,0 +1,7 @@ +--- +'@clerk/backend': minor +'@clerk/nextjs': minor +'@clerk/astro': minor +--- + +Redirect to tasks on `auth.protect` and `auth.redirectToSignIn` diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index d7a643c62bf..8d2c7dc79b6 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; @@ -38,6 +38,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( await u.po.signIn.continue(); await u.po.expect.toBeSignedIn(); + // Redirects back to tasks when accessing protected route by `auth.protect` + await u.page.goToRelative('/page-protected'); + expect(page.url()).toContain('tasks'); + // Resolves task await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); await u.po.expect.toHaveResolvedTask(); diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts index b9752365115..680d949bc89 100644 --- a/integration/tests/session-tasks-sign-up.test.ts +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; @@ -38,6 +38,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( email: fakeUser.email, password: fakeUser.password, }); + await u.po.expect.toBeSignedIn(); + + // Redirects back to tasks when accessing protected route by `auth.protect` + await u.page.goToRelative('/page-protected'); + expect(page.url()).toContain('tasks'); // Resolves task await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization); diff --git a/packages/astro/src/server/clerk-middleware.ts b/packages/astro/src/server/clerk-middleware.ts index edf7398b4ec..a3a4048818f 100644 --- a/packages/astro/src/server/clerk-middleware.ts +++ b/packages/astro/src/server/clerk-middleware.ts @@ -277,6 +277,7 @@ function decorateAstroLocal(clerkRequest: ClerkRequest, context: APIContext, req publishableKey: getSafeEnv(context).pk!, signInUrl: requestState.signInUrl, signUpUrl: requestState.signUpUrl, + sessionStatus: requestState.toAuth()?.sessionStatus, }).redirectToSignIn({ returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl.toString(), }); @@ -395,6 +396,7 @@ const handleControlFlowErrors = ( signUpUrl: requestState.signUpUrl, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion publishableKey: getSafeEnv(context).pk!, + sessionStatus: requestState.toAuth()?.sessionStatus, }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); default: throw e; diff --git a/packages/backend/src/__tests__/createRedirect.test.ts b/packages/backend/src/__tests__/createRedirect.test.ts index 9823dad795d..82e415e7064 100644 --- a/packages/backend/src/__tests__/createRedirect.test.ts +++ b/packages/backend/src/__tests__/createRedirect.test.ts @@ -1,3 +1,4 @@ +import type { SessionStatusClaim } from '@clerk/types'; import { describe, expect, it, vi } from 'vitest'; import { createRedirect } from '../createRedirect'; @@ -6,242 +7,318 @@ describe('redirect(redirectAdapter)', () => { const returnBackUrl = 'http://current.url:3000/path?q=1#hash'; const encodedUrl = 'http%3A%2F%2Fcurrent.url%3A3000%2Fpath%3Fq%3D1%23hash'; - it('exposes redirectToSignIn / redirectToSignUp', () => { - const helpers = createRedirect({ - redirectAdapter: vi.fn().mockImplementation(() => {}), - publishableKey: '', - baseUrl: 'http://www.clerk.com', + describe.each(['active', 'pending'] satisfies Array)('with %s session status', sessionStatus => { + it('exposes redirectToSignIn / redirectToSignUp', () => { + const helpers = createRedirect({ + redirectAdapter: vi.fn().mockImplementation(() => {}), + publishableKey: '', + baseUrl: 'http://www.clerk.com', + sessionStatus, + }); + expect(Object.keys(helpers).sort()).toEqual(['redirectToSignIn', 'redirectToSignUp']); }); - expect(Object.keys(helpers).sort()).toEqual(['redirectToSignIn', 'redirectToSignUp']); - }); - it('returns path based url with signInUrl as absolute path and returnBackUrl missing', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - signInUrl: 'http://signin.url:3001/sign-in', - signUpUrl: 'http://signin.url:3001/sign-up', - publishableKey: '', + it('raises error without signInUrl and publishableKey in redirectToSignIn', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + signUpUrl: 'http://signin.url:3001/sign-up', + baseUrl: 'http://www.clerk.com', + sessionStatus, + } as any); + + expect(() => redirectToSignIn({ returnBackUrl })).toThrowError( + '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + ); }); - - const result = redirectToSignIn(); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-in`); }); - it('returns path based url with signInUrl as relative path', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - baseUrl: 'http://current.url:3000', - signInUrl: '/sign-in', - signUpUrl: '/sign-up', - publishableKey: '', + describe('with active session status', () => { + it('returns path based url with development publishableKey but without signUpUrl to redirectToSignUp', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.included.katydid-92.lcl.dev/sign-up?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignIn({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://current.url:3000/sign-in?redirect_url=${encodedUrl}`); - }); - - it('returns path based url with signInUrl as absolute path', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - signInUrl: 'http://signin.url:3001/sign-in', - signUpUrl: 'http://signin.url:3001/sign-up', - publishableKey: '', - baseUrl: 'http://www.clerk.com', + it('returns path based url with production publishableKey but without signUpUrl to redirectToSignUp', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.example.com/sign-up?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignIn({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-in?redirect_url=${encodedUrl}`); - }); - - it('raises error without signInUrl and publishableKey in redirectToSignIn', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - signUpUrl: 'http://signin.url:3001/sign-up', - baseUrl: 'http://www.clerk.com', - } as any); - - expect(() => redirectToSignIn({ returnBackUrl })).toThrowError( - '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', - ); - }); - - it('returns path based url with development publishableKey but without signInUrl to redirectToSignIn', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + it('returns path based url with development (kima) publishableKey but without signUpUrl to redirectToSignUp', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://included.katydid-92.accounts.dev/sign-up?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignIn({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=${encodedUrl}`, - ); - }); - - it('returns path based url with production publishableKey but without signInUrl to redirectToSignIn', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + it('passed dev browser when cross-origin redirect in dev', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + devBrowserToken: 'deadbeef', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://included.katydid-92.accounts.dev/sign-up?redirect_url=${encodedUrl}&__clerk_db_jwt=deadbeef`, + ); }); - const result = redirectToSignIn({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`https://accounts.example.com/sign-in?redirect_url=${encodedUrl}`); - }); - - it('returns path based url with development (kima) publishableKey but without signInUrl to redirectToSignIn', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignIn } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + it('returns path based url with development (kima) publishableKey (with staging Clerk) but without signUpUrl to redirectToSignUp', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50c3N0YWdlLmRldiQ', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://included.katydid-92.accountsstage.dev/sign-up?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignIn({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://included.katydid-92.accounts.dev/sign-in?redirect_url=${encodedUrl}`, - ); - }); - - it('returns path based url with signUpUrl as absolute path and returnBackUrl missing', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - signUpUrl: 'http://signin.url:3001/sign-up', - baseUrl: 'http://www.clerk.com', - publishableKey: '', + it('returns path based url with development publishableKey but without signInUrl to redirectToSignIn', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + sessionStatus: 'active', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.included.katydid-92.lcl.dev/sign-in?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignUp(); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-up`); - }); - - it('returns path based url with signUpUrl as relative path', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - signUpUrl: '/sign-up', - baseUrl: 'http://current.url:3000', - publishableKey: '', + it('returns path based url with production publishableKey but without signInUrl to redirectToSignIn', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + sessionStatus: 'active', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.example.com/sign-in?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://current.url:3000/sign-up?redirect_url=${encodedUrl}`); - }); - - it('returns path based url with signUpUrl as absolute path', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - signUpUrl: 'http://signup.url:3001/sign-up', - baseUrl: 'http://www.clerk.com', - publishableKey: '', + it('returns path based url with development (kima) publishableKey but without signInUrl to redirectToSignIn', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + sessionStatus: 'active', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://included.katydid-92.accounts.dev/sign-in?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signup.url:3001/sign-up?redirect_url=${encodedUrl}`); - }); - - it('raises error without signUpUrl and publishableKey in redirectToSignUp', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - redirectAdapter: redirectAdapterSpy, - publishableKey: '', - baseUrl: 'http://www.clerk.com', + it('returns path based url with signUpUrl as absolute path and returnBackUrl missing', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + signUpUrl: 'http://signin.url:3001/sign-up', + baseUrl: 'http://www.clerk.com', + publishableKey: '', + sessionStatus: 'active', + }); + + const result = redirectToSignUp(); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-up`); }); - expect(() => redirectToSignUp({ returnBackUrl })).toThrowError( - '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', - ); - }); - - it('returns path based url with development publishableKey but without signUpUrl to redirectToSignUp', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + it('returns path based url with signUpUrl as relative path', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + signUpUrl: '/sign-up', + baseUrl: 'http://current.url:3000', + publishableKey: '', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://current.url:3000/sign-up?redirect_url=${encodedUrl}`); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://accounts.included.katydid-92.lcl.dev/sign-up?redirect_url=${encodedUrl}`, - ); - }); + it('returns path based url with signUpUrl as absolute path', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + signUpUrl: 'http://signup.url:3001/sign-up', + baseUrl: 'http://www.clerk.com', + publishableKey: '', + sessionStatus: 'active', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signup.url:3001/sign-up?redirect_url=${encodedUrl}`); + }); - it('returns path based url with production publishableKey but without signUpUrl to redirectToSignUp', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + it('raises error without signUpUrl and publishableKey in redirectToSignUp', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + publishableKey: '', + baseUrl: 'http://www.clerk.com', + sessionStatus: 'active', + }); + + expect(() => redirectToSignUp({ returnBackUrl })).toThrowError( + '@clerk/backend: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.', + ); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith(`https://accounts.example.com/sign-up?redirect_url=${encodedUrl}`); - }); + it('returns path based url with signInUrl as absolute path and returnBackUrl missing', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + signInUrl: 'http://signin.url:3001/sign-in', + signUpUrl: 'http://signin.url:3001/sign-up', + publishableKey: '', + sessionStatus: 'active', + }); + + const result = redirectToSignIn(); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-in`); + }); - it('returns path based url with development (kima) publishableKey but without signUpUrl to redirectToSignUp', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + it('returns path based url with signInUrl as relative path', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + baseUrl: 'http://current.url:3000', + signInUrl: '/sign-in', + signUpUrl: '/sign-up', + publishableKey: '', + sessionStatus: 'active', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://current.url:3000/sign-in?redirect_url=${encodedUrl}`); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://included.katydid-92.accounts.dev/sign-up?redirect_url=${encodedUrl}`, - ); + it('returns path based url with signInUrl as absolute path', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + redirectAdapter: redirectAdapterSpy, + signInUrl: 'http://signin.url:3001/sign-in', + signUpUrl: 'http://signin.url:3001/sign-up', + publishableKey: '', + baseUrl: 'http://www.clerk.com', + sessionStatus: 'active', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith(`http://signin.url:3001/sign-in?redirect_url=${encodedUrl}`); + }); }); - it('returns path based url with development (kima) publishableKey (with staging Clerk) but without signUpUrl to redirectToSignUp', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - baseUrl: 'http://www.clerk.com', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50c3N0YWdlLmRldiQ', + describe('with pending session status', () => { + it('on redirectToSignIn, redirects to tasks', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignIn } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA', + sessionStatus: 'pending', + }); + + const result = redirectToSignIn({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.included.katydid-92.lcl.dev/sign-in/tasks?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://included.katydid-92.accountsstage.dev/sign-up?redirect_url=${encodedUrl}`, - ); - }); - - it('passed dev browser when cross-origin redirect in dev', () => { - const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); - const { redirectToSignUp } = createRedirect({ - baseUrl: 'http://www.clerk.com', - devBrowserToken: 'deadbeef', - redirectAdapter: redirectAdapterSpy, - publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + it('on redirectToSignUp, redirects to tasks', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k', + sessionStatus: 'pending', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://accounts.example.com/sign-up/tasks?redirect_url=${encodedUrl}`, + ); }); - const result = redirectToSignUp({ returnBackUrl }); - expect(result).toBe('redirectAdapterValue'); - expect(redirectAdapterSpy).toHaveBeenCalledWith( - `https://included.katydid-92.accounts.dev/sign-up?redirect_url=${encodedUrl}&__clerk_db_jwt=deadbeef`, - ); + it('passed dev browser when cross-origin redirect in dev', () => { + const redirectAdapterSpy = vi.fn().mockImplementation(_url => 'redirectAdapterValue'); + const { redirectToSignUp } = createRedirect({ + baseUrl: 'http://www.clerk.com', + devBrowserToken: 'deadbeef', + redirectAdapter: redirectAdapterSpy, + publishableKey: 'pk_test_aW5jbHVkZWQua2F0eWRpZC05Mi5jbGVyay5hY2NvdW50cy5kZXYk', + sessionStatus: 'pending', + }); + + const result = redirectToSignUp({ returnBackUrl }); + expect(result).toBe('redirectAdapterValue'); + expect(redirectAdapterSpy).toHaveBeenCalledWith( + `https://included.katydid-92.accounts.dev/sign-up/tasks?redirect_url=${encodedUrl}&__clerk_db_jwt=deadbeef`, + ); + }); }); }); diff --git a/packages/backend/src/createRedirect.ts b/packages/backend/src/createRedirect.ts index 6230a7f14fc..ee97a00de82 100644 --- a/packages/backend/src/createRedirect.ts +++ b/packages/backend/src/createRedirect.ts @@ -1,4 +1,5 @@ import { buildAccountsBaseUrl } from '@clerk/shared/buildAccountsBaseUrl'; +import type { SessionStatusClaim } from '@clerk/types'; import { constants } from './constants'; import { errorThrower, parsePublishableKey } from './util/shared'; @@ -70,36 +71,54 @@ type CreateRedirect = (params: { baseUrl: URL | string; signInUrl?: URL | string; signUpUrl?: URL | string; + sessionStatus?: SessionStatusClaim | null; }) => { redirectToSignIn: RedirectFun; redirectToSignUp: RedirectFun; }; export const createRedirect: CreateRedirect = params => { - const { publishableKey, redirectAdapter, signInUrl, signUpUrl, baseUrl } = params; + const { publishableKey, redirectAdapter, signInUrl, signUpUrl, baseUrl, sessionStatus } = params; const parsedPublishableKey = parsePublishableKey(publishableKey); const frontendApi = parsedPublishableKey?.frontendApi; const isDevelopment = parsedPublishableKey?.instanceType === 'development'; const accountsBaseUrl = buildAccountsBaseUrl(frontendApi); + const hasPendingStatus = sessionStatus === 'pending'; + + const redirectToTasks = (url: string | URL, { returnBackUrl }: RedirectToParams) => { + return redirectAdapter( + buildUrl(baseUrl, `${url}/tasks`, returnBackUrl, isDevelopment ? params.devBrowserToken : null), + ); + }; const redirectToSignUp = ({ returnBackUrl }: RedirectToParams = {}) => { if (!signUpUrl && !accountsBaseUrl) { errorThrower.throwMissingPublishableKeyError(); } + const accountsSignUpUrl = `${accountsBaseUrl}/sign-up`; - return redirectAdapter( - buildUrl(baseUrl, signUpUrl || accountsSignUpUrl, returnBackUrl, isDevelopment ? params.devBrowserToken : null), - ); + const targetUrl = signUpUrl || accountsSignUpUrl; + + if (hasPendingStatus) { + return redirectToTasks(targetUrl, { returnBackUrl }); + } + + return redirectAdapter(buildUrl(baseUrl, targetUrl, returnBackUrl, isDevelopment ? params.devBrowserToken : null)); }; const redirectToSignIn = ({ returnBackUrl }: RedirectToParams = {}) => { if (!signInUrl && !accountsBaseUrl) { errorThrower.throwMissingPublishableKeyError(); } + const accountsSignInUrl = `${accountsBaseUrl}/sign-in`; - return redirectAdapter( - buildUrl(baseUrl, signInUrl || accountsSignInUrl, returnBackUrl, isDevelopment ? params.devBrowserToken : null), - ); + const targetUrl = signInUrl || accountsSignInUrl; + + if (hasPendingStatus) { + return redirectToTasks(targetUrl, { returnBackUrl }); + } + + return redirectAdapter(buildUrl(baseUrl, targetUrl, returnBackUrl, isDevelopment ? params.devBrowserToken : null)); }; return { redirectToSignUp, redirectToSignIn }; diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index eddee32a647..1fa1859b3ea 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -99,6 +99,7 @@ export const auth: AuthFn = async () => { publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY, signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL, signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL, + sessionStatus: authObject.sessionStatus, }).redirectToSignIn({ returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl?.toString(), }); diff --git a/packages/nextjs/src/server/clerkMiddleware.ts b/packages/nextjs/src/server/clerkMiddleware.ts index 10b114cb9d5..9b093db3802 100644 --- a/packages/nextjs/src/server/clerkMiddleware.ts +++ b/packages/nextjs/src/server/clerkMiddleware.ts @@ -352,6 +352,7 @@ const handleControlFlowErrors = ( signInUrl: requestState.signInUrl, signUpUrl: requestState.signUpUrl, publishableKey: requestState.publishableKey, + sessionStatus: requestState.toAuth()?.sessionStatus, }).redirectToSignIn({ returnBackUrl: e.returnBackUrl }); } diff --git a/packages/nextjs/src/server/protect.ts b/packages/nextjs/src/server/protect.ts index ad601de804f..c53bb104a5a 100644 --- a/packages/nextjs/src/server/protect.ts +++ b/packages/nextjs/src/server/protect.ts @@ -90,6 +90,13 @@ export function createProtect(opts: { return notFound(); }; + /** + * Redirects the user back to the tasks URL if their session status is pending + */ + if (authObject.sessionStatus === 'pending') { + return handleUnauthenticated(); + } + /** * User is not authenticated */