Skip to content

feat: add redirectToSignUp() to ClerkMiddlewareAuthObject #5407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
18 changes: 18 additions & 0 deletions .changeset/giant-items-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@clerk/nextjs': patch
---

- Introduce `auth().redirectToSignUp()` that can be used in API routes and pages, eg
```ts
import { auth } from '@clerk/nextjs/server';

export const Layout = ({ children }) => {
const { userId } = auth();

if (!userId) {
return auth().redirectToSignUp();
}

return <>{children}</>;
};
```
33 changes: 32 additions & 1 deletion packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ type Auth = AuthObject & {
* `auth()` on the server-side can only access redirect URLs defined via [environment variables](https://clerk.com/docs/deployments/clerk-environment-variables#sign-in-and-sign-up-redirects) or [`clerkMiddleware` dynamic keys](https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys).
*/
redirectToSignIn: RedirectFun<ReturnType<typeof redirect>>;

/**
* The `auth()` helper returns the `redirectToSignUp()` method, which you can use to redirect the user to the sign-up page.
*
* @param [returnBackUrl] {string | URL} - The URL to redirect the user back to after they sign up.
*
* @note
* `auth()` on the server-side can only access redirect URLs defined via [environment variables](https://clerk.com/docs/deployments/clerk-environment-variables#sign-in-and-sign-up-redirects) or [`clerkMiddleware` dynamic keys](https://clerk.com/docs/references/nextjs/clerk-middleware#dynamic-keys).
*/
redirectToSignUp: RedirectFun<ReturnType<typeof redirect>>;
};

export interface AuthFn {
Expand Down Expand Up @@ -104,7 +114,28 @@ export const auth: AuthFn = async () => {
});
};

return Object.assign(authObject, { redirectToSignIn });
const redirectToSignUp: RedirectFun<never> = (opts = {}) => {
const clerkRequest = createClerkRequest(request);
const devBrowserToken =
clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) ||
clerkRequest.cookies.get(constants.Cookies.DevBrowser);

const encryptedRequestData = getHeader(request, constants.Headers.ClerkRequestData);
const decryptedRequestData = decryptClerkRequestData(encryptedRequestData);

return createRedirect({
redirectAdapter: redirect,
devBrowserToken: devBrowserToken,
baseUrl: clerkRequest.clerkUrl.toString(),
publishableKey: decryptedRequestData.publishableKey || PUBLISHABLE_KEY,
signInUrl: decryptedRequestData.signInUrl || SIGN_IN_URL,
signUpUrl: decryptedRequestData.signUpUrl || SIGN_UP_URL,
}).redirectToSignUp({
returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl?.toString(),
});
};

return Object.assign(authObject, { redirectToSignIn, redirectToSignUp });
};

auth.protect = async (...args: any[]) => {
Expand Down
75 changes: 75 additions & 0 deletions packages/nextjs/src/server/__tests__/clerkMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,81 @@ describe('clerkMiddleware(params)', () => {
});
});

describe('auth().redirectToSignUp()', () => {
it('redirects to sign-up url when redirectToSignUp is called and the request is a page request', async () => {
const req = mockRequest({
url: '/protected',
headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }),
appendDevBrowserCookie: true,
});

const resp = await clerkMiddleware(async auth => {
const { redirectToSignUp } = await auth();
redirectToSignUp();
})(req, {} as NextFetchEvent);

expect(resp?.status).toEqual(307);
expect(resp?.headers.get('location')).toContain('sign-up');
expect((await clerkClient()).authenticateRequest).toBeCalled();
});

it('redirects to sign-up url when redirectToSignUp is called with the correct returnBackUrl', async () => {
const req = mockRequest({
url: '/protected',
headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }),
appendDevBrowserCookie: true,
});

const resp = await clerkMiddleware(async auth => {
const { redirectToSignUp } = await auth();
redirectToSignUp();
})(req, {} as NextFetchEvent);

expect(resp?.status).toEqual(307);
expect(resp?.headers.get('location')).toContain('sign-up');
expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toContain('/protected');
expect((await clerkClient()).authenticateRequest).toBeCalled();
});

it('redirects to sign-up url with redirect_url set to the provided returnBackUrl param', async () => {
const req = mockRequest({
url: '/protected',
headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }),
appendDevBrowserCookie: true,
});

const resp = await clerkMiddleware(async auth => {
const { redirectToSignUp } = await auth();
redirectToSignUp({ returnBackUrl: 'https://www.clerk.com/hello' });
})(req, {} as NextFetchEvent);

expect(resp?.status).toEqual(307);
expect(resp?.headers.get('location')).toContain('sign-up');
expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toEqual(
'https://www.clerk.com/hello',
);
expect((await clerkClient()).authenticateRequest).toBeCalled();
});

it('redirects to sign-up url without a redirect_url when returnBackUrl is null', async () => {
const req = mockRequest({
url: '/protected',
headers: new Headers({ [constants.Headers.SecFetchDest]: 'document' }),
appendDevBrowserCookie: true,
});

const resp = await clerkMiddleware(async auth => {
const { redirectToSignUp } = await auth();
redirectToSignUp({ returnBackUrl: null });
})(req, {} as NextFetchEvent);

expect(resp?.status).toEqual(307);
expect(resp?.headers.get('location')).toContain('sign-up');
expect(new URL(resp!.headers.get('location')!).searchParams.get('redirect_url')).toBeNull();
expect((await clerkClient()).authenticateRequest).toBeCalled();
});
});

describe('auth.protect()', () => {
it('redirects to sign-in url when protect is called, the user is signed out and the request is a page request', async () => {
const req = mockRequest({
Expand Down
28 changes: 27 additions & 1 deletion packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
isNextjsNotFoundError,
isNextjsRedirectError,
isRedirectToSignInError,
isRedirectToSignUpError,
nextjsRedirectError,
redirectToSignInError,
redirectToSignUpError,
} from './nextErrors';
import type { AuthProtect } from './protect';
import { createProtect } from './protect';
Expand All @@ -34,6 +36,7 @@ import {

export type ClerkMiddlewareAuthObject = AuthObject & {
redirectToSignIn: RedirectFun<Response>;
redirectToSignUp: RedirectFun<Response>;
};

export interface ClerkMiddlewareAuth {
Expand Down Expand Up @@ -172,9 +175,13 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
logger.debug('auth', () => ({ auth: authObject, debug: authObject.debug() }));

const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest);
const redirectToSignUp = createMiddlewareRedirectToSignUp(clerkRequest);
const protect = await createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn);

const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { redirectToSignIn });
const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, {
redirectToSignIn,
redirectToSignUp,
});
const authHandler = () => Promise.resolve(authObjWithMethods);
authHandler.protect = protect;

Expand Down Expand Up @@ -311,6 +318,15 @@ const createMiddlewareRedirectToSignIn = (
};
};

const createMiddlewareRedirectToSignUp = (
clerkRequest: ClerkRequest,
): ClerkMiddlewareAuthObject['redirectToSignUp'] => {
return (opts = {}) => {
const url = clerkRequest.clerkUrl.toString();
redirectToSignUpError(url, opts.returnBackUrl);
};
};

const createMiddlewareProtect = (
clerkRequest: ClerkRequest,
authObject: AuthObject,
Expand Down Expand Up @@ -363,6 +379,16 @@ const handleControlFlowErrors = (
}).redirectToSignIn({ returnBackUrl: e.returnBackUrl });
}

if (isRedirectToSignUpError(e)) {
return createRedirect({
redirectAdapter,
baseUrl: clerkRequest.clerkUrl,
signInUrl: requestState.signInUrl,
signUpUrl: requestState.signUpUrl,
publishableKey: requestState.publishableKey,
}).redirectToSignUp({ returnBackUrl: e.returnBackUrl });
}

if (isNextjsRedirectError(e)) {
return redirectAdapter(e.redirectUrl);
}
Expand Down
18 changes: 18 additions & 0 deletions packages/nextjs/src/server/nextErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
const CONTROL_FLOW_ERROR = {
REDIRECT_TO_URL: 'CLERK_PROTECT_REDIRECT_TO_URL',
REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN',
REDIRECT_TO_SIGN_UP: 'CLERK_PROTECT_REDIRECT_TO_SIGN_UP',
};

/**
Expand Down Expand Up @@ -99,6 +100,13 @@ function redirectToSignInError(url: string, returnBackUrl?: string | URL | null)
});
}

function redirectToSignUpError(url: string, returnBackUrl?: string | URL | null): never {
nextjsRedirectError(url, {
clerk_digest: CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_UP,
returnBackUrl: returnBackUrl === null ? '' : returnBackUrl || url,
});
}

/**
* Checks an error to determine if it's an error generated by the
* `redirect(url)` helper.
Expand Down Expand Up @@ -135,11 +143,21 @@ function isRedirectToSignInError(error: unknown): error is RedirectError<{ retur
return false;
}

function isRedirectToSignUpError(error: unknown): error is RedirectError<{ returnBackUrl: string | URL }> {
if (isNextjsRedirectError(error) && 'clerk_digest' in error) {
return error.clerk_digest === CONTROL_FLOW_ERROR.REDIRECT_TO_SIGN_UP;
}

return false;
}

export {
isNextjsNotFoundError,
isLegacyNextjsNotFoundError,
redirectToSignInError,
redirectToSignUpError,
nextjsRedirectError,
isNextjsRedirectError,
isRedirectToSignInError,
isRedirectToSignUpError,
};