diff --git a/server/middleware/01.auth.ts b/server/middleware/01.auth.ts index 5a1da4c6..f0f0758d 100644 --- a/server/middleware/01.auth.ts +++ b/server/middleware/01.auth.ts @@ -39,6 +39,17 @@ const PUBLIC_PATHS = [ '/api/media/', // Media management API — Bearer CDN key (media:* scope) ] +// Migrate's server-to-server calls carry no session: each one is a request +// signed with Migrate's key, verified inside the route. Exact paths, never a +// prefix: `/api/migrate/claim` and `/api/migrate/grants/:id/*` are user +// routes and stay behind the session. +const MIGRATE_S2S_PATHS = [ + '/api/migrate/account-state', + '/api/migrate/provision', + '/api/migrate/grants/status', + '/api/migrate/grants/install-url', +] + // Refresh tokens 5 minutes before expiry to avoid edge-case failures const REFRESH_BUFFER_SECONDS = 5 * 60 @@ -46,7 +57,7 @@ export default defineEventHandler(async (event) => { const path = getRequestPath(event) // Skip non-API routes and public paths - if (!path.startsWith('/api') || PUBLIC_PATHS.some(p => path.startsWith(p))) + if (!path.startsWith('/api') || PUBLIC_PATHS.some(p => path.startsWith(p)) || MIGRATE_S2S_PATHS.includes(path)) return let sessionData diff --git a/tests/unit/auth-middleware-public-paths.test.ts b/tests/unit/auth-middleware-public-paths.test.ts index 0c995a96..750494e3 100644 --- a/tests/unit/auth-middleware-public-paths.test.ts +++ b/tests/unit/auth-middleware-public-paths.test.ts @@ -55,6 +55,29 @@ describe('auth middleware public paths', () => { expect(getServerSession).not.toHaveBeenCalled() }) + // Migrate's signed server-to-server calls: no session, the route verifies the signature. + it.each([ + '/api/migrate/account-state', + '/api/migrate/provision', + '/api/migrate/grants/status', + '/api/migrate/grants/install-url', + ])('lets Migrate\'s signed server-to-server call %s through without a session lookup', async (path) => { + await expect(run(path)).resolves.toBeUndefined() + expect(getServerSession).not.toHaveBeenCalled() + }) + + // The allowlist is exact paths: the user-facing Migrate routes keep their session. + it.each([ + '/api/migrate/claim', + '/api/migrate/grants/grant-1', + '/api/migrate/grants/grant-1/checkout', + '/api/migrate/grants/status/extra', + '/api/migrate/account-state/extra', + ])('still 401s the user-facing Migrate route %s without a session', async (path) => { + getServerSession.mockResolvedValue(null) + await expect(run(path)).rejects.toMatchObject({ statusCode: 401 }) + }) + it('still 401s a protected API path when there is no session', async () => { getServerSession.mockResolvedValue(null) await expect(run('/api/workspaces/w1/projects')).rejects.toMatchObject({ statusCode: 401 })