Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .changeset/cool-guests-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'@clerk/nuxt': minor
---

Introduces machine authentication, supporting four token types: `api_key`, `oauth_token`, `machine_token`, and `session_token`. For backwards compatibility, `session_token` remains the default when no token type is specified. This enables machine-to-machine authentication and use cases such as API keys and OAuth integrations. Existing applications continue to work without modification.

You can specify which token types are allowed by using the `acceptsToken` option in the `event.context.auth()` context. This option can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.

Example usage:

```ts
export default eventHandler((event) => {
const auth = event.locals.auth({ acceptsToken: 'any' })

if (authObject.tokenType === 'session_token') {
console.log('this is session token from a user')
} else {
console.log('this is some other type of machine token')
console.log('more specifically, a ' + authObject.tokenType)
}

return {}
})
```
8 changes: 3 additions & 5 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,11 @@ export default defineNuxtModule<ModuleOptions>({
{
filename: 'types/clerk.d.ts',
getContents: () => `import type { SessionAuthObject } from '@clerk/backend';
declare module 'h3' {
type AuthObjectHandler = SessionAuthObject & {
(): SessionAuthObject;
}
import type { AuthFn } from '@clerk/nuxt/server';

declare module 'h3' {
interface H3EventContext {
auth: AuthObjectHandler;
auth: SessionAuthObject & AuthFn;
}
}
`,
Expand Down
56 changes: 56 additions & 0 deletions packages/nuxt/src/runtime/server/__tests__/auth.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { AuthObject, InvalidTokenAuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
import { expectTypeOf, test } from 'vitest';

import type { AuthFn } from '../types';

test('infers the correct AuthObject type for each accepted token type', () => {
// Mock event object
const event = {
locals: {
auth: (() => {}) as AuthFn,
},
};

// Session token by default
expectTypeOf(event.locals.auth()).toMatchTypeOf<SessionAuthObject>();

// Individual token types
expectTypeOf(event.locals.auth({ acceptsToken: 'session_token' })).toMatchTypeOf<SessionAuthObject>();
expectTypeOf(event.locals.auth({ acceptsToken: 'api_key' })).toMatchTypeOf<MachineAuthObject<'api_key'>>();
expectTypeOf(event.locals.auth({ acceptsToken: 'machine_token' })).toMatchTypeOf<
MachineAuthObject<'machine_token'>
>();
expectTypeOf(event.locals.auth({ acceptsToken: 'oauth_token' })).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();

// Array of token types
expectTypeOf(event.locals.auth({ acceptsToken: ['session_token', 'machine_token'] })).toMatchTypeOf<
SessionAuthObject | MachineAuthObject<'machine_token'> | InvalidTokenAuthObject
>();
expectTypeOf(event.locals.auth({ acceptsToken: ['machine_token', 'oauth_token'] })).toMatchTypeOf<
MachineAuthObject<'machine_token' | 'oauth_token'> | InvalidTokenAuthObject
>();

// Any token type
expectTypeOf(event.locals.auth({ acceptsToken: 'any' })).toMatchTypeOf<AuthObject>();
});

test('verifies discriminated union works correctly with acceptsToken: any', () => {
// Mock event object
const event = {
locals: {
auth: (() => {}) as AuthFn,
},
};

const auth = event.locals.auth({ acceptsToken: 'any' });

if (auth.tokenType === 'session_token') {
expectTypeOf(auth).toMatchTypeOf<SessionAuthObject>();
} else if (auth.tokenType === 'api_key') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'api_key'>>();
} else if (auth.tokenType === 'machine_token') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'machine_token'>>();
} else if (auth.tokenType === 'oauth_token') {
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();
}
});
136 changes: 130 additions & 6 deletions packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,31 @@ import { vi } from 'vitest';

import { clerkMiddleware } from '../clerkMiddleware';

const AUTH_RESPONSE = {
const SESSION_AUTH_RESPONSE = {
userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
sessionId: 'sess_2jZSstSbxtTndD9P7q4kDl0VVZa',
tokenType: 'session_token',
isAuthenticated: true,
sessionStatus: 'active',
sessionClaims: {},
actor: null,
factorVerificationAge: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
};

const MACHINE_AUTH_RESPONSE = {
id: 'ak_123456789',
subject: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
scopes: ['read:users', 'write:users'],
tokenType: 'api_key',
isAuthenticated: true,
name: 'Test API Key',
claims: { custom: 'claim' },
userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
orgId: null,
};

const MOCK_OPTIONS = {
Expand All @@ -22,7 +44,7 @@ vi.mock('#imports', () => {
});

const authenticateRequestMock = vi.fn().mockResolvedValue({
toAuth: () => AUTH_RESPONSE,
toAuth: () => SESSION_AUTH_RESPONSE,
headers: new Headers(),
});

Expand All @@ -47,7 +69,7 @@ describe('clerkMiddleware(params)', () => {
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
expect(await response.json()).toEqual(AUTH_RESPONSE);
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
});

test('renders route as normally when used with options param', async () => {
Expand All @@ -62,7 +84,7 @@ describe('clerkMiddleware(params)', () => {

expect(response.status).toBe(200);
expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS));
expect(await response.json()).toEqual(AUTH_RESPONSE);
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
});

test('executes handler and renders route when used with a custom handler', async () => {
Expand All @@ -81,7 +103,7 @@ describe('clerkMiddleware(params)', () => {

expect(response.status).toBe(200);
expect(response.headers.get('a-custom-header')).toBe('1');
expect(await response.json()).toEqual(AUTH_RESPONSE);
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
});

test('executes handler and renders route when used with a custom handler and options', async () => {
Expand All @@ -101,6 +123,108 @@ describe('clerkMiddleware(params)', () => {
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);
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
});

describe('machine authentication', () => {
test('returns machine auth object when acceptsToken is machine token type', async () => {
authenticateRequestMock.mockResolvedValueOnce({
toAuth: () => MACHINE_AUTH_RESPONSE,
headers: new Headers(),
});

const app = createApp();
const handler = toWebHandler(app);
app.use(clerkMiddleware());
app.use(
'/',
eventHandler(event => event.context.auth({ acceptsToken: 'api_key' })),
);
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
});

test('returns machine auth object when acceptsToken array includes machine token type', async () => {
authenticateRequestMock.mockResolvedValueOnce({
toAuth: () => MACHINE_AUTH_RESPONSE,
headers: new Headers(),
});

const app = createApp();
const handler = toWebHandler(app);
app.use(clerkMiddleware());
app.use(
'/',
eventHandler(event => event.context.auth({ acceptsToken: ['session_token', 'api_key'] })),
);
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
});

test('returns any auth object when acceptsToken is any', async () => {
authenticateRequestMock.mockResolvedValueOnce({
toAuth: () => MACHINE_AUTH_RESPONSE,
headers: new Headers(),
});

const app = createApp();
const handler = toWebHandler(app);
app.use(clerkMiddleware());
app.use(
'/',
eventHandler(event => event.context.auth({ acceptsToken: 'any' })),
);
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
});

test('returns unauthenticated machine object when token type does not match acceptsToken', async () => {
authenticateRequestMock.mockResolvedValueOnce({
toAuth: () => MACHINE_AUTH_RESPONSE,
headers: new Headers(),
});

const app = createApp();
const handler = toWebHandler(app);
app.use(clerkMiddleware());
app.use(
'/',
eventHandler(event => event.context.auth({ acceptsToken: 'machine_token' })),
);
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
const result = await response.json();
expect(result.tokenType).toBe('machine_token');
expect(result.isAuthenticated).toBe(false);
expect(result.id).toBe(null);
});

test('returns invalid token object when token type is not in acceptsToken array', async () => {
authenticateRequestMock.mockResolvedValueOnce({
toAuth: () => MACHINE_AUTH_RESPONSE,
headers: new Headers(),
});

const app = createApp();
const handler = toWebHandler(app);
app.use(clerkMiddleware());
app.use(
'/',
eventHandler(event => event.context.auth({ acceptsToken: ['session_token', 'machine_token'] })),
);
const response = await handler(new Request(new URL('/', 'http://localhost')));

expect(response.status).toBe(200);
const result = await response.json();
expect(result.tokenType).toBe(null);
expect(result.isAuthenticated).toBe(false);
});
});
});
23 changes: 16 additions & 7 deletions packages/nuxt/src/runtime/server/clerkMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { AuthenticateRequestOptions } from '@clerk/backend/internal';
import { AuthStatus, constants } from '@clerk/backend/internal';
import { AuthStatus, constants, getAuthObjectForAcceptedToken, TokenType } from '@clerk/backend/internal';
import { deprecated } from '@clerk/shared/deprecated';
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
import type { EventHandler } from 'h3';
import { createError, eventHandler, setResponseHeader } from 'h3';

import { clerkClient } from './clerkClient';
import type { AuthFn, AuthOptions } from './types';
import { createInitialState, toWebRequest } from './utils';

function parseHandlerAndOptions(args: unknown[]) {
Expand Down Expand Up @@ -81,7 +82,10 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
return eventHandler(async event => {
const clerkRequest = toWebRequest(event);

const requestState = await clerkClient(event).authenticateRequest(clerkRequest, options);
const requestState = await clerkClient(event).authenticateRequest(clerkRequest, {
...options,
acceptsToken: 'any',
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the option that enables verification of api keys, oauth tokens and machine tokens

});

const locationHeader = requestState.headers.get(constants.Headers.Location);
if (locationHeader) {
Expand All @@ -105,13 +109,18 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
}

const authObject = requestState.toAuth();
const authHandler = () => authObject;
const authHandler: AuthFn = ((options?: AuthOptions) => {
const acceptsToken = options?.acceptsToken ?? TokenType.SessionToken;
return getAuthObjectForAcceptedToken({ authObject, acceptsToken });
Copy link
Member Author

Choose a reason for hiding this comment

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

This is a shared helper across our SDKs to get the actual auth object based on the acceptsToken value. It is unit tested there.

}) as AuthFn;

const auth = new Proxy(Object.assign(authHandler, authObject), {
get(target, prop: string, receiver) {
const auth = new Proxy(authHandler, {
get(target, prop, receiver) {
deprecated('event.context.auth', 'Use `event.context.auth()` as a function instead.');

return Reflect.get(target, prop, receiver);
// If the property exists on the function, return it
if (prop in target) return Reflect.get(target, prop, receiver);
// Otherwise, get it from the authObject
return authObject?.[prop as keyof typeof authObject];
},
});

Expand Down
10 changes: 8 additions & 2 deletions packages/nuxt/src/runtime/server/getAuth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
import type { SessionAuthObject } from '@clerk/backend';
import { deprecated } from '@clerk/shared/deprecated';
import type { H3Event } from 'h3';

import { moduleRegistrationRequired } from './errors';

export function getAuth(event: H3Event): SignedInAuthObject | SignedOutAuthObject {
/**
* @deprecated Use `event.context.auth()` instead.
*/
export function getAuth(event: H3Event): SessionAuthObject {
Copy link
Member Author

@wobsoriano wobsoriano Jul 25, 2025

Choose a reason for hiding this comment

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

We're deprecating this helper in favor of event.context.auth() to follow Nuxt conventions where event.context is the standard pattern for accessing request-scoped data.

This function is not documented as well so it's safe to deprecated. We already use event.context.auth() in our docs

deprecated('getAuth', 'Use `event.context.auth()` instead.');

const authObject = event.context.auth();

if (!authObject) {
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/runtime/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { clerkClient } from './clerkClient';
export { clerkMiddleware } from './clerkMiddleware';
export { createRouteMatcher } from './routeMatcher';
export { getAuth } from './getAuth';
export type { AuthFn } from './types';
45 changes: 45 additions & 0 deletions packages/nuxt/src/runtime/server/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { AuthObject, InvalidTokenAuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
import type {
AuthenticateRequestOptions,
InferAuthObjectFromToken,
InferAuthObjectFromTokenArray,
SessionTokenType,
TokenType,
} from '@clerk/backend/internal';

export type AuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };

/**
* @internal This type is used to define the `auth` function in the event context.
*/
export interface AuthFn {
/**
* @example
* const auth = event.context.auth({ acceptsToken: ['session_token', 'api_key'] })
*/
<T extends TokenType[]>(
options: AuthOptions & { acceptsToken: T },
):
| InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<Exclude<T[number], SessionTokenType>>>
| InvalidTokenAuthObject;

/**
* @example
* const auth = event.context.auth({ acceptsToken: 'session_token' })
*/
<T extends TokenType>(
options: AuthOptions & { acceptsToken: T },
): InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<Exclude<T, SessionTokenType>>>;

/**
* @example
* const auth = event.context.auth({ acceptsToken: 'any' })
*/
(options: AuthOptions & { acceptsToken: 'any' }): AuthObject;

/**
* @example
* const auth = event.context.auth()
*/
(): SessionAuthObject;
}
Loading
Loading