Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 32 additions & 0 deletions .changeset/long-flies-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
"@hono/clerk-auth": 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 { clerkMiddleware, getAuth } from '@hono/clerk-auth'
import { Hono } from 'hono'

const app = new Hono()

app.use('*', clerkMiddleware())
app.get('/api/protected', (c) => {
const auth = getAuth(c, { acceptsToken: 'any' })

if (!auth.isAuthenticated) {
// do something for unauthenticated requests
}

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)
}
})
```
33 changes: 30 additions & 3 deletions packages/clerk-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ const app = new Hono()

app.use('*', clerkMiddleware())
app.get('/', (c) => {
const auth = getAuth(c)
const { userId } = getAuth(c)

if (!auth?.userId) {
if (!userId) {
return c.json({
message: 'You are not logged in.',
})
}

return c.json({
message: 'You are logged in!',
userId: auth.userId,
userId,
})
})

Expand Down Expand Up @@ -79,6 +79,33 @@ app.get('/', async (c) => {
export default app
```

## Using `acceptsToken` to verify other types of token

```ts
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
import { Hono } from 'hono'

const app = new Hono()

app.use('*', clerkMiddleware())
app.get('/', (c) => {
const { userId } = getAuth(c, { acceptsToken: 'api_key' })

if (!userId) {
return c.json({
message: 'You are not logged in.',
})
}

return c.json({
message: 'You are logged in!',
userId,
})
})

export default app
```

## Author

Vaggelis Yfantis <https://github.com/octoper>
4 changes: 2 additions & 2 deletions packages/clerk-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
},
"homepage": "https://github.com/honojs/middleware",
"dependencies": {
"@clerk/backend": "^2.4.1",
"@clerk/types": "^4.64.0"
"@clerk/backend": "^2.29.2",
"@clerk/shared": "^3.42.0"
},
"peerDependencies": {
"hono": ">=3.0.0"
Expand Down
30 changes: 20 additions & 10 deletions packages/clerk-auth/src/clerk-auth.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { createClerkClient } from '@clerk/backend'
import type { ClerkClient, SessionAuthObject } from '@clerk/backend'
import type { AuthenticateRequestOptions } from '@clerk/backend/internal'
import { TokenType } from '@clerk/backend/internal'
import type { AuthObject, ClerkClient } from '@clerk/backend'
import type {
AuthenticateRequestOptions,
AuthOptions,
GetAuthFn,
GetAuthFnNoRequest,
} from '@clerk/backend/internal'
import { getAuthObjectForAcceptedToken } from '@clerk/backend/internal'
import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter'

export type ClerkAuthVariables = {
clerk: ClerkClient
clerkAuth: () => SessionAuthObject | null
clerkAuth: GetAuthFnNoRequest
}

export const getAuth = (c: Context): SessionAuthObject | null => {
export const getAuth: GetAuthFn<Context> = ((c: Context, options?: AuthOptions) => {
const authFn = c.get('clerkAuth')
return authFn()
}
return authFn(options)
}) as GetAuthFn<Context>

type ClerkEnv = {
CLERK_SECRET_KEY: string
Expand Down Expand Up @@ -53,7 +58,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan
...rest,
secretKey,
publishableKey,
acceptsToken: TokenType.SessionToken,
acceptsToken: 'any',
})

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

// Options will be added soon
c.set('clerkAuth', () => requestState.toAuth())
const authObjectFn = ((options?: AuthOptions) =>
getAuthObjectForAcceptedToken({
authObject: requestState.toAuth(options) as AuthObject,
acceptsToken: 'any',
})) as GetAuthFnNoRequest

c.set('clerkAuth', authObjectFn)
c.set('clerk', clerkClient)

await next()
Expand Down
111 changes: 104 additions & 7 deletions packages/clerk-auth/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ const EnvVariables = {
CLERK_PUBLISHABLE_KEY: 'TEST_API_KEY',
}

const createMockSessionAuth = () => ({
tokenType: 'session_token' as const,
userId: 'user_123',
sessionId: 'sess_456',
orgId: null,
orgRole: null,
orgSlug: null,
})

const authenticateRequestMock = vi.fn()

vi.mock(import('@clerk/backend'), async (importOriginal) => {
Expand All @@ -31,7 +40,7 @@ describe('clerkMiddleware()', () => {
test('handles signin with Authorization Bearer', async () => {
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
toAuth: createMockSessionAuth,
})
const app = new Hono()
app.use('*', clerkMiddleware())
Expand All @@ -57,7 +66,7 @@ describe('clerkMiddleware()', () => {
const response = await app.request(req)

expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
Expand All @@ -69,7 +78,7 @@ describe('clerkMiddleware()', () => {
test('handles signin with cookie', async () => {
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
toAuth: createMockSessionAuth,
})
const app = new Hono()
app.use('*', clerkMiddleware())
Expand All @@ -95,7 +104,7 @@ describe('clerkMiddleware()', () => {
const response = await app.request(req)

expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
Expand All @@ -115,7 +124,7 @@ describe('clerkMiddleware()', () => {
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-status': 'handshake',
}),
toAuth: () => 'mockedAuth',
toAuth: createMockSessionAuth,
})
const app = new Hono()
app.use('*', clerkMiddleware())
Expand Down Expand Up @@ -145,7 +154,7 @@ describe('clerkMiddleware()', () => {
test('handles signout case by populating the req.auth', async () => {
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
toAuth: createMockSessionAuth,
})
const app = new Hono()
app.use('*', clerkMiddleware())
Expand All @@ -164,12 +173,100 @@ describe('clerkMiddleware()', () => {
const response = await app.request(req)

expect(response.status).toEqual(200)
expect(await response.json()).toEqual({ auth: 'mockedAuth' })
expect(await response.json()).toEqual({ auth: createMockSessionAuth() })
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
})
)
})

describe('Machine Auth', () => {
test('handles machine auth with api_key using acceptsToken as string', async () => {
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => ({
tokenType: 'api_key',
id: 'ak_1234',
userId: 'user_456',
orgId: null,
}),
})
const app = new Hono()
app.use('*', clerkMiddleware())

app.get('/', (ctx) => {
const auth = getAuth(ctx, { acceptsToken: 'api_key' })
return ctx.json({ auth })
})

const req = new Request('http://localhost/', {
headers: {
Authorization: 'Bearer ak_deadbeef',
},
})

const response = await app.request(req)

expect(response.status).toEqual(200)
expect(await response.json()).toEqual({
auth: {
tokenType: 'api_key',
id: 'ak_1234',
userId: 'user_456',
orgId: null,
},
})
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
})
)
})

test('handles machine auth with api_key using acceptsToken as array', async () => {
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => ({
tokenType: 'api_key',
id: 'ak_5678',
userId: 'user_789',
orgId: null,
}),
})
const app = new Hono()
app.use('*', clerkMiddleware())

app.get('/', (ctx) => {
const auth = getAuth(ctx, { acceptsToken: ['api_key', 'session_token'] })
return ctx.json({ auth })
})

const req = new Request('http://localhost/', {
headers: {
Authorization: 'Bearer ak_deadbeef',
},
})

const response = await app.request(req)

expect(response.status).toEqual(200)
expect(await response.json()).toEqual({
auth: {
tokenType: 'api_key',
id: 'ak_5678',
userId: 'user_789',
orgId: null,
},
})
expect(authenticateRequestMock).toHaveBeenCalledWith(
expect.any(Request),
expect.objectContaining({
secretKey: EnvVariables.CLERK_SECRET_KEY,
})
)
})
})
})
Loading
Loading