Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

clerk-auth: Migrate to Clerk Core v2 #465

Merged
merged 19 commits into from
Apr 25, 2024
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
5 changes: 5 additions & 0 deletions .changeset/lazy-pumpkins-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/clerk-auth': major
---

Migrate to Clerk Core v2
4 changes: 0 additions & 4 deletions packages/clerk-auth/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,4 @@ module.exports = {
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
transform: { '^.+\\.m?tsx?$': 'ts-jest' },
testPathIgnorePatterns: ['/node_modules/', '/jest/'],
moduleNameMapper: {
'#crypto': '@clerk/backend/dist/runtime/node/crypto.js',
MonsterDeveloper marked this conversation as resolved.
Show resolved Hide resolved
'#fetch': '@clerk/backend/dist/runtime/node/fetch.js',
},
}
7 changes: 5 additions & 2 deletions packages/clerk-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@
},
"homepage": "https://github.com/honojs/middleware",
"peerDependencies": {
"@clerk/backend": ">=0.30.0 <1",
"@clerk/backend": "^1.0.0",
"hono": ">=3.*"
},
"devDependencies": {
"@clerk/backend": "^0.30.1",
"@clerk/backend": "^1.0.0",
"@types/react": "^18",
"hono": "^3.11.7",
"jest": "^29.7.0",
"node-fetch-native": "^1.4.0",
"react": "^18.2.0",
"tsup": "^8.0.1"
},
"engines": {
"node": ">=16.x.x"
}
}
50 changes: 17 additions & 33 deletions packages/clerk-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import type { ClerkOptions } from '@clerk/backend'
import { Clerk, createIsomorphicRequest, constants } from '@clerk/backend'
import { type ClerkClient, type ClerkOptions, createClerkClient } from '@clerk/backend'
import type { Context, MiddlewareHandler } from 'hono'
import { env } from 'hono/adapter'

type ClerkAuth = Awaited<ReturnType<ReturnType<typeof Clerk>['authenticateRequest']>>['toAuth']
type ClerkAuth = ReturnType<Awaited<ReturnType<ClerkClient['authenticateRequest']>>['toAuth']>

declare module 'hono' {
MonsterDeveloper marked this conversation as resolved.
Show resolved Hide resolved
interface ContextVariableMap {
clerk: ReturnType<typeof Clerk>
clerkAuth: ReturnType<ClerkAuth>
clerk: ClerkClient
clerkAuth: ClerkAuth
}
}

Expand All @@ -21,7 +20,6 @@ type ClerkEnv = {
CLERK_PUBLISHABLE_KEY: string
CLERK_API_URL: string
CLERK_API_VERSION: string
CLERK_FRONTEND_API: string
}

export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
Expand All @@ -30,10 +28,9 @@ export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
const { secretKey, publishableKey, apiUrl, apiVersion, ...rest } = options || {
secretKey: clerkEnv.CLERK_SECRET_KEY || '',
publishableKey: clerkEnv.CLERK_PUBLISHABLE_KEY || '',
apiUrl: clerkEnv.CLERK_API_URL || 'https://api.clerk.dev',
apiVersion: clerkEnv.CLERK_API_VERSION || 'v1',
apiUrl: clerkEnv.CLERK_API_URL,
apiVersion: clerkEnv.CLERK_API_VERSION,
}
const frontendApi = clerkEnv.CLERK_FRONTEND_API || ''
if (!secretKey) {
throw new Error('Missing Clerk Secret key')
}
Expand All @@ -42,43 +39,30 @@ export const clerkMiddleware = (options?: ClerkOptions): MiddlewareHandler => {
throw new Error('Missing Clerk Publishable key')
}

const clerkClient = Clerk({
const clerkClient = createClerkClient({
...rest,
apiUrl,
apiVersion,
secretKey,
publishableKey,
})

const requestState = await clerkClient.authenticateRequest({
const requestState = await clerkClient.authenticateRequest(c.req.raw, {
MonsterDeveloper marked this conversation as resolved.
Show resolved Hide resolved
...rest,
secretKey,
publishableKey,
request: createIsomorphicRequest((Request) => {
return new Request(c.req.url, {
method: c.req.method,
headers: c.req.raw.headers,
})
}),
})

// Interstitial cases
if (requestState.isUnknown) {
c.header(constants.Headers.AuthReason, requestState.reason)
c.header(constants.Headers.AuthMessage, requestState.message)
return c.body(null, 401)
}

if (requestState.isInterstitial) {
const interstitialHtmlPage = clerkClient.localInterstitial({
publishableKey,
frontendApi,
})

c.header(constants.Headers.AuthReason, requestState.reason)
c.header(constants.Headers.AuthMessage, requestState.message)
if (requestState.headers) {
requestState.headers.forEach((value, key) => c.res.headers.append(key, value))

return c.html(interstitialHtmlPage, 401)
const locationHeader = requestState.headers.get('location')

if (locationHeader) {
return c.redirect(locationHeader, 307)
} else if (requestState.status === 'handshake') {
throw new Error('Clerk: unexpected handshake without redirect')
}
}

c.set('clerkAuth', requestState.toAuth())
Expand Down
99 changes: 31 additions & 68 deletions packages/clerk-auth/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ const EnvVariables = {
}

const authenticateRequestMock = jest.fn()
const localInterstitialMock = jest.fn()

jest.mock('@clerk/backend', () => {
return {
...jest.requireActual('@clerk/backend'),
Clerk: () => {
createClerkClient: () => {
return {
authenticateRequest: (...args: any) => authenticateRequestMock(...args),
localInterstitial: (...args: any) => localInterstitialMock(...args),
}
},
}
Expand All @@ -36,10 +34,8 @@ describe('clerkMiddleware()', () => {
})

test('handles signin with Authorization Bearer', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: true,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
Expand Down Expand Up @@ -67,20 +63,17 @@ describe('clerkMiddleware()', () => {

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

test('handles signin with cookie', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: true,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
Expand Down Expand Up @@ -108,22 +101,25 @@ describe('clerkMiddleware()', () => {

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

test('handles unknown case by terminating the request with empty response and 401 http code', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: true,
isInterstitial: false,
isSignedIn: false,
test('handles handshake case by redirecting the request to fapi', async () => {
authenticateRequestMock.mockResolvedValueOnce({
status: 'handshake',
reason: 'auth-reason',
message: 'auth-message',
headers: new Headers({
location: 'https://fapi.example.com/v1/clients/handshake',
'x-clerk-auth-message': 'auth-message',
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-status': 'handshake',
}),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
Expand All @@ -142,50 +138,18 @@ describe('clerkMiddleware()', () => {

const response = await app.request(req)

expect(response.status).toEqual(401)
expect(response.headers.get('x-clerk-auth-reason')).toEqual('auth-reason')
expect(response.headers.get('x-clerk-auth-message')).toEqual('auth-message')
expect(await response.text()).toEqual('')
})

test('handles interstitial case by terminating the request with interstitial html page and 401 http code', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: true,
isSignedIn: false,
reason: 'auth-reason',
message: 'auth-message',
toAuth: () => 'mockedAuth',
expect(response.status).toEqual(307)
expect(Object.fromEntries(response.headers.entries())).toMatchObject({
location: 'https://fapi.example.com/v1/clients/handshake',
'x-clerk-auth-status': 'handshake',
'x-clerk-auth-reason': 'auth-reason',
'x-clerk-auth-message': 'auth-message',
})
localInterstitialMock.mockReturnValue('<html><body>Interstitial</body></html>')
const app = new Hono()
app.use('*', clerkMiddleware())

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

const req = new Request('http://localhost/', {
headers: {
cookie: '_gcl_au=value1; ko_id=value2; __session=deadbeef; __client_uat=1675692233',
},
})

const response = await app.request(req)

expect(response.status).toEqual(401)
expect(response.headers.get('content-type')).toMatch('text/html')
expect(response.headers.get('x-clerk-auth-reason')).toEqual('auth-reason')
expect(response.headers.get('x-clerk-auth-message')).toEqual('auth-message')
expect(await response.text()).toEqual('<html><body>Interstitial</body></html>')
})

test('handles signout case by populating the req.auth', async () => {
authenticateRequestMock.mockResolvedValue({
isUnknown: false,
isInterstitial: false,
isSignedIn: false,
authenticateRequestMock.mockResolvedValueOnce({
headers: new Headers(),
toAuth: () => 'mockedAuth',
})
const app = new Hono()
Expand All @@ -206,12 +170,11 @@ describe('clerkMiddleware()', () => {

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