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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
MemberAuthor

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
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 {

@wobsorianowobsorianoJul 25, 2025

Copy link
Copy Markdown
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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