Skip to content
Merged
7 changes: 7 additions & 0 deletions .changeset/tiny-planes-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
'@clerk/astro': minor
---

Redirect to tasks on `auth.protect` and `auth.redirectToSignIn`
6 changes: 5 additions & 1 deletion integration/tests/session-tasks-sign-in.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
Expand Down Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion integration/tests/session-tasks-sign-up.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';

import { appConfigs } from '../presets';
import type { FakeUser } from '../testUtils';
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/server/clerk-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand Down Expand Up @@ -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;
Expand Down
477 changes: 277 additions & 200 deletions packages/backend/src/__tests__/createRedirect.test.ts

Large diffs are not rendered by default.

33 changes: 26 additions & 7 deletions packages/backend/src/createRedirect.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -70,36 +71,54 @@ type CreateRedirect = <ReturnType>(params: {
baseUrl: URL | string;
signInUrl?: URL | string;
signUpUrl?: URL | string;
sessionStatus?: SessionStatusClaim | null;
}) => {
redirectToSignIn: RedirectFun<ReturnType>;
redirectToSignUp: RedirectFun<ReturnType>;
};

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 };
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ const handleControlFlowErrors = (
signInUrl: requestState.signInUrl,
signUpUrl: requestState.signUpUrl,
publishableKey: requestState.publishableKey,
sessionStatus: requestState.toAuth()?.sessionStatus,
}).redirectToSignIn({ returnBackUrl: e.returnBackUrl });
}

Expand Down
7 changes: 7 additions & 0 deletions packages/nextjs/src/server/protect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down