diff --git a/.changeset/fix-hono-getauth-accepts-token.md b/.changeset/fix-hono-getauth-accepts-token.md new file mode 100644 index 00000000000..2cdeeebd833 --- /dev/null +++ b/.changeset/fix-hono-getauth-accepts-token.md @@ -0,0 +1,5 @@ +--- +'@clerk/hono': patch +--- + +Add support for `CLERK_MACHINE_SECRET_KEY` environment variable. This enables M2M token scope verification without needing to pass `machineSecretKey` explicitly to `clerkMiddleware()`. diff --git a/integration/templates/express-vite/src/client/main.ts b/integration/templates/express-vite/src/client/main.ts index a21f68d5c23..380161f1d77 100644 --- a/integration/templates/express-vite/src/client/main.ts +++ b/integration/templates/express-vite/src/client/main.ts @@ -9,6 +9,8 @@ document.addEventListener('DOMContentLoaded', async function () { await clerk.load({ ui: { ClerkUI }, }); + // @ts-expect-error: Make waitForSession test utility work + window.Clerk = clerk; if (clerk.isSignedIn) { document.getElementById('app')!.innerHTML = ` diff --git a/integration/templates/hono-vite/src/client/main.ts b/integration/templates/hono-vite/src/client/main.ts index a21f68d5c23..380161f1d77 100644 --- a/integration/templates/hono-vite/src/client/main.ts +++ b/integration/templates/hono-vite/src/client/main.ts @@ -9,6 +9,8 @@ document.addEventListener('DOMContentLoaded', async function () { await clerk.load({ ui: { ClerkUI }, }); + // @ts-expect-error: Make waitForSession test utility work + window.Clerk = clerk; if (clerk.isSignedIn) { document.getElementById('app')!.innerHTML = ` diff --git a/integration/tests/express/machine.test.ts b/integration/tests/express/machine.test.ts new file mode 100644 index 00000000000..921fa228692 --- /dev/null +++ b/integration/tests/express/machine.test.ts @@ -0,0 +1,142 @@ +import { test } from '@playwright/test'; + +import { appConfigs } from '../../presets'; +import type { MachineAuthTestAdapter } from '../../testUtils/machineAuthHelpers'; +import { + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../../testUtils/machineAuthHelpers'; + +const createMainFile = () => ` +import 'dotenv/config'; + +import { clerkMiddleware } from '@clerk/express'; +import express from 'express'; +import ViteExpress from 'vite-express'; +import { machineRoutes } from './routes/machine'; + +const app = express(); + +app.use(express.json()); +app.use( + clerkMiddleware({ + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + }), +); + +app.use('/api', machineRoutes); + +const port = parseInt(process.env.PORT as string) || 3002; +ViteExpress.listen(app, port, () => console.log(\`Server is listening on port \${port}...\`)); +`; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.express.vite, + apiKey: { + path: '/api/me', + addRoutes: config => + config + .addFile( + 'src/server/routes/machine.ts', + () => ` +import { getAuth } from '@clerk/express'; +import { Router } from 'express'; + +const router = Router(); + +router.get('/me', (req: any, res: any) => { + const { userId, tokenType } = getAuth(req, { acceptsToken: 'api_key' }); + + if (!userId) { + res.status(401).send('Unauthorized'); + return; + } + + res.json({ userId, tokenType }); +}); + +router.post('/me', (req: any, res: any) => { + const authObject = getAuth(req, { acceptsToken: ['api_key', 'session_token'] }); + + if (!authObject.isAuthenticated) { + res.status(401).send('Unauthorized'); + return; + } + + res.json({ userId: authObject.userId, tokenType: authObject.tokenType }); +}); + +export const machineRoutes = router; + `, + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, + m2m: { + path: '/api/m2m', + addRoutes: config => + config + .addFile( + 'src/server/routes/machine.ts', + () => ` +import { getAuth } from '@clerk/express'; +import { Router } from 'express'; + +const router = Router(); + +router.get('/m2m', (req: any, res: any) => { + const { subject, tokenType, machineId } = getAuth(req, { acceptsToken: 'm2m_token' }); + + if (!machineId) { + res.status(401).send('Unauthorized'); + return; + } + + res.json({ subject, tokenType }); +}); + +export const machineRoutes = router; + `, + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, + oauth: { + verifyPath: '/api/oauth-verify', + callbackPath: '/api/oauth/callback', + addRoutes: config => + config + .addFile( + 'src/server/routes/machine.ts', + () => ` +import { getAuth } from '@clerk/express'; +import { Router } from 'express'; + +const router = Router(); + +router.get('/oauth-verify', (req: any, res: any) => { + const { userId, tokenType } = getAuth(req, { acceptsToken: 'oauth_token' }); + + if (!userId) { + res.status(401).send('Unauthorized'); + return; + } + + res.json({ userId, tokenType }); +}); + +router.get('/oauth/callback', (_req: any, res: any) => { + res.json({ message: 'OAuth callback received' }); +}); + +export const machineRoutes = router; + `, + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, +}; + +test.describe('Express machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); +}); diff --git a/integration/tests/hono/machine.test.ts b/integration/tests/hono/machine.test.ts new file mode 100644 index 00000000000..16d0fddd9e6 --- /dev/null +++ b/integration/tests/hono/machine.test.ts @@ -0,0 +1,128 @@ +import { test } from '@playwright/test'; + +import { appConfigs } from '../../presets'; +import type { MachineAuthTestAdapter } from '../../testUtils/machineAuthHelpers'; +import { + registerApiKeyAuthTests, + registerM2MAuthTests, + registerOAuthAuthTests, +} from '../../testUtils/machineAuthHelpers'; + +const createAppFile = (routes: string) => ` +import { clerkMiddleware, getAuth } from '@clerk/hono'; +import { Hono } from 'hono'; + +const app = new Hono(); + +app.use( + '*', + clerkMiddleware({ + publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY, + }), +); + +${routes} + +export default app; +`; + +const createMainFile = () => ` +import 'dotenv/config'; + +import { getRequestListener } from '@hono/node-server'; +import express from 'express'; +import ViteExpress from 'vite-express'; +import app from './app'; + +const expressApp = express(); +const honoRequestListener = getRequestListener(app.fetch); + +expressApp.use('/api', async (req: any, res: any) => { + await honoRequestListener(req, res); +}); + +const port = parseInt(process.env.PORT as string) || 3002; +ViteExpress.listen(expressApp, port, () => console.log(\`Server is listening on port \${port}...\`)); +`; + +const adapter: MachineAuthTestAdapter = { + baseConfig: appConfigs.hono.vite, + apiKey: { + path: '/api/me', + addRoutes: config => + config + .addFile('src/server/app.ts', () => + createAppFile(` +app.get('/me', c => { + const { userId, tokenType } = getAuth(c, { acceptsToken: 'api_key' }); + + if (!userId) { + return c.text('Unauthorized', 401); + } + + return c.json({ userId, tokenType }); +}); + +app.post('/me', c => { + const authObject = getAuth(c, { acceptsToken: ['api_key', 'session_token'] }); + + if (!authObject.isAuthenticated) { + return c.text('Unauthorized', 401); + } + + return c.json({ userId: authObject.userId, tokenType: authObject.tokenType }); +}); +`), + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, + m2m: { + path: '/api/m2m', + addRoutes: config => + config + .addFile('src/server/app.ts', () => + createAppFile(` +app.get('/m2m', c => { + const { subject, tokenType, machineId } = getAuth(c, { acceptsToken: 'm2m_token' }); + + if (!machineId) { + return c.text('Unauthorized', 401); + } + + return c.json({ subject, tokenType }); +}); +`), + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, + oauth: { + verifyPath: '/api/oauth-verify', + callbackPath: '/api/oauth/callback', + addRoutes: config => + config + .addFile('src/server/app.ts', () => + createAppFile(` +app.get('/oauth-verify', c => { + const { userId, tokenType } = getAuth(c, { acceptsToken: 'oauth_token' }); + + if (!userId) { + return c.text('Unauthorized', 401); + } + + return c.json({ userId, tokenType }); +}); + +app.get('/oauth/callback', c => { + return c.json({ message: 'OAuth callback received' }); +}); +`), + ) + .addFile('src/server/main.ts', () => createMainFile()), + }, +}; + +test.describe('Hono machine authentication @machine', () => { + registerApiKeyAuthTests(adapter); + registerM2MAuthTests(adapter); + registerOAuthAuthTests(adapter); +}); diff --git a/packages/hono/src/clerkMiddleware.ts b/packages/hono/src/clerkMiddleware.ts index 98b15182d6e..67bf53686c5 100644 --- a/packages/hono/src/clerkMiddleware.ts +++ b/packages/hono/src/clerkMiddleware.ts @@ -11,6 +11,7 @@ import type { FrontendApiProxyOptions } from './types'; type ClerkEnv = { CLERK_SECRET_KEY: string; CLERK_PUBLISHABLE_KEY: string; + CLERK_MACHINE_SECRET_KEY?: string; CLERK_API_URL?: string; CLERK_API_VERSION?: string; }; @@ -43,6 +44,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan const { secretKey = clerkEnv.CLERK_SECRET_KEY || '', publishableKey = clerkEnv.CLERK_PUBLISHABLE_KEY || '', + machineSecretKey = clerkEnv.CLERK_MACHINE_SECRET_KEY || '', apiUrl = clerkEnv.CLERK_API_URL, apiVersion = clerkEnv.CLERK_API_VERSION, frontendApiProxy, @@ -92,6 +94,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan apiVersion, secretKey, publishableKey, + machineSecretKey, userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`, }); @@ -99,6 +102,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan ...rest, secretKey, publishableKey, + machineSecretKey, proxyUrl: derivedProxyUrl, acceptsToken: 'any', }); @@ -120,7 +124,7 @@ export const clerkMiddleware = (options?: ClerkMiddlewareOptions): MiddlewareHan const authObjectFn = ((authOptions?: AuthOptions) => getAuthObjectForAcceptedToken({ authObject: requestState.toAuth(authOptions) as AuthObject, - acceptsToken: 'any', + acceptsToken: authOptions?.acceptsToken, })) as GetAuthFnNoRequest; c.set('clerkAuth', authObjectFn);