Skip to content

Commit 0452a0e

Browse files
authored
feat(nuxt): Introduce machine auth (#6391)
1 parent 6162d91 commit 0452a0e

File tree

10 files changed

+288
-22
lines changed

10 files changed

+288
-22
lines changed

.changeset/cool-guests-trade.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@clerk/nuxt': minor
3+
---
4+
5+
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.
6+
7+
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.
8+
9+
Example usage:
10+
11+
```ts
12+
export default eventHandler((event) => {
13+
const auth = event.locals.auth({ acceptsToken: 'any' })
14+
15+
if (authObject.tokenType === 'session_token') {
16+
console.log('this is session token from a user')
17+
} else {
18+
console.log('this is some other type of machine token')
19+
console.log('more specifically, a ' + authObject.tokenType)
20+
}
21+
22+
return {}
23+
})
24+
```

packages/nuxt/src/module.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,11 @@ export default defineNuxtModule<ModuleOptions>({
110110
{
111111
filename: 'types/clerk.d.ts',
112112
getContents: () => `import type { SessionAuthObject } from '@clerk/backend';
113-
declare module 'h3' {
114-
type AuthObjectHandler = SessionAuthObject & {
115-
(): SessionAuthObject;
116-
}
113+
import type { AuthFn } from '@clerk/nuxt/server';
117114
115+
declare module 'h3' {
118116
interface H3EventContext {
119-
auth: AuthObjectHandler;
117+
auth: SessionAuthObject & AuthFn;
120118
}
121119
}
122120
`,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { AuthObject, InvalidTokenAuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
2+
import { expectTypeOf, test } from 'vitest';
3+
4+
import type { AuthFn } from '../types';
5+
6+
test('infers the correct AuthObject type for each accepted token type', () => {
7+
// Mock event object
8+
const event = {
9+
locals: {
10+
auth: (() => {}) as AuthFn,
11+
},
12+
};
13+
14+
// Session token by default
15+
expectTypeOf(event.locals.auth()).toMatchTypeOf<SessionAuthObject>();
16+
17+
// Individual token types
18+
expectTypeOf(event.locals.auth({ acceptsToken: 'session_token' })).toMatchTypeOf<SessionAuthObject>();
19+
expectTypeOf(event.locals.auth({ acceptsToken: 'api_key' })).toMatchTypeOf<MachineAuthObject<'api_key'>>();
20+
expectTypeOf(event.locals.auth({ acceptsToken: 'machine_token' })).toMatchTypeOf<
21+
MachineAuthObject<'machine_token'>
22+
>();
23+
expectTypeOf(event.locals.auth({ acceptsToken: 'oauth_token' })).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();
24+
25+
// Array of token types
26+
expectTypeOf(event.locals.auth({ acceptsToken: ['session_token', 'machine_token'] })).toMatchTypeOf<
27+
SessionAuthObject | MachineAuthObject<'machine_token'> | InvalidTokenAuthObject
28+
>();
29+
expectTypeOf(event.locals.auth({ acceptsToken: ['machine_token', 'oauth_token'] })).toMatchTypeOf<
30+
MachineAuthObject<'machine_token' | 'oauth_token'> | InvalidTokenAuthObject
31+
>();
32+
33+
// Any token type
34+
expectTypeOf(event.locals.auth({ acceptsToken: 'any' })).toMatchTypeOf<AuthObject>();
35+
});
36+
37+
test('verifies discriminated union works correctly with acceptsToken: any', () => {
38+
// Mock event object
39+
const event = {
40+
locals: {
41+
auth: (() => {}) as AuthFn,
42+
},
43+
};
44+
45+
const auth = event.locals.auth({ acceptsToken: 'any' });
46+
47+
if (auth.tokenType === 'session_token') {
48+
expectTypeOf(auth).toMatchTypeOf<SessionAuthObject>();
49+
} else if (auth.tokenType === 'api_key') {
50+
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'api_key'>>();
51+
} else if (auth.tokenType === 'machine_token') {
52+
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'machine_token'>>();
53+
} else if (auth.tokenType === 'oauth_token') {
54+
expectTypeOf(auth).toMatchTypeOf<MachineAuthObject<'oauth_token'>>();
55+
}
56+
});

packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts

Lines changed: 130 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,31 @@ import { vi } from 'vitest';
33

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

6-
const AUTH_RESPONSE = {
6+
const SESSION_AUTH_RESPONSE = {
77
userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
88
sessionId: 'sess_2jZSstSbxtTndD9P7q4kDl0VVZa',
9+
tokenType: 'session_token',
10+
isAuthenticated: true,
11+
sessionStatus: 'active',
12+
sessionClaims: {},
13+
actor: null,
14+
factorVerificationAge: null,
15+
orgId: null,
16+
orgRole: null,
17+
orgSlug: null,
18+
orgPermissions: null,
19+
};
20+
21+
const MACHINE_AUTH_RESPONSE = {
22+
id: 'ak_123456789',
23+
subject: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
24+
scopes: ['read:users', 'write:users'],
25+
tokenType: 'api_key',
26+
isAuthenticated: true,
27+
name: 'Test API Key',
28+
claims: { custom: 'claim' },
29+
userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa',
30+
orgId: null,
931
};
1032

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

2446
const authenticateRequestMock = vi.fn().mockResolvedValue({
25-
toAuth: () => AUTH_RESPONSE,
47+
toAuth: () => SESSION_AUTH_RESPONSE,
2648
headers: new Headers(),
2749
});
2850

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

4971
expect(response.status).toBe(200);
50-
expect(await response.json()).toEqual(AUTH_RESPONSE);
72+
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
5173
});
5274

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

6385
expect(response.status).toBe(200);
6486
expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS));
65-
expect(await response.json()).toEqual(AUTH_RESPONSE);
87+
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
6688
});
6789

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

82104
expect(response.status).toBe(200);
83105
expect(response.headers.get('a-custom-header')).toBe('1');
84-
expect(await response.json()).toEqual(AUTH_RESPONSE);
106+
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
85107
});
86108

87109
test('executes handler and renders route when used with a custom handler and options', async () => {
@@ -101,6 +123,108 @@ describe('clerkMiddleware(params)', () => {
101123
expect(response.status).toBe(200);
102124
expect(response.headers.get('a-custom-header')).toBe('1');
103125
expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS));
104-
expect(await response.json()).toEqual(AUTH_RESPONSE);
126+
expect(await response.json()).toEqual(SESSION_AUTH_RESPONSE);
127+
});
128+
129+
describe('machine authentication', () => {
130+
test('returns machine auth object when acceptsToken is machine token type', async () => {
131+
authenticateRequestMock.mockResolvedValueOnce({
132+
toAuth: () => MACHINE_AUTH_RESPONSE,
133+
headers: new Headers(),
134+
});
135+
136+
const app = createApp();
137+
const handler = toWebHandler(app);
138+
app.use(clerkMiddleware());
139+
app.use(
140+
'/',
141+
eventHandler(event => event.context.auth({ acceptsToken: 'api_key' })),
142+
);
143+
const response = await handler(new Request(new URL('/', 'http://localhost')));
144+
145+
expect(response.status).toBe(200);
146+
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
147+
});
148+
149+
test('returns machine auth object when acceptsToken array includes machine token type', async () => {
150+
authenticateRequestMock.mockResolvedValueOnce({
151+
toAuth: () => MACHINE_AUTH_RESPONSE,
152+
headers: new Headers(),
153+
});
154+
155+
const app = createApp();
156+
const handler = toWebHandler(app);
157+
app.use(clerkMiddleware());
158+
app.use(
159+
'/',
160+
eventHandler(event => event.context.auth({ acceptsToken: ['session_token', 'api_key'] })),
161+
);
162+
const response = await handler(new Request(new URL('/', 'http://localhost')));
163+
164+
expect(response.status).toBe(200);
165+
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
166+
});
167+
168+
test('returns any auth object when acceptsToken is any', async () => {
169+
authenticateRequestMock.mockResolvedValueOnce({
170+
toAuth: () => MACHINE_AUTH_RESPONSE,
171+
headers: new Headers(),
172+
});
173+
174+
const app = createApp();
175+
const handler = toWebHandler(app);
176+
app.use(clerkMiddleware());
177+
app.use(
178+
'/',
179+
eventHandler(event => event.context.auth({ acceptsToken: 'any' })),
180+
);
181+
const response = await handler(new Request(new URL('/', 'http://localhost')));
182+
183+
expect(response.status).toBe(200);
184+
expect(await response.json()).toEqual(MACHINE_AUTH_RESPONSE);
185+
});
186+
187+
test('returns unauthenticated machine object when token type does not match acceptsToken', async () => {
188+
authenticateRequestMock.mockResolvedValueOnce({
189+
toAuth: () => MACHINE_AUTH_RESPONSE,
190+
headers: new Headers(),
191+
});
192+
193+
const app = createApp();
194+
const handler = toWebHandler(app);
195+
app.use(clerkMiddleware());
196+
app.use(
197+
'/',
198+
eventHandler(event => event.context.auth({ acceptsToken: 'machine_token' })),
199+
);
200+
const response = await handler(new Request(new URL('/', 'http://localhost')));
201+
202+
expect(response.status).toBe(200);
203+
const result = await response.json();
204+
expect(result.tokenType).toBe('machine_token');
205+
expect(result.isAuthenticated).toBe(false);
206+
expect(result.id).toBe(null);
207+
});
208+
209+
test('returns invalid token object when token type is not in acceptsToken array', async () => {
210+
authenticateRequestMock.mockResolvedValueOnce({
211+
toAuth: () => MACHINE_AUTH_RESPONSE,
212+
headers: new Headers(),
213+
});
214+
215+
const app = createApp();
216+
const handler = toWebHandler(app);
217+
app.use(clerkMiddleware());
218+
app.use(
219+
'/',
220+
eventHandler(event => event.context.auth({ acceptsToken: ['session_token', 'machine_token'] })),
221+
);
222+
const response = await handler(new Request(new URL('/', 'http://localhost')));
223+
224+
expect(response.status).toBe(200);
225+
const result = await response.json();
226+
expect(result.tokenType).toBe(null);
227+
expect(result.isAuthenticated).toBe(false);
228+
});
105229
});
106230
});

packages/nuxt/src/runtime/server/clerkMiddleware.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { AuthenticateRequestOptions } from '@clerk/backend/internal';
2-
import { AuthStatus, constants } from '@clerk/backend/internal';
2+
import { AuthStatus, constants, getAuthObjectForAcceptedToken, TokenType } from '@clerk/backend/internal';
33
import { deprecated } from '@clerk/shared/deprecated';
44
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
55
import type { EventHandler } from 'h3';
66
import { createError, eventHandler, setResponseHeader } from 'h3';
77

88
import { clerkClient } from './clerkClient';
9+
import type { AuthFn, AuthOptions } from './types';
910
import { createInitialState, toWebRequest } from './utils';
1011

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

84-
const requestState = await clerkClient(event).authenticateRequest(clerkRequest, options);
85+
const requestState = await clerkClient(event).authenticateRequest(clerkRequest, {
86+
...options,
87+
acceptsToken: 'any',
88+
});
8589

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

107111
const authObject = requestState.toAuth();
108-
const authHandler = () => authObject;
112+
const authHandler: AuthFn = ((options?: AuthOptions) => {
113+
const acceptsToken = options?.acceptsToken ?? TokenType.SessionToken;
114+
return getAuthObjectForAcceptedToken({ authObject, acceptsToken });
115+
}) as AuthFn;
109116

110-
const auth = new Proxy(Object.assign(authHandler, authObject), {
111-
get(target, prop: string, receiver) {
117+
const auth = new Proxy(authHandler, {
118+
get(target, prop, receiver) {
112119
deprecated('event.context.auth', 'Use `event.context.auth()` as a function instead.');
113-
114-
return Reflect.get(target, prop, receiver);
120+
// If the property exists on the function, return it
121+
if (prop in target) return Reflect.get(target, prop, receiver);
122+
// Otherwise, get it from the authObject
123+
return authObject?.[prop as keyof typeof authObject];
115124
},
116125
});
117126

packages/nuxt/src/runtime/server/getAuth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
1+
import type { SessionAuthObject } from '@clerk/backend';
2+
import { deprecated } from '@clerk/shared/deprecated';
23
import type { H3Event } from 'h3';
34

45
import { moduleRegistrationRequired } from './errors';
56

6-
export function getAuth(event: H3Event): SignedInAuthObject | SignedOutAuthObject {
7+
/**
8+
* @deprecated Use `event.context.auth()` instead.
9+
*/
10+
export function getAuth(event: H3Event): SessionAuthObject {
11+
deprecated('getAuth', 'Use `event.context.auth()` instead.');
12+
713
const authObject = event.context.auth();
814

915
if (!authObject) {

packages/nuxt/src/runtime/server/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { clerkClient } from './clerkClient';
33
export { clerkMiddleware } from './clerkMiddleware';
44
export { createRouteMatcher } from './routeMatcher';
55
export { getAuth } from './getAuth';
6+
export type { AuthFn } from './types';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { AuthObject, InvalidTokenAuthObject, MachineAuthObject, SessionAuthObject } from '@clerk/backend';
2+
import type {
3+
AuthenticateRequestOptions,
4+
InferAuthObjectFromToken,
5+
InferAuthObjectFromTokenArray,
6+
SessionTokenType,
7+
TokenType,
8+
} from '@clerk/backend/internal';
9+
10+
export type AuthOptions = { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };
11+
12+
/**
13+
* @internal This type is used to define the `auth` function in the event context.
14+
*/
15+
export interface AuthFn {
16+
/**
17+
* @example
18+
* const auth = event.context.auth({ acceptsToken: ['session_token', 'api_key'] })
19+
*/
20+
<T extends TokenType[]>(
21+
options: AuthOptions & { acceptsToken: T },
22+
):
23+
| InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<Exclude<T[number], SessionTokenType>>>
24+
| InvalidTokenAuthObject;
25+
26+
/**
27+
* @example
28+
* const auth = event.context.auth({ acceptsToken: 'session_token' })
29+
*/
30+
<T extends TokenType>(
31+
options: AuthOptions & { acceptsToken: T },
32+
): InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<Exclude<T, SessionTokenType>>>;
33+
34+
/**
35+
* @example
36+
* const auth = event.context.auth({ acceptsToken: 'any' })
37+
*/
38+
(options: AuthOptions & { acceptsToken: 'any' }): AuthObject;
39+
40+
/**
41+
* @example
42+
* const auth = event.context.auth()
43+
*/
44+
(): SessionAuthObject;
45+
}

0 commit comments

Comments
 (0)