Skip to content

Commit 293e989

Browse files
feat(clerk): Add machine auth support (#1664)
* feat(clerk): Add machine auth support * chore: accommodate updated exports * bump clerk deps * chore: add changeset * test: add machine auth tests * fix types * ci: apply automated fixes * update readme --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 16a83af commit 293e989

File tree

6 files changed

+237
-116
lines changed

6 files changed

+237
-116
lines changed

.changeset/long-flies-begin.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@hono/clerk-auth": 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 { clerkMiddleware, getAuth } from '@hono/clerk-auth'
13+
import { Hono } from 'hono'
14+
15+
const app = new Hono()
16+
17+
app.use('*', clerkMiddleware())
18+
app.get('/api/protected', (c) => {
19+
const auth = getAuth(c, { acceptsToken: 'any' })
20+
21+
if (!auth.isAuthenticated) {
22+
// do something for unauthenticated requests
23+
}
24+
25+
if (authObject.tokenType === 'session_token') {
26+
console.log('this is session token from a user')
27+
} else {
28+
console.log('this is some other type of machine token')
29+
console.log('more specifically, a ' + authObject.tokenType)
30+
}
31+
})
32+
```

packages/clerk-auth/README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ const app = new Hono()
3131

3232
app.use('*', clerkMiddleware())
3333
app.get('/', (c) => {
34-
const auth = getAuth(c)
34+
const { userId } = getAuth(c)
3535

36-
if (!auth?.userId) {
36+
if (!userId) {
3737
return c.json({
3838
message: 'You are not logged in.',
3939
})
4040
}
4141

4242
return c.json({
4343
message: 'You are logged in!',
44-
userId: auth.userId,
44+
userId,
4545
})
4646
})
4747

@@ -79,6 +79,33 @@ app.get('/', async (c) => {
7979
export default app
8080
```
8181

82+
## Using `acceptsToken` to verify other types of token
83+
84+
```ts
85+
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
86+
import { Hono } from 'hono'
87+
88+
const app = new Hono()
89+
90+
app.use('*', clerkMiddleware())
91+
app.get('/', (c) => {
92+
const { userId } = getAuth(c, { acceptsToken: 'api_key' })
93+
94+
if (!userId) {
95+
return c.json({
96+
message: 'You are not logged in.',
97+
})
98+
}
99+
100+
return c.json({
101+
message: 'You are logged in!',
102+
userId,
103+
})
104+
})
105+
106+
export default app
107+
```
108+
82109
## Author
83110

84111
Vaggelis Yfantis <https://github.com/octoper>

packages/clerk-auth/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
},
4343
"homepage": "https://github.com/honojs/middleware",
4444
"dependencies": {
45-
"@clerk/backend": "^2.4.1",
46-
"@clerk/types": "^4.64.0"
45+
"@clerk/backend": "^2.29.2",
46+
"@clerk/shared": "^3.42.0"
4747
},
4848
"peerDependencies": {
4949
"hono": ">=3.0.0"

packages/clerk-auth/src/clerk-auth.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
import { createClerkClient } from '@clerk/backend'
2-
import type { ClerkClient, SessionAuthObject } from '@clerk/backend'
3-
import type { AuthenticateRequestOptions } from '@clerk/backend/internal'
4-
import { TokenType } from '@clerk/backend/internal'
2+
import type { AuthObject, ClerkClient } from '@clerk/backend'
3+
import type {
4+
AuthenticateRequestOptions,
5+
AuthOptions,
6+
GetAuthFn,
7+
GetAuthFnNoRequest,
8+
} from '@clerk/backend/internal'
9+
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'
510
import type { Context, MiddlewareHandler } from 'hono'
611
import { env } from 'hono/adapter'
712

813
export type ClerkAuthVariables = {
914
clerk: ClerkClient
10-
clerkAuth: () => SessionAuthObject | null
15+
clerkAuth: GetAuthFnNoRequest
1116
}
1217

13-
export const getAuth = (c: Context): SessionAuthObject | null => {
18+
export const getAuth: GetAuthFn<Context> = ((c: Context, options?: AuthOptions) => {
1419
const authFn = c.get('clerkAuth')
15-
return authFn()
16-
}
20+
return authFn(options)
21+
}) as GetAuthFn<Context>
1722

1823
type ClerkEnv = {
1924
CLERK_SECRET_KEY: string
@@ -53,7 +58,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan
5358
...rest,
5459
secretKey,
5560
publishableKey,
56-
acceptsToken: TokenType.SessionToken,
61+
acceptsToken: 'any',
5762
})
5863

5964
if (requestState.headers) {
@@ -70,8 +75,13 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan
7075
}
7176
}
7277

73-
// Options will be added soon
74-
c.set('clerkAuth', () => requestState.toAuth())
78+
const authObjectFn = ((options?: AuthOptions) =>
79+
getAuthObjectForAcceptedToken({
80+
authObject: requestState.toAuth(options) as AuthObject,
81+
acceptsToken: 'any',
82+
})) as GetAuthFnNoRequest
83+
84+
c.set('clerkAuth', authObjectFn)
7585
c.set('clerk', clerkClient)
7686

7787
await next()

packages/clerk-auth/src/index.test.ts

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ const EnvVariables = {
66
CLERK_PUBLISHABLE_KEY: 'TEST_API_KEY',
77
}
88

9+
const createMockSessionAuth = () => ({
10+
tokenType: 'session_token' as const,
11+
userId: 'user_123',
12+
sessionId: 'sess_456',
13+
orgId: null,
14+
orgRole: null,
15+
orgSlug: null,
16+
})
17+
918
const authenticateRequestMock = vi.fn()
1019

1120
vi.mock(import('@clerk/backend'), async (importOriginal) => {
@@ -31,7 +40,7 @@ describe('clerkMiddleware()', () => {
3140
test('handles signin with Authorization Bearer', async () => {
3241
authenticateRequestMock.mockResolvedValueOnce({
3342
headers: new Headers(),
34-
toAuth: () => 'mockedAuth',
43+
toAuth: createMockSessionAuth,
3544
})
3645
const app = new Hono()
3746
app.use('*', clerkMiddleware())
@@ -57,7 +66,7 @@ describe('clerkMiddleware()', () => {
5766
const response = await app.request(req)
5867

5968
expect(response.status).toEqual(200)
60-
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
69+
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
6170
expect(authenticateRequestMock).toHaveBeenCalledWith(
6271
expect.any(Request),
6372
expect.objectContaining({
@@ -69,7 +78,7 @@ describe('clerkMiddleware()', () => {
6978
test('handles signin with cookie', async () => {
7079
authenticateRequestMock.mockResolvedValueOnce({
7180
headers: new Headers(),
72-
toAuth: () => 'mockedAuth',
81+
toAuth: createMockSessionAuth,
7382
})
7483
const app = new Hono()
7584
app.use('*', clerkMiddleware())
@@ -95,7 +104,7 @@ describe('clerkMiddleware()', () => {
95104
const response = await app.request(req)
96105

97106
expect(response.status).toEqual(200)
98-
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
107+
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
99108
expect(authenticateRequestMock).toHaveBeenCalledWith(
100109
expect.any(Request),
101110
expect.objectContaining({
@@ -115,7 +124,7 @@ describe('clerkMiddleware()', () => {
115124
'x-clerk-auth-reason': 'auth-reason',
116125
'x-clerk-auth-status': 'handshake',
117126
}),
118-
toAuth: () => 'mockedAuth',
127+
toAuth: createMockSessionAuth,
119128
})
120129
const app = new Hono()
121130
app.use('*', clerkMiddleware())
@@ -145,7 +154,7 @@ describe('clerkMiddleware()', () => {
145154
test('handles signout case by populating the req.auth', async () => {
146155
authenticateRequestMock.mockResolvedValueOnce({
147156
headers: new Headers(),
148-
toAuth: () => 'mockedAuth',
157+
toAuth: createMockSessionAuth,
149158
})
150159
const app = new Hono()
151160
app.use('*', clerkMiddleware())
@@ -164,12 +173,100 @@ describe('clerkMiddleware()', () => {
164173
const response = await app.request(req)
165174

166175
expect(response.status).toEqual(200)
167-
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
176+
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
168177
expect(authenticateRequestMock).toHaveBeenCalledWith(
169178
expect.any(Request),
170179
expect.objectContaining({
171180
secretKey: EnvVariables.CLERK_SECRET_KEY,
172181
})
173182
)
174183
})
184+
185+
describe('Machine Auth', () => {
186+
test('handles machine auth with api_key using acceptsToken as string', async () => {
187+
authenticateRequestMock.mockResolvedValueOnce({
188+
headers: new Headers(),
189+
toAuth: () => ({
190+
tokenType: 'api_key',
191+
id: 'ak_1234',
192+
userId: 'user_456',
193+
orgId: null,
194+
}),
195+
})
196+
const app = new Hono()
197+
app.use('*', clerkMiddleware())
198+
199+
app.get('/', (ctx) => {
200+
const auth = getAuth(ctx, { acceptsToken: 'api_key' })
201+
return ctx.json({ auth })
202+
})
203+
204+
const req = new Request('http://localhost/', {
205+
headers: {
206+
Authorization: 'Bearer ak_deadbeef',
207+
},
208+
})
209+
210+
const response = await app.request(req)
211+
212+
expect(response.status).toEqual(200)
213+
expect(await response.json()).toEqual({
214+
auth: {
215+
tokenType: 'api_key',
216+
id: 'ak_1234',
217+
userId: 'user_456',
218+
orgId: null,
219+
},
220+
})
221+
expect(authenticateRequestMock).toHaveBeenCalledWith(
222+
expect.any(Request),
223+
expect.objectContaining({
224+
secretKey: EnvVariables.CLERK_SECRET_KEY,
225+
})
226+
)
227+
})
228+
229+
test('handles machine auth with api_key using acceptsToken as array', async () => {
230+
authenticateRequestMock.mockResolvedValueOnce({
231+
headers: new Headers(),
232+
toAuth: () => ({
233+
tokenType: 'api_key',
234+
id: 'ak_5678',
235+
userId: 'user_789',
236+
orgId: null,
237+
}),
238+
})
239+
const app = new Hono()
240+
app.use('*', clerkMiddleware())
241+
242+
app.get('/', (ctx) => {
243+
const auth = getAuth(ctx, { acceptsToken: ['api_key', 'session_token'] })
244+
return ctx.json({ auth })
245+
})
246+
247+
const req = new Request('http://localhost/', {
248+
headers: {
249+
Authorization: 'Bearer ak_deadbeef',
250+
},
251+
})
252+
253+
const response = await app.request(req)
254+
255+
expect(response.status).toEqual(200)
256+
expect(await response.json()).toEqual({
257+
auth: {
258+
tokenType: 'api_key',
259+
id: 'ak_5678',
260+
userId: 'user_789',
261+
orgId: null,
262+
},
263+
})
264+
expect(authenticateRequestMock).toHaveBeenCalledWith(
265+
expect.any(Request),
266+
expect.objectContaining({
267+
secretKey: EnvVariables.CLERK_SECRET_KEY,
268+
})
269+
)
270+
})
271+
})
175272
})

0 commit comments

Comments
 (0)