Skip to content

Commit ea622ba

Browse files
authored
feat(backend,nextjs): Introduce machine authentication (#5689)
1 parent 09fbafd commit ea622ba

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2851
-221
lines changed

.changeset/bumpy-carpets-study.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
'@clerk/backend': major
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 `authenticateRequest()` 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 { clerkClient } from '@clerk/backend';
14+
15+
const app = express();
16+
17+
app.use(async (req, res, next) => {
18+
const requestState = await clerkClient.authenticateRequest(req, {
19+
acceptsToken: 'any'
20+
});
21+
22+
if (!requestState.isAuthenticated) {
23+
// do something for unauthenticated requests
24+
}
25+
26+
const authObject = requestState.toAuth();
27+
28+
if (authObject.tokenType === 'session_token') {
29+
console.log('this is session token from a user')
30+
} else {
31+
console.log('this is some other type of machine token')
32+
console.log('more specifically, a ' + authObject.tokenType)
33+
}
34+
35+
// Attach the auth object to locals so downstream handlers
36+
// and middleware can access it
37+
res.locals.auth = authObject;
38+
next();
39+
});
40+
```

.changeset/chatty-lions-stay.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+
'@clerk/agent-toolkit': minor
4+
'@clerk/react-router': minor
5+
'@clerk/express': minor
6+
'@clerk/fastify': minor
7+
'@clerk/astro': minor
8+
'@clerk/remix': minor
9+
'@clerk/nuxt': minor
10+
---
11+
12+
Machine authentication is now supported for advanced use cases via the backend SDK. You can use `clerkClient.authenticateRequest` to validate machine tokens (such as API keys, OAuth tokens, and machine-to-machine tokens). No new helpers are included in these packages yet.
13+
14+
Example (Astro):
15+
16+
```ts
17+
import { clerkClient } from '@clerk/astro/server';
18+
19+
export const GET: APIRoute = ({ request }) => {
20+
const requestState = await clerkClient.authenticateRequest(request, {
21+
acceptsToken: 'api_key'
22+
});
23+
24+
if (!requestState.isAuthenticated) {
25+
return new Response(401, { message: 'Unauthorized' })
26+
}
27+
28+
return new Response(JSON.stringify(requestState.toAuth()))
29+
}
30+
```

.changeset/fast-turkeys-melt.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
'@clerk/nextjs': 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 for a given route or handler using the `acceptsToken` property in the `auth()` helper, or the `token` property in the `auth.protect()` helper. Each can be set to a specific type, an array of types, or `'any'` to accept all supported tokens.
8+
9+
Example usage in Nextjs middleware:
10+
11+
```ts
12+
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
13+
14+
const isOAuthAccessible = createRouteMatcher(['/oauth(.*)'])
15+
const isApiKeyAccessible = createRouteMatcher(['/api(.*)'])
16+
const isMachineTokenAccessible = createRouteMatcher(['/m2m(.*)'])
17+
const isUserAccessible = createRouteMatcher(['/user(.*)'])
18+
const isAccessibleToAnyValidToken = createRouteMatcher(['/any(.*)'])
19+
20+
export default clerkMiddleware(async (auth, req) => {
21+
if (isOAuthAccessible(req)) await auth.protect({ token: 'oauth_token' })
22+
if (isApiKeyAccessible(req)) await auth.protect({ token: 'api_key' })
23+
if (isMachineTokenAccessible(req)) await auth.protect({ token: 'machine_token' })
24+
if (isUserAccessible(req)) await auth.protect({ token: 'session_token' })
25+
26+
if (isAccessibleToAnyValidToken(req)) await auth.protect({ token: 'any' })
27+
});
28+
29+
export const config = {
30+
matcher: [
31+
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
32+
'/(api|trpc)(.*)',
33+
],
34+
}
35+
```
36+
37+
Leaf node route protection:
38+
39+
```ts
40+
import { auth } from '@clerk/nextjs/server'
41+
42+
// In this example, we allow users and oauth tokens with the "profile" scope
43+
// to access the data. Other types of tokens are rejected.
44+
function POST(req, res) {
45+
const authObject = await auth({ acceptsToken: ['session_token', 'oauth_token'] })
46+
47+
if (authObject.tokenType === 'oauth_token' &&
48+
!authObject.scopes?.includes('profile')) {
49+
throw new Error('Unauthorized: OAuth token missing the "profile" scope')
50+
}
51+
52+
// get data from db using userId
53+
const data = db.select().from(user).where(eq(user.id, authObject.userId))
54+
55+
return { data }
56+
}
57+
```

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
118118
"shared/version-selector.mdx",
119119
"nextjs/auth.mdx",
120120
"nextjs/build-clerk-props.mdx",
121+
"nextjs/clerk-middleware-auth-object.mdx",
121122
"nextjs/clerk-middleware-options.mdx",
122123
"nextjs/clerk-middleware.mdx",
123124
"nextjs/create-async-get-auth.mdx",
@@ -139,6 +140,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
139140
"clerk-react/use-sign-in.mdx",
140141
"clerk-react/use-sign-up.mdx",
141142
"clerk-react/use-user.mdx",
143+
"backend/verify-machine-auth-token.mdx",
142144
"backend/verify-token-options.mdx",
143145
"backend/verify-token.mdx",
144146
"backend/verify-webhook-options.mdx",

packages/agent-toolkit/src/lib/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { AuthObject, ClerkClient } from '@clerk/backend';
1+
import type { ClerkClient } from '@clerk/backend';
2+
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
23

34
import type { ClerkTool } from './clerk-tool';
45

@@ -12,7 +13,7 @@ export type ToolkitParams = {
1213
* @default {}
1314
*/
1415
authContext?: Pick<
15-
AuthObject,
16+
SignedInAuthObject | SignedOutAuthObject,
1617
'userId' | 'sessionId' | 'sessionClaims' | 'orgId' | 'orgRole' | 'orgSlug' | 'orgPermissions' | 'actor'
1718
>;
1819
/**

packages/astro/src/server/clerk-middleware.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import type { AuthObject, ClerkClient } from '@clerk/backend';
2-
import type { AuthenticateRequestOptions, ClerkRequest, RedirectFun, RequestState } from '@clerk/backend/internal';
1+
import type { ClerkClient } from '@clerk/backend';
2+
import type {
3+
AuthenticateRequestOptions,
4+
ClerkRequest,
5+
RedirectFun,
6+
RequestState,
7+
SignedInAuthObject,
8+
SignedOutAuthObject,
9+
} from '@clerk/backend/internal';
310
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
411
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
512
import { handleNetlifyCacheInDevInstance } from '@clerk/shared/netlifyCacheHandler';
@@ -28,7 +35,7 @@ const CONTROL_FLOW_ERROR = {
2835
REDIRECT_TO_SIGN_IN: 'CLERK_PROTECT_REDIRECT_TO_SIGN_IN',
2936
};
3037

31-
type ClerkMiddlewareAuthObject = AuthObject & {
38+
type ClerkMiddlewareAuthObject = (SignedInAuthObject | SignedOutAuthObject) & {
3239
redirectToSignIn: (opts?: { returnBackUrl?: URL | string | null }) => Response;
3340
};
3441

packages/astro/src/server/get-auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AuthObject } from '@clerk/backend';
1+
import type { SignedInAuthObject, SignedOutAuthObject } from '@clerk/backend/internal';
22
import { AuthStatus, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal';
33
import { decodeJwt } from '@clerk/backend/jwt';
44
import type { PendingSessionOptions } from '@clerk/types';
@@ -7,7 +7,7 @@ import type { APIContext } from 'astro';
77
import { getSafeEnv } from './get-safe-env';
88
import { getAuthKeyFromRequest } from './utils';
99

10-
export type GetAuthReturn = AuthObject;
10+
export type GetAuthReturn = SignedInAuthObject | SignedOutAuthObject;
1111

1212
export const createGetAuth = ({ noAuthStatusMessage }: { noAuthStatusMessage: string }) => {
1313
return (

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ describe('subpath /errors exports', () => {
2222
it('should not include a breaking change', () => {
2323
expect(Object.keys(errorExports).sort()).toMatchInlineSnapshot(`
2424
[
25+
"MachineTokenVerificationError",
26+
"MachineTokenVerificationErrorCode",
2527
"SignJWTError",
2628
"TokenVerificationError",
2729
"TokenVerificationErrorAction",
@@ -37,18 +39,25 @@ describe('subpath /internal exports', () => {
3739
expect(Object.keys(internalExports).sort()).toMatchInlineSnapshot(`
3840
[
3941
"AuthStatus",
42+
"TokenType",
43+
"authenticatedMachineObject",
4044
"constants",
4145
"createAuthenticateRequest",
4246
"createClerkRequest",
4347
"createRedirect",
4448
"debugRequestState",
4549
"decorateObjectWithResources",
50+
"getMachineTokenType",
51+
"isMachineToken",
52+
"isTokenTypeAccepted",
4653
"makeAuthObjectSerializable",
4754
"reverificationError",
4855
"reverificationErrorResponse",
4956
"signedInAuthObject",
5057
"signedOutAuthObject",
5158
"stripPrivateDataFromObject",
59+
"unauthenticatedMachineObject",
60+
"verifyMachineAuthToken",
5261
]
5362
`);
5463
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { joinPaths } from '../../util/path';
2+
import type { APIKey } from '../resources/APIKey';
3+
import { AbstractAPI } from './AbstractApi';
4+
5+
const basePath = '/api_keys';
6+
7+
export class APIKeysAPI extends AbstractAPI {
8+
async verifySecret(secret: string) {
9+
return this.request<APIKey>({
10+
method: 'POST',
11+
path: joinPaths(basePath, 'verify'),
12+
bodyParams: { secret },
13+
});
14+
}
15+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { joinPaths } from '../../util/path';
2+
import type { IdPOAuthAccessToken } from '../resources';
3+
import { AbstractAPI } from './AbstractApi';
4+
5+
const basePath = '/oauth_applications/access_tokens';
6+
7+
export class IdPOAuthAccessTokenApi extends AbstractAPI {
8+
async verifySecret(secret: string) {
9+
return this.request<IdPOAuthAccessToken>({
10+
method: 'POST',
11+
path: joinPaths(basePath, 'verify'),
12+
bodyParams: { secret },
13+
});
14+
}
15+
}

0 commit comments

Comments
 (0)