+ Hello, {{ data.firstName }}
+ {{ error.statusCode }}: {{ error.statusMessage }}
+ Unknown status
+ `,
+ )
+ .commit();
+
+ await app.setup();
+ await app.withEnv(appConfigs.envs.withCustomRoles);
+ await app.dev();
+ });
+
+ test.afterAll(async () => {
+ await app.teardown();
+ });
+
+ test('guard API route with custom middleware', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ const fakeUser = u.services.users.createFakeUser();
+ await u.services.users.createBapiUser(fakeUser);
+
+ // Verify unauthorized access is blocked
+ await u.page.goToAppHome();
+ await u.po.expect.toBeSignedOut();
+ await u.page.goToRelative('/me');
+ await expect(u.page.getByText('401: You are not authorized to access this resource')).toBeVisible();
+
+ // Sign in flow
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+ await u.page.waitForAppUrl('/');
+
+ // Verify authorized access works
+ await u.page.goToRelative('/me');
+ await expect(u.page.getByText(`Hello, ${fakeUser.firstName}`)).toBeVisible();
+
+ await fakeUser.deleteIfExists();
+ });
+});
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index f199d27dc3..cab6860a0c 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -46,7 +46,8 @@
"lint": "eslint src/",
"lint:attw": "attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm",
"lint:publint": "publint",
- "publish:local": "pnpm yalc push --replace --sig"
+ "publish:local": "pnpm yalc push --replace --sig",
+ "test": "vitest"
},
"dependencies": {
"@clerk/backend": "workspace:*",
diff --git a/packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts b/packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts
new file mode 100644
index 0000000000..89766f0b1e
--- /dev/null
+++ b/packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts
@@ -0,0 +1,106 @@
+import { createApp, eventHandler, setResponseHeader, toWebHandler } from 'h3';
+import { vi } from 'vitest';
+
+import { clerkMiddleware } from '../clerkMiddleware';
+
+const AUTH_RESPONSE = {
+ userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
+ sessionId: 'sess_2jZSstSbxtTndD9P7q4kDl0VVZa',
+};
+
+const MOCK_OPTIONS = {
+ secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx',
+ publishableKey: 'pk_test_xxxxxxxxxxxxx',
+ signInUrl: '/foo',
+ signUpUrl: '/bar',
+};
+
+vi.mock('#imports', () => {
+ return {
+ useRuntimeConfig: () => ({}),
+ };
+});
+
+const authenticateRequestMock = vi.fn().mockResolvedValue({
+ toAuth: () => AUTH_RESPONSE,
+ headers: new Headers(),
+});
+
+vi.mock('../clerkClient', () => {
+ return {
+ clerkClient: () => ({
+ authenticateRequest: authenticateRequestMock,
+ telemetry: { record: vi.fn() },
+ }),
+ };
+});
+
+describe('clerkMiddleware(params)', () => {
+ test('renders route as normally when used without params', async () => {
+ const app = createApp();
+ const handler = toWebHandler(app);
+ app.use(clerkMiddleware());
+ app.use(
+ '/',
+ eventHandler(event => event.context.auth),
+ );
+ const response = await handler(new Request(new URL('/', 'http://localhost')));
+
+ expect(response.status).toBe(200);
+ expect(await response.json()).toEqual(AUTH_RESPONSE);
+ });
+
+ test('renders route as normally when used with options param', async () => {
+ const app = createApp();
+ const handler = toWebHandler(app);
+ app.use(clerkMiddleware(MOCK_OPTIONS));
+ app.use(
+ '/',
+ eventHandler(event => event.context.auth),
+ );
+ const response = await handler(new Request(new URL('/', 'http://localhost')));
+
+ expect(response.status).toBe(200);
+ expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS));
+ expect(await response.json()).toEqual(AUTH_RESPONSE);
+ });
+
+ test('executes handler and renders route when used with a custom handler', async () => {
+ const app = createApp();
+ const handler = toWebHandler(app);
+ app.use(
+ clerkMiddleware(event => {
+ setResponseHeader(event, 'a-custom-header', '1');
+ }),
+ );
+ app.use(
+ '/',
+ eventHandler(event => event.context.auth),
+ );
+ const response = await handler(new Request(new URL('/', 'http://localhost')));
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get('a-custom-header')).toBe('1');
+ expect(await response.json()).toEqual(AUTH_RESPONSE);
+ });
+
+ test('executes handler and renders route when used with a custom handler and options', async () => {
+ const app = createApp();
+ const handler = toWebHandler(app);
+ app.use(
+ clerkMiddleware(event => {
+ setResponseHeader(event, 'a-custom-header', '1');
+ }, MOCK_OPTIONS),
+ );
+ app.use(
+ '/',
+ eventHandler(event => event.context.auth),
+ );
+ const response = await handler(new Request(new URL('/', 'http://localhost')));
+
+ expect(response.status).toBe(200);
+ expect(response.headers.get('a-custom-header')).toBe('1');
+ expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS));
+ expect(await response.json()).toEqual(AUTH_RESPONSE);
+ });
+});
diff --git a/packages/nuxt/src/runtime/server/clerkMiddleware.ts b/packages/nuxt/src/runtime/server/clerkMiddleware.ts
index 620182a9ea..8d38aa53fc 100644
--- a/packages/nuxt/src/runtime/server/clerkMiddleware.ts
+++ b/packages/nuxt/src/runtime/server/clerkMiddleware.ts
@@ -1,31 +1,65 @@
import type { AuthObject } from '@clerk/backend';
+import type { AuthenticateRequestOptions } from '@clerk/backend/internal';
import { AuthStatus, constants } from '@clerk/backend/internal';
-import type { H3Event } from 'h3';
+import { eventMethodCalled } from '@clerk/shared/telemetry';
+import type { EventHandler } from 'h3';
import { createError, eventHandler, setResponseHeader } from 'h3';
import { clerkClient } from './clerkClient';
import { createInitialState, toWebRequest } from './utils';
-type Handler = (event: H3Event) => void;
+function parseHandlerAndOptions(args: unknown[]) {
+ return [
+ typeof args[0] === 'function' ? args[0] : undefined,
+ (args.length === 2 ? args[1] : typeof args[0] === 'function' ? {} : args[0]) || {},
+ ] as [EventHandler | undefined, AuthenticateRequestOptions];
+}
+
+interface ClerkMiddleware {
+ /**
+ * @example
+ * export default clerkMiddleware((event) => { ... }, options);
+ */
+ (handler: EventHandler, options?: AuthenticateRequestOptions): ReturnType