Skip to content

chore(backend,nextjs): Introduce treatPendingAsSignedOut option to server-side utilities and control components #5756

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
21 changes: 21 additions & 0 deletions .changeset/tender-brooms-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@clerk/nextjs': minor
---

Introduce `treatPendingAsSignedOut` option to `getAuth` and `auth` from `clerkMiddleware`

By default, `treatPendingAsSignedOut` is set to `true`, which means pending sessions are treated as signed-out. You can set this option to `false` to treat pending sessions as authenticated.

```ts
const { userId } = auth({ treatPendingAsSignedOut: false })
```

```ts
const { userId } = getAuth(req, { treatPendingAsSignedOut: false })
```

```tsx
<SignedIn treatPendingAsSignedOut={false}>
User has a session that is either pending (requires tasks resolution) or active
</SignedIn>
```
10 changes: 7 additions & 3 deletions packages/backend/src/tokens/authObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
JwtPayload,
ServerGetToken,
ServerGetTokenOptions,
SessionStatusClaim,
SharedSignedInAuthObjectProperties,
} from '@clerk/types';

Expand Down Expand Up @@ -37,7 +38,7 @@ export type SignedInAuthObject = SharedSignedInAuthObjectProperties & {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
sessionStatus: null;
sessionStatus: SessionStatusClaim | null;
actor: null;
userId: null;
orgId: null;
Expand Down Expand Up @@ -113,11 +114,14 @@ export function signedInAuthObject(
/**
* @internal
*/
export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutAuthObject {
export function signedOutAuthObject(
debugData?: AuthObjectDebugData,
initialSessionStatus?: SessionStatusClaim,
): SignedOutAuthObject {
return {
sessionClaims: null,
sessionId: null,
sessionStatus: null,
sessionStatus: initialSessionStatus ?? null,
userId: null,
actor: null,
orgId: null,
Expand Down
13 changes: 10 additions & 3 deletions packages/backend/src/tokens/authStatus.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { JwtPayload } from '@clerk/types';
import type { JwtPayload, PendingSessionOptions } from '@clerk/types';

import { constants } from '../constants';
import type { TokenVerificationErrorReason } from '../errors';
Expand Down Expand Up @@ -27,7 +27,7 @@ export type SignedInState = {
afterSignInUrl: string;
afterSignUpUrl: string;
isSignedIn: true;
toAuth: () => SignedInAuthObject;
toAuth: (opts?: PendingSessionOptions) => SignedInAuthObject;
headers: Headers;
token: string;
};
Expand Down Expand Up @@ -99,7 +99,14 @@ export function signedIn(
afterSignInUrl: authenticateContext.afterSignInUrl || '',
afterSignUpUrl: authenticateContext.afterSignUpUrl || '',
isSignedIn: true,
toAuth: () => authObject,
// @ts-expect-error The return type is intentionally overridden here to support consumer-facing logic that treats pending sessions as signed out. This override does not affect internal session management like handshake flows.
toAuth: ({ treatPendingAsSignedOut = true } = {}) => {
if (treatPendingAsSignedOut && authObject.sessionStatus === 'pending') {
return signedOutAuthObject(undefined, authObject.sessionStatus);
}

return authObject;
},
headers,
token,
};
Expand Down
7 changes: 4 additions & 3 deletions packages/nextjs/src/app-router/server/auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AuthObject } from '@clerk/backend';
import { constants, createClerkRequest, createRedirect, type RedirectFun } from '@clerk/backend/internal';
import type { PendingSessionOptions } from '@clerk/types';
import { notFound, redirect } from 'next/navigation';

import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants';
Expand Down Expand Up @@ -38,7 +39,7 @@ type Auth = AuthObject & {
};

export interface AuthFn {
(): Promise<Auth>;
(options?: PendingSessionOptions): Promise<Auth>;

/**
* `auth` includes a single property, the `protect()` method, which you can use in two ways:
Expand Down Expand Up @@ -68,7 +69,7 @@ export interface AuthFn {
* - Only works on the server-side, such as in Server Components, Route Handlers, and Server Actions.
* - Requires [`clerkMiddleware()`](https://clerk.com/docs/references/nextjs/clerk-middleware) to be configured.
*/
export const auth: AuthFn = async () => {
export const auth: AuthFn = async ({ treatPendingAsSignedOut } = {}) => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('server-only');

Expand All @@ -89,7 +90,7 @@ export const auth: AuthFn = async () => {
const authObject = await createAsyncGetAuth({
debugLoggerName: 'auth()',
noAuthStatusMessage: authAuthHeaderMissing('auth', await stepsBasedOnSrcDirectory()),
})(request);
})(request, { treatPendingAsSignedOut });

const clerkUrl = getAuthKeyFromRequest(request, 'ClerkUrl');

Expand Down
15 changes: 10 additions & 5 deletions packages/nextjs/src/app-router/server/controlComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import type { ProtectProps } from '@clerk/clerk-react';
import type { PendingSessionOptions } from '@clerk/types';
import React from 'react';

import { auth } from './auth';

export async function SignedIn(props: React.PropsWithChildren): Promise<React.JSX.Element | null> {
export async function SignedIn(
props: React.PropsWithChildren<PendingSessionOptions>,
): Promise<React.JSX.Element | null> {
const { children } = props;
const { userId } = await auth();
const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
return userId ? <>{children}</> : null;
}

export async function SignedOut(props: React.PropsWithChildren): Promise<React.JSX.Element | null> {
export async function SignedOut(
props: React.PropsWithChildren<PendingSessionOptions>,
): Promise<React.JSX.Element | null> {
const { children } = props;
const { userId } = await auth();
const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
return userId ? null : <>{children}</>;
}

Expand All @@ -29,7 +34,7 @@ export async function SignedOut(props: React.PropsWithChildren): Promise<React.J
*/
export async function Protect(props: ProtectProps): Promise<React.JSX.Element | null> {
const { children, fallback, ...restAuthorizedParams } = props;
const { has, userId } = await auth();
const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });

/**
* Fallback to UI provided by user or `null` if authorization checks failed
Expand Down
16 changes: 10 additions & 6 deletions packages/nextjs/src/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AuthObject, ClerkClient } from '@clerk/backend';
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
import { parsePublishableKey } from '@clerk/shared/keys';
import type { PendingSessionOptions } from '@clerk/types';
import { notFound as nextjsNotFound } from 'next/navigation';
import type { NextMiddleware, NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
Expand Down Expand Up @@ -41,7 +42,7 @@ export type ClerkMiddlewareAuthObject = AuthObject & {
};

export interface ClerkMiddlewareAuth {
(): Promise<ClerkMiddlewareAuthObject>;
(opts?: PendingSessionOptions): Promise<ClerkMiddlewareAuthObject>;

protect: AuthProtect;
}
Expand Down Expand Up @@ -182,11 +183,14 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
const redirectToSignUp = createMiddlewareRedirectToSignUp(clerkRequest);
const protect = await createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn);

const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, {
redirectToSignIn,
redirectToSignUp,
});
const authHandler = () => Promise.resolve(authObjWithMethods);
const authHandler = (opts?: PendingSessionOptions) => {
const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(requestState.toAuth(opts), {
redirectToSignIn,
redirectToSignUp,
});

return Promise.resolve(authObjWithMethods);
};
authHandler.protect = protect;

let handlerResult: Response = NextResponse.next();
Expand Down
7 changes: 4 additions & 3 deletions packages/nextjs/src/server/createGetAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AuthObject } from '@clerk/backend';
import { constants } from '@clerk/backend/internal';
import { isTruthy } from '@clerk/shared/underscore';
import type { PendingSessionOptions } from '@clerk/types';

import { withLogger } from '../utils/debugLogger';
import { isNextWithUnstableServerActions } from '../utils/sdk-versions';
Expand All @@ -22,7 +23,7 @@ export const createAsyncGetAuth = ({
noAuthStatusMessage: string;
}) =>
withLogger(debugLoggerName, logger => {
return async (req: RequestLike, opts?: { secretKey?: string }): Promise<AuthObject> => {
return async (req: RequestLike, opts?: { secretKey?: string } & PendingSessionOptions): Promise<AuthObject> => {
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
logger.enable();
}
Expand Down Expand Up @@ -52,7 +53,7 @@ export const createAsyncGetAuth = ({
/**
* Previous known as `createGetAuth`. We needed to create a sync and async variant in order to allow for improvements
* that required dynamic imports (using `require` would not work).
* It powers the synchronous top-level api `getAuh()`.
* It powers the synchronous top-level api `getAuth()`.
*/
export const createSyncGetAuth = ({
debugLoggerName,
Expand All @@ -62,7 +63,7 @@ export const createSyncGetAuth = ({
noAuthStatusMessage: string;
}) =>
withLogger(debugLoggerName, logger => {
return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => {
return (req: RequestLike, opts?: { secretKey?: string } & PendingSessionOptions): AuthObject => {
if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) {
logger.enable();
}
Expand Down
11 changes: 10 additions & 1 deletion packages/nextjs/src/server/data/getAuthDataFromRequest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { AuthObject } from '@clerk/backend';
import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal';
import { decodeJwt } from '@clerk/backend/jwt';
import type { PendingSessionOptions } from '@clerk/types';

import type { LoggerNoCommit } from '../../utils/debugLogger';
import { API_URL, API_VERSION, PUBLISHABLE_KEY, SECRET_KEY } from '../constants';
Expand All @@ -14,7 +15,10 @@ import { assertTokenSignature, decryptClerkRequestData } from '../utils';
*/
export function getAuthDataFromRequest(
req: RequestLike,
opts: { secretKey?: string; logger?: LoggerNoCommit } = {},
{
treatPendingAsSignedOut = true,
...opts
}: { secretKey?: string; logger?: LoggerNoCommit } & PendingSessionOptions = {},
): AuthObject {
const authStatus = getAuthKeyFromRequest(req, 'AuthStatus');
const authToken = getAuthKeyFromRequest(req, 'AuthToken');
Expand All @@ -35,6 +39,7 @@ export function getAuthDataFromRequest(
authStatus,
authMessage,
authReason,
treatPendingAsSignedOut,
};

opts.logger?.debug('auth options', options);
Expand All @@ -53,5 +58,9 @@ export function getAuthDataFromRequest(
authObject = signedInAuthObject(options, jwt.raw.text, jwt.payload);
}

if (treatPendingAsSignedOut && authObject.sessionStatus === 'pending') {
authObject = signedOutAuthObject(options, authObject.sessionStatus);
}
Comment on lines +61 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as the Astro PR, I would like to see this come from @clerk/backend:

getAuthObjectFromJwt(jwt)


return authObject;
}