Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
6 changes: 6 additions & 0 deletions .changeset/free-times-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/backend': patch
'@clerk/nextjs': patch
---

Export and reuse internal machine auth helpers
30 changes: 30 additions & 0 deletions .changeset/large-adults-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@clerk/tanstack-react-start': 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 `getAuth()` function. This option can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.

Example usage:

```ts
import { createServerFn } from '@tanstack/react-start'
import { getAuth } from '@clerk/tanstack-react-start/server'
import { getWebRequest } from '@tanstack/react-start/server'

const authStateFn = createServerFn({ method: 'GET' }).handler(async () => {
const request = getWebRequest()
const auth = await getAuth(request, { 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 {}
})

```
27 changes: 27 additions & 0 deletions .changeset/sour-onions-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@clerk/express': 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 `getAuth()` function. This option can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.

Example usage:

```ts
import express from 'express';
import { getAuth } from '@clerk/express';

const app = express();

app.get('/path', (req, res) => {
const authObject = getAuth(req, { 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)
}
});
```
27 changes: 27 additions & 0 deletions .changeset/two-trains-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@clerk/react-router': 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 `getAuth()` function. This option can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.

Example usage:

```ts
import { getAuth } from '@clerk/react-router/ssr.server'
import type { Route } from './+types/profile'

export async function loader(args: Route.LoaderArgs) {
const authObject = await getAuth(args, { 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 {}
}
```
27 changes: 27 additions & 0 deletions .changeset/yummy-socks-join.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@clerk/fastify': 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 `getAuth()` function. This option can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.

Example usage:

```ts
import Fastify from 'fastify'
import { getAuth } from '@clerk/fastify'

const fastify = Fastify()

fastify.get('/path', (request, reply) => {
const authObject = getAuth(req, { 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)
}
});
```
11 changes: 10 additions & 1 deletion packages/backend/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ export { createAuthenticateRequest } from './tokens/factory';

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

export type { AuthenticateRequestOptions, OrganizationSyncOptions } from './tokens/types';
export type {
AuthenticateRequestOptions,
OrganizationSyncOptions,
InferAuthObjectFromToken,
InferAuthObjectFromTokenArray,
SessionAuthObject,
MachineAuthObject,
GetAuthFn,
} from './tokens/types';

export { TokenType } from './tokens/tokenTypes';
export type { SessionTokenType, MachineTokenType } from './tokens/tokenTypes';
Expand All @@ -26,6 +34,7 @@ export {
authenticatedMachineObject,
unauthenticatedMachineObject,
getAuthObjectFromJwt,
getAuthObjectForAcceptedToken,
} from './tokens/authObjects';

export { AuthStatus } from './tokens/authStatus';
Expand Down
27 changes: 26 additions & 1 deletion packages/backend/src/tokens/authObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import type {
SessionStatusClaim,
SharedSignedInAuthObjectProperties,
} from '@clerk/types';
import { isTokenTypeAccepted } from 'src/internal';

import type { APIKey, CreateBackendApiOptions, MachineToken } from '../api';
import { createBackendApiClient } from '../api';
import type { AuthenticateContext } from './authenticateContext';
import type { MachineTokenType, SessionTokenType } from './tokenTypes';
import { TokenType } from './tokenTypes';
import type { MachineAuthType } from './types';
import type { AuthenticateRequestOptions, MachineAuthType } from './types';

/**
* @inline
Expand Down Expand Up @@ -361,3 +362,27 @@ export const getAuthObjectFromJwt = (

return authObject;
};

/**
* @internal
*/
export function getAuthObjectForAcceptedToken({
authObject,
acceptsToken = TokenType.SessionToken,
}: {
authObject: AuthObject;
acceptsToken: AuthenticateRequestOptions['acceptsToken'];
}): AuthObject {
if (acceptsToken === 'any') {
return authObject;
}

if (!isTokenTypeAccepted(authObject.tokenType, acceptsToken)) {
if (authObject.tokenType === TokenType.SessionToken) {
return signedOutAuthObject(authObject.debug);
}
return unauthenticatedMachineObject(authObject.tokenType, authObject.debug);
}

return authObject;
}
83 changes: 82 additions & 1 deletion packages/backend/src/tokens/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { MatchFunction } from '@clerk/shared/pathToRegexp';
import type { PendingSessionOptions } from '@clerk/types';

import type { ApiClient, APIKey, IdPOAuthAccessToken, MachineToken } from '../api';
import type { TokenType } from './tokenTypes';
import type {
AuthenticatedMachineObject,
AuthObject,
SignedInAuthObject,
SignedOutAuthObject,
UnauthenticatedMachineObject,
} from './authObjects';
import type { SessionTokenType, TokenType } from './tokenTypes';
import type { VerifyTokenOptions } from './verify';

/**
Expand Down Expand Up @@ -141,3 +149,76 @@ export type OrganizationSyncTargetMatchers = {
export type OrganizationSyncTarget =
| { type: 'personalAccount' }
| { type: 'organization'; organizationId?: string; organizationSlug?: string };

/**
* Infers auth object type from an array of token types.
* - Session token only -> SessionType
* - Mixed tokens -> SessionType | MachineType
* - Machine tokens only -> MachineType
*/
export type InferAuthObjectFromTokenArray<
T extends readonly TokenType[],
SessionType extends AuthObject,
MachineType extends AuthObject,
> = SessionTokenType extends T[number]
? T[number] extends SessionTokenType
? SessionType
: SessionType | (MachineType & { tokenType: T[number] })
: MachineType & { tokenType: T[number] };

/**
* Infers auth object type from a single token type.
* Returns SessionType for session tokens, or MachineType for machine tokens.
*/
export type InferAuthObjectFromToken<
T extends TokenType,
SessionType extends AuthObject,
MachineType extends AuthObject,
> = T extends SessionTokenType ? SessionType : MachineType & { tokenType: T };

export type SessionAuthObject = SignedInAuthObject | SignedOutAuthObject;
export type MachineAuthObject<T extends TokenType> = (AuthenticatedMachineObject | UnauthenticatedMachineObject) & {
tokenType: T;
};

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

type MaybePromise<T, IsPromise extends boolean> = IsPromise extends true ? Promise<T> : T;

/**
* Shared generic overload type for getAuth() helpers across SDKs.
*
* - Parameterized by the request type (RequestType).
* - Handles different accepted token types and their corresponding return types.
*/
export interface GetAuthFn<RequestType, ReturnsPromise extends boolean = false> {
/**
* @example
* const auth = await getAuth(req, { acceptsToken: ['session_token', 'api_key'] })
*/
<T extends TokenType[]>(
req: RequestType,
options: AuthOptions & { acceptsToken: T },
): MaybePromise<InferAuthObjectFromTokenArray<T, SessionAuthObject, MachineAuthObject<T[number]>>, ReturnsPromise>;

/**
* @example
* const auth = await getAuth(req, { acceptsToken: 'session_token' })
*/
<T extends TokenType>(
req: RequestType,
options: AuthOptions & { acceptsToken: T },
): MaybePromise<InferAuthObjectFromToken<T, SessionAuthObject, MachineAuthObject<T>>, ReturnsPromise>;

/**
* @example
* const auth = await getAuth(req, { acceptsToken: 'any' })
*/
(req: RequestType, options: AuthOptions & { acceptsToken: 'any' }): MaybePromise<AuthObject, ReturnsPromise>;

/**
* @example
* const auth = await getAuth(req)
*/
(req: RequestType, options?: PendingSessionOptions): MaybePromise<SessionAuthObject, ReturnsPromise>;
}
27 changes: 26 additions & 1 deletion packages/express/src/__tests__/getAuth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,36 @@ describe('getAuth', () => {
});

it('returns auth from request for signed-out request', async () => {
expect(getAuth(mockRequestWithAuth())).toHaveProperty('userId', null);
expect(getAuth(mockRequestWithAuth({ userId: null }))).toHaveProperty('userId', null);
});

it('returns auth from request', async () => {
const req = mockRequestWithAuth({ userId: 'user_12345' });
expect(getAuth(req)).toHaveProperty('userId', 'user_12345');
});

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we wanna add a test case that asserts that the default acceptsToken is 'any'?

Copy link
Member Author

Choose a reason for hiding this comment

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

So the actual authenticateRequest() call is using 'any' and cannot be overridden. Meaning any session or machine request will be verified.

However, the acceptsToken in getAuth() (which defaults to session_token) is used after authentication to filter or coerce the returned auth object to match the expected type.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, sorry, I misread lol. The changeset is saying what you're saying. So never mind :)

it('returns the actual auth object when its tokenType matches acceptsToken', () => {
const req = mockRequestWithAuth({ tokenType: 'api_key', id: 'ak_1234', subject: 'api_key_1234' });
const result = getAuth(req, { acceptsToken: 'api_key' });
expect(result.tokenType).toBe('api_key');
expect(result.id).toBe('ak_1234');
expect(result.subject).toBe('api_key_1234');
});

it('returns the actual auth object if its tokenType is included in the acceptsToken array', () => {
const req = mockRequestWithAuth({ tokenType: 'machine_token', id: 'mt_1234' });
const result = getAuth(req, { acceptsToken: ['machine_token', 'api_key'] });
expect(result.tokenType).toBe('machine_token');
expect(result.id).toBe('mt_1234');
expect(result.subject).toBeUndefined();
});

it('returns an unauthenticated auth object when the tokenType does not match acceptsToken', () => {
const req = mockRequestWithAuth({ tokenType: 'session_token', userId: 'user_12345' });
const result = getAuth(req, { acceptsToken: 'api_key' });
expect(result.tokenType).toBe('session_token'); // reflects the actual token found
// Properties specific to authenticated objects should be null or undefined
// @ts-expect-error - userId is not a property of the unauthenticated object
expect(result.userId).toBeNull();
});
});
17 changes: 4 additions & 13 deletions packages/express/src/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
import type { AuthObject } from '@clerk/backend';
import type { Application, Request as ExpressRequest, RequestHandler, Response as ExpressResponse } from 'express';
import express from 'express';
import supertest from 'supertest';
Expand Down Expand Up @@ -26,22 +26,13 @@ export function mockRequest(): ExpressRequest {
return {} as ExpressRequest;
}

export function mockRequestWithAuth(
auth: Partial<SignedInAuthObject | SignedOutAuthObject> = {},
): ExpressRequestWithAuth {
export function mockRequestWithAuth(auth: Partial<AuthObject> = {}): ExpressRequestWithAuth {
return {
auth: () => ({
sessionClaims: null,
sessionId: null,
actor: null,
userId: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
getToken: async () => '',
getToken: () => Promise.resolve(''),
has: () => false,
debug: () => ({}),
tokenType: 'session_token',
...auth,
}),
} as unknown as ExpressRequestWithAuth;
Expand Down
13 changes: 8 additions & 5 deletions packages/express/src/authenticateRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { incomingMessageToRequest, loadApiEnv, loadClientEnv } from './utils';

export const authenticateRequest = (opts: AuthenticateRequestParams) => {
const { clerkClient, request, options } = opts;
const { jwtKey, authorizedParties, audience } = options || {};
const { jwtKey, authorizedParties, audience, acceptsToken } = options || {};

const clerkRequest = createClerkRequest(incomingMessageToRequest(request));
const env = { ...loadApiEnv(), ...loadClientEnv() };
Expand Down Expand Up @@ -47,6 +47,7 @@ export const authenticateRequest = (opts: AuthenticateRequestParams) => {
isSatellite,
domain,
signInUrl,
acceptsToken,
});
};

Expand Down Expand Up @@ -116,11 +117,13 @@ export const authenticateAndDecorateRequest = (options: ClerkMiddlewareOptions =
const authHandler = (opts: Parameters<typeof requestState.toAuth>[0]) => requestState.toAuth(opts);
const authObject = requestState.toAuth();

const auth = new Proxy(Object.assign(authHandler, authObject), {
get(target, prop: string, receiver) {
const auth = new Proxy(authHandler, {
get(target, prop, receiver) {
deprecated('req.auth', 'Use `req.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
Loading
Loading