Skip to content

Commit 2148166

Browse files
authored
feat(express,fastify,tanstack-react-start,react-router): Support machine auth tokens in getAuth() (#6067)
1 parent 6bd1d2f commit 2148166

File tree

27 files changed

+454
-115
lines changed

27 files changed

+454
-115
lines changed

.changeset/free-times-refuse.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/backend': patch
3+
'@clerk/nextjs': patch
4+
---
5+
6+
Re-organize internal types for the recently added "machine authentication" feature.

.changeset/large-adults-juggle.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@clerk/tanstack-react-start': 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 `getAuth()` function. 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+
import { createServerFn } from '@tanstack/react-start'
13+
import { getAuth } from '@clerk/tanstack-react-start/server'
14+
import { getWebRequest } from '@tanstack/react-start/server'
15+
16+
const authStateFn = createServerFn({ method: 'GET' }).handler(async () => {
17+
const request = getWebRequest()
18+
const auth = await getAuth(request, { acceptsToken: 'any' })
19+
20+
if (authObject.tokenType === 'session_token') {
21+
console.log('this is session token from a user')
22+
} else {
23+
console.log('this is some other type of machine token')
24+
console.log('more specifically, a ' + authObject.tokenType)
25+
}
26+
27+
return {}
28+
})
29+
30+
```

.changeset/sour-onions-wear.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@clerk/express': 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 `getAuth()` function. 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+
import express from 'express';
13+
import { getAuth } from '@clerk/express';
14+
15+
const app = express();
16+
17+
app.get('/path', (req, res) => {
18+
const authObject = getAuth(req, { acceptsToken: 'any' });
19+
20+
if (authObject.tokenType === 'session_token') {
21+
console.log('this is session token from a user')
22+
} else {
23+
console.log('this is some other type of machine token')
24+
console.log('more specifically, a ' + authObject.tokenType)
25+
}
26+
});
27+
```

.changeset/two-trains-pull.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@clerk/react-router': 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 `getAuth()` function. 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+
import { getAuth } from '@clerk/react-router/ssr.server'
13+
import type { Route } from './+types/profile'
14+
15+
export async function loader(args: Route.LoaderArgs) {
16+
const authObject = await getAuth(args, { acceptsToken: 'any' })
17+
18+
if (authObject.tokenType === 'session_token') {
19+
console.log('this is session token from a user')
20+
} else {
21+
console.log('this is some other type of machine token')
22+
console.log('more specifically, a ' + authObject.tokenType)
23+
}
24+
25+
return {}
26+
}
27+
```

.changeset/yummy-socks-join.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@clerk/fastify': 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 `getAuth()` function. 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+
import Fastify from 'fastify'
13+
import { getAuth } from '@clerk/fastify'
14+
15+
const fastify = Fastify()
16+
17+
fastify.get('/path', (request, reply) => {
18+
const authObject = getAuth(req, { acceptsToken: 'any' });
19+
20+
if (authObject.tokenType === 'session_token') {
21+
console.log('this is session token from a user')
22+
} else {
23+
console.log('this is some other type of machine token')
24+
console.log('more specifically, a ' + authObject.tokenType)
25+
}
26+
});
27+
```

.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
150150
"backend/client.mdx",
151151
"backend/email-address.mdx",
152152
"backend/external-account.mdx",
153+
"backend/get-auth-fn.mdx",
153154
"backend/identification-link.mdx",
155+
"backend/infer-auth-object-from-token-array.mdx",
156+
"backend/infer-auth-object-from-token.mdx",
154157
"backend/invitation-status.mdx",
155158
"backend/invitation.mdx",
156159
"backend/organization-invitation-status.mdx",

packages/backend/src/__tests__/exports.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('subpath /internal exports', () => {
4747
"createRedirect",
4848
"debugRequestState",
4949
"decorateObjectWithResources",
50+
"getAuthObjectForAcceptedToken",
5051
"getAuthObjectFromJwt",
5152
"getMachineTokenType",
5253
"isMachineToken",

packages/backend/src/internal.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ export { createAuthenticateRequest } from './tokens/factory';
77

88
export { debugRequestState } from './tokens/request';
99

10-
export type { AuthenticateRequestOptions, OrganizationSyncOptions } from './tokens/types';
10+
export type {
11+
AuthenticateRequestOptions,
12+
OrganizationSyncOptions,
13+
InferAuthObjectFromToken,
14+
InferAuthObjectFromTokenArray,
15+
SessionAuthObject,
16+
MachineAuthObject,
17+
GetAuthFn,
18+
} from './tokens/types';
1119

1220
export { TokenType } from './tokens/tokenTypes';
1321
export type { SessionTokenType, MachineTokenType } from './tokens/tokenTypes';
@@ -26,6 +34,7 @@ export {
2634
authenticatedMachineObject,
2735
unauthenticatedMachineObject,
2836
getAuthObjectFromJwt,
37+
getAuthObjectForAcceptedToken,
2938
} from './tokens/authObjects';
3039

3140
export { AuthStatus } from './tokens/authStatus';

packages/backend/src/tokens/authObjects.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import type {
1313

1414
import type { APIKey, CreateBackendApiOptions, MachineToken } from '../api';
1515
import { createBackendApiClient } from '../api';
16+
import { isTokenTypeAccepted } from '../internal';
1617
import type { AuthenticateContext } from './authenticateContext';
1718
import type { MachineTokenType, SessionTokenType } from './tokenTypes';
1819
import { TokenType } from './tokenTypes';
19-
import type { MachineAuthType } from './types';
20+
import type { AuthenticateRequestOptions, MachineAuthType } from './types';
2021

2122
/**
2223
* @inline
@@ -361,3 +362,63 @@ export const getAuthObjectFromJwt = (
361362

362363
return authObject;
363364
};
365+
366+
/**
367+
* @internal
368+
* Filters and coerces an AuthObject based on the accepted token type(s).
369+
*
370+
* This function is used after authentication to ensure that the returned auth object
371+
* matches the expected token type(s) specified by `acceptsToken`. If the token type
372+
* of the provided `authObject` does not match any of the types in `acceptsToken`,
373+
* it returns an unauthenticated or signed-out version of the object, depending on the token type.
374+
*
375+
* - If `acceptsToken` is `'any'`, the original auth object is returned.
376+
* - If `acceptsToken` is a single token type or an array of token types, the function checks if
377+
* `authObject.tokenType` matches any of them.
378+
* - If the token type does not match and is a session token, a signed-out object is returned.
379+
* - If the token type does not match and is a machine token, an unauthenticated machine object is returned.
380+
* - If the token type matches, the original auth object is returned.
381+
*
382+
* @param {Object} params
383+
* @param {AuthObject} params.authObject - The authenticated object to filter.
384+
* @param {AuthenticateRequestOptions['acceptsToken']} [params.acceptsToken=TokenType.SessionToken] - The accepted token type(s). Can be a string, array of strings, or 'any'.
385+
* @returns {AuthObject} The filtered or coerced auth object.
386+
*
387+
* @example
388+
* // Accept only 'api_key' tokens
389+
* const authObject = { tokenType: 'session_token', userId: 'user_123' };
390+
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: 'api_key' });
391+
* // result will be a signed-out object (since tokenType is 'session_token' and does not match)
392+
*
393+
* @example
394+
* // Accept 'api_key' or 'machine_token'
395+
* const authObject = { tokenType: 'machine_token', id: 'mt_123' };
396+
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: ['api_key', 'machine_token'] });
397+
* // result will be the original authObject (since tokenType matches one in the array)
398+
*
399+
* @example
400+
* // Accept any token type
401+
* const authObject = { tokenType: 'api_key', id: 'ak_123' };
402+
* const result = getAuthObjectForAcceptedToken({ authObject, acceptsToken: 'any' });
403+
* // result will be the original authObject
404+
*/
405+
export function getAuthObjectForAcceptedToken({
406+
authObject,
407+
acceptsToken = TokenType.SessionToken,
408+
}: {
409+
authObject: AuthObject;
410+
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
411+
}): AuthObject {
412+
if (acceptsToken === 'any') {
413+
return authObject;
414+
}
415+
416+
if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
417+
if (authObject.tokenType === TokenType.SessionToken) {
418+
return signedOutAuthObject(authObject.debug);
419+
}
420+
return unauthenticatedMachineObject(authObject.tokenType, authObject.debug);
421+
}
422+
423+
return authObject;
424+
}

packages/backend/src/tokens/types.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import type { MatchFunction } from '@clerk/shared/pathToRegexp';
2+
import type { PendingSessionOptions } from '@clerk/types';
23

34
import type { ApiClient, APIKey, IdPOAuthAccessToken, MachineToken } from '../api';
4-
import type { TokenType } from './tokenTypes';
5+
import type {
6+
AuthenticatedMachineObject,
7+
AuthObject,
8+
SignedInAuthObject,
9+
SignedOutAuthObject,
10+
UnauthenticatedMachineObject,
11+
} from './authObjects';
12+
import type { SessionTokenType, TokenType } from './tokenTypes';
513
import type { VerifyTokenOptions } from './verify';
614

715
/**
@@ -141,3 +149,76 @@ export type OrganizationSyncTargetMatchers = {
141149
export type OrganizationSyncTarget =
142150
| { type: 'personalAccount' }
143151
| { type: 'organization'; organizationId?: string; organizationSlug?: string };
152+
153+
/**
154+
* Infers auth object type from an array of token types.
155+
* - Session token only -> SessionType
156+
* - Mixed tokens -> SessionType | MachineType
157+
* - Machine tokens only -> MachineType
158+
*/
159+
export type InferAuthObjectFromTokenArray<
160+
T extends readonly TokenType[],
161+
SessionType extends AuthObject,
162+
MachineType extends AuthObject,
163+
> = SessionTokenType extends T[number]
164+
? T[number] extends SessionTokenType
165+
? SessionType
166+
: SessionType | (MachineType & { tokenType: T[number] })
167+
: MachineType & { tokenType: T[number] };
168+
169+
/**
170+
* Infers auth object type from a single token type.
171+
* Returns SessionType for session tokens, or MachineType for machine tokens.
172+
*/
173+
export type InferAuthObjectFromToken<
174+
T extends TokenType,
175+
SessionType extends AuthObject,
176+
MachineType extends AuthObject,
177+
> = T extends SessionTokenType ? SessionType : MachineType & { tokenType: T };
178+
179+
export type SessionAuthObject = SignedInAuthObject | SignedOutAuthObject;
180+
export type MachineAuthObject<T extends TokenType> = (AuthenticatedMachineObject | UnauthenticatedMachineObject) & {
181+
tokenType: T;
182+
};
183+
184+
type AuthOptions = PendingSessionOptions & { acceptsToken?: AuthenticateRequestOptions['acceptsToken'] };
185+
186+
type MaybePromise<T, IsPromise extends boolean> = IsPromise extends true ? Promise<T> : T;
187+
188+
/**
189+
* Shared generic overload type for getAuth() helpers across SDKs.
190+
*
191+
* - Parameterized by the request type (RequestType).
192+
* - Handles different accepted token types and their corresponding return types.
193+
*/
194+
export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false> {
195+
/**
196+
* @example
197+
* const auth = await getAuth(req, { acceptsToken: ['session_token', 'api_key'] })
198+
*/
199+
<T extends TokenType[]>(
200+
req: RequestType,
201+
options: AuthOptions & { acceptsToken: T },
202+
): MaybePromise<InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<T[number]>>, ReturnsPromise>;
203+
204+
/**
205+
* @example
206+
* const auth = await getAuth(req, { acceptsToken: 'session_token' })
207+
*/
208+
<T extends TokenType>(
209+
req: RequestType,
210+
options: AuthOptions & { acceptsToken: T },
211+
): MaybePromise<InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<T>>, ReturnsPromise>;
212+
213+
/**
214+
* @example
215+
* const auth = await getAuth(req, { acceptsToken: 'any' })
216+
*/
217+
(req: RequestType, options: AuthOptions & { acceptsToken: 'any' }): MaybePromise<AuthObject, ReturnsPromise>;
218+
219+
/**
220+
* @example
221+
* const auth = await getAuth(req)
222+
*/
223+
(req: RequestType, options?: PendingSessionOptions): MaybePromise<SessionAuthObject, ReturnsPromise>;
224+
}

packages/express/src/__tests__/getAuth.test.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,46 @@ import { getAuth } from '../getAuth';
22
import { mockRequest, mockRequestWithAuth } from './helpers';
33

44
describe('getAuth', () => {
5-
it('throws error if clerkMiddleware is not executed before getAuth', async () => {
5+
it('throws error if clerkMiddleware is not executed before getAuth', () => {
66
expect(() => getAuth(mockRequest())).toThrow(/The "clerkMiddleware" should be registered before using "getAuth"/);
77
});
88

9-
it('returns auth from request for signed-out request', async () => {
10-
expect(getAuth(mockRequestWithAuth())).toHaveProperty('userId', null);
9+
it('returns auth from request for signed-out request', () => {
10+
const req = mockRequestWithAuth({ userId: null, tokenType: 'session_token' });
11+
const auth = getAuth(req);
12+
expect(auth.userId).toBeNull();
13+
expect(auth.tokenType).toBe('session_token');
1114
});
1215

13-
it('returns auth from request', async () => {
14-
const req = mockRequestWithAuth({ userId: 'user_12345' });
15-
expect(getAuth(req)).toHaveProperty('userId', 'user_12345');
16+
it('returns auth from request', () => {
17+
const req = mockRequestWithAuth({ userId: 'user_12345', tokenType: 'session_token' });
18+
const auth = getAuth(req);
19+
expect(auth.userId).toBe('user_12345');
20+
expect(auth.tokenType).toBe('session_token');
21+
});
22+
23+
it('returns the actual auth object when its tokenType matches acceptsToken', () => {
24+
const req = mockRequestWithAuth({ tokenType: 'api_key', id: 'ak_1234', subject: 'api_key_1234' });
25+
const result = getAuth(req, { acceptsToken: 'api_key' });
26+
expect(result.tokenType).toBe('api_key');
27+
expect(result.id).toBe('ak_1234');
28+
expect(result.subject).toBe('api_key_1234');
29+
});
30+
31+
it('returns the actual auth object if its tokenType is included in the acceptsToken array', () => {
32+
const req = mockRequestWithAuth({ tokenType: 'machine_token', id: 'mt_1234' });
33+
const result = getAuth(req, { acceptsToken: ['machine_token', 'api_key'] });
34+
expect(result.tokenType).toBe('machine_token');
35+
expect(result.id).toBe('mt_1234');
36+
expect(result.subject).toBeUndefined();
37+
});
38+
39+
it('returns an unauthenticated auth object when the tokenType does not match acceptsToken', () => {
40+
const req = mockRequestWithAuth({ tokenType: 'session_token', userId: 'user_12345' });
41+
const result = getAuth(req, { acceptsToken: 'api_key' });
42+
expect(result.tokenType).toBe('session_token'); // reflects the actual token found
43+
// Properties specific to authenticated objects should be null or undefined
44+
// @ts-expect-error - userId is not a property of the unauthenticated object
45+
expect(result.userId).toBeNull();
1646
});
1747
});

0 commit comments

Comments
 (0)