diff --git a/src/client/hooks/use-user.integration.test.tsx b/src/client/hooks/use-user.integration.test.tsx
index 9d35594b..c6087f55 100644
--- a/src/client/hooks/use-user.integration.test.tsx
+++ b/src/client/hooks/use-user.integration.test.tsx
@@ -146,4 +146,28 @@ describe("useUser Integration with SWR Cache", () => {
// Verify fetch was called twice
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
+
+ it("should handle unauthenticated requests to the profile endpoint", async () => {
+ fetchSpy.mockResolvedValueOnce(
+ new Response(null, {
+ status: 204
+ })
+ );
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+ new Map() }}>
+ {children}
+
+ );
+
+ const { result } = renderHook(() => useUser(), { wrapper });
+
+ // Wait for the initial data to load
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
+
+ expect(result.current.user).toEqual(null);
+ expect(result.current.error).toBe(undefined);
+ expect(fetchSpy).toHaveBeenCalledOnce();
+ expect(fetchSpy).toHaveBeenCalledWith("/auth/profile");
+ });
});
diff --git a/src/client/hooks/use-user.ts b/src/client/hooks/use-user.ts
index bfb84454..e43273d2 100644
--- a/src/client/hooks/use-user.ts
+++ b/src/client/hooks/use-user.ts
@@ -15,6 +15,11 @@ export function useUser() {
if (!res.ok) {
throw new Error("Unauthorized");
}
+
+ if (res.status === 204) {
+ return null;
+ }
+
return res.json();
})
);
diff --git a/src/server/auth-client.test.ts b/src/server/auth-client.test.ts
index 7d582f94..6d63b905 100644
--- a/src/server/auth-client.test.ts
+++ b/src/server/auth-client.test.ts
@@ -2450,6 +2450,42 @@ ca/T0LLtgmbMmxSv/MmzIg==
expect(response.status).toEqual(401);
expect(response.body).toBeNull();
});
+
+ it("should return a 204 if the user is not authenticated and noContentProfileResponseWhenUnauthenticated is enabled", async () => {
+ const secret = await generateSecret(32);
+ const transactionStore = new TransactionStore({
+ secret
+ });
+ const sessionStore = new StatelessSessionStore({
+ secret
+ });
+ const authClient = new AuthClient({
+ transactionStore,
+ sessionStore,
+
+ domain: DEFAULT.domain,
+ clientId: DEFAULT.clientId,
+ clientSecret: DEFAULT.clientSecret,
+
+ secret,
+ appBaseUrl: DEFAULT.appBaseUrl,
+
+ fetch: getMockAuthorizationServer(),
+
+ noContentProfileResponseWhenUnauthenticated: true
+ });
+
+ const request = new NextRequest(
+ new URL("/auth/profile", DEFAULT.appBaseUrl),
+ {
+ method: "GET"
+ }
+ );
+
+ const response = await authClient.handleProfile(request);
+ expect(response.status).toEqual(204);
+ expect(response.body).toBeNull();
+ });
});
describe("handleCallback", async () => {
diff --git a/src/server/auth-client.ts b/src/server/auth-client.ts
index 5a396425..7d4da359 100644
--- a/src/server/auth-client.ts
+++ b/src/server/auth-client.ts
@@ -136,6 +136,7 @@ export interface AuthClientOptions {
httpTimeout?: number;
enableTelemetry?: boolean;
enableAccessTokenEndpoint?: boolean;
+ noContentProfileResponseWhenUnauthenticated?: boolean;
}
function createRouteUrl(url: string, base: string) {
@@ -173,6 +174,7 @@ export class AuthClient {
private authorizationServerMetadata?: oauth.AuthorizationServer;
private readonly enableAccessTokenEndpoint: boolean;
+ private readonly noContentProfileResponseWhenUnauthenticated: boolean;
constructor(options: AuthClientOptions) {
// dependencies
@@ -262,6 +264,8 @@ export class AuthClient {
};
this.enableAccessTokenEndpoint = options.enableAccessTokenEndpoint ?? true;
+ this.noContentProfileResponseWhenUnauthenticated =
+ options.noContentProfileResponseWhenUnauthenticated ?? false;
}
async handler(req: NextRequest): Promise {
@@ -584,6 +588,12 @@ export class AuthClient {
const session = await this.sessionStore.get(req.cookies);
if (!session) {
+ if (this.noContentProfileResponseWhenUnauthenticated) {
+ return new NextResponse(null, {
+ status: 204
+ });
+ }
+
return new NextResponse(null, {
status: 401
});
diff --git a/src/server/client.ts b/src/server/client.ts
index 2a692a4b..86654d80 100644
--- a/src/server/client.ts
+++ b/src/server/client.ts
@@ -168,6 +168,14 @@ export interface Auth0ClientOptions {
* See: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#name-token-mediating-backend
*/
enableAccessTokenEndpoint?: boolean;
+
+ /**
+ * If true, the profile endpoint will return a 204 No Content response when the user is not authenticated
+ * instead of returning a 401 Unauthorized response.
+ *
+ * Defaults to `false`.
+ */
+ noContentProfileResponseWhenUnauthenticated?: boolean;
}
export type PagesRouterRequest = IncomingMessage | NextApiRequest;
@@ -270,7 +278,9 @@ export class Auth0Client {
allowInsecureRequests: options.allowInsecureRequests,
httpTimeout: options.httpTimeout,
enableTelemetry: options.enableTelemetry,
- enableAccessTokenEndpoint: options.enableAccessTokenEndpoint
+ enableAccessTokenEndpoint: options.enableAccessTokenEndpoint,
+ noContentProfileResponseWhenUnauthenticated:
+ options.noContentProfileResponseWhenUnauthenticated
});
}