From 2fd2899ee8705209a7b4e2cda08e85a98fc639c5 Mon Sep 17 00:00:00 2001 From: Pedro Ferreira <10789765+apedroferreira@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:51:12 +0000 Subject: [PATCH] Protect runtime RPC from unauthorized requests --- packages/toolpad-app/src/server/auth.ts | 77 ++++++++++++++----- packages/toolpad-app/src/server/index.ts | 7 +- .../src/server/toolpadAppServer.ts | 5 +- 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/toolpad-app/src/server/auth.ts b/packages/toolpad-app/src/server/auth.ts index b65121af49e..1873475756c 100644 --- a/packages/toolpad-app/src/server/auth.ts +++ b/packages/toolpad-app/src/server/auth.ts @@ -2,11 +2,12 @@ import express, { Router } from 'express'; import { Auth } from '@auth/core'; import GithubProvider from '@auth/core/providers/github'; import GoogleProvider from '@auth/core/providers/google'; -import { getToken } from '@auth/core/jwt'; +import { JWT, getToken } from '@auth/core/jwt'; import { asyncHandler } from '../utils/express'; import * as appDom from '../appDom'; import { ToolpadProject } from './localMode'; import { adaptRequestFromExpressToFetch } from './httpApiAdapters'; +import { AuthProviderConfig } from '../types'; async function getProfileRoles(email: string, project: ToolpadProject) { const dom = await project.loadDom(); @@ -132,18 +133,43 @@ export function createAuthHandler(project: ToolpadProject): Router { return router; } +async function getAuthProviders(project: ToolpadProject): Promise { + const dom = await project.loadDom(); + const app = appDom.getApp(dom); + + const authProviders = app.attributes.authentication?.providers ?? []; + + return authProviders; +} + +async function getHasAuthentication(project: ToolpadProject): Promise { + const authProviders = await getAuthProviders(project); + return authProviders.length > 0; +} + +async function getUserToken(req: express.Request): Promise { + let token = null; + if (process.env.TOOLPAD_AUTH_SECRET) { + const request = adaptRequestFromExpressToFetch(req); + + // @TODO: Library types are wrong as salt should not be required, remove once fixed + // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 + // @ts-ignore + token = await getToken({ + req: request, + secret: process.env.TOOLPAD_AUTH_SECRET, + }); + } + + return token; +} + export async function createAuthPagesMiddleware(project: ToolpadProject) { return async (req: express.Request, res: express.Response, next: express.NextFunction) => { const { options } = project; const { base } = options; - const dom = await project.loadDom(); - - const app = appDom.getApp(dom); - - const authProviders = app.attributes.authentication?.providers ?? []; - - const hasAuthentication = authProviders.length > 0; + const hasAuthentication = await getHasAuthentication(project); const signInPath = `${base}/signin`; @@ -154,19 +180,7 @@ export async function createAuthPagesMiddleware(project: ToolpadProject) { req.originalUrl !== signInPath && !req.originalUrl.startsWith(`${base}/api/auth`) ) { - const request = adaptRequestFromExpressToFetch(req); - - let token; - if (process.env.TOOLPAD_AUTH_SECRET) { - // @TODO: Library types are wrong as salt should not be required, remove once fixed - // Github discussion: https://github.com/nextauthjs/next-auth/discussions/9133 - // @ts-ignore - token = await getToken({ - req: request, - secret: process.env.TOOLPAD_AUTH_SECRET, - }); - } - + const token = await getUserToken(req); if (!token) { isRedirect = true; } @@ -180,3 +194,24 @@ export async function createAuthPagesMiddleware(project: ToolpadProject) { } }; } + +export async function createAuthRpcMiddleware(project: ToolpadProject) { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + const hasAuthentication = await getHasAuthentication(project); + + let errorMessage = ''; + if (hasAuthentication) { + const token = await getUserToken(req); + if (!token) { + errorMessage = 'Unauthorized. Must be authenticated.'; + } + } + + if (errorMessage) { + res.status(401).send(errorMessage); + res.end(); + } else { + next(); + } + }; +} diff --git a/packages/toolpad-app/src/server/index.ts b/packages/toolpad-app/src/server/index.ts index ee1986cb191..6e31b629d7f 100644 --- a/packages/toolpad-app/src/server/index.ts +++ b/packages/toolpad-app/src/server/index.ts @@ -28,7 +28,7 @@ import { createRpcHandler } from './rpc'; import { APP_URL_WINDOW_PROPERTY } from '../constants'; import { createRpcServer as createProjectRpcServer } from './projectRpcServer'; import { createRpcServer as createRuntimeRpcServer } from './runtimeRpcServer'; -import { createAuthHandler, createAuthPagesMiddleware } from './auth'; +import { createAuthRpcMiddleware, createAuthHandler, createAuthPagesMiddleware } from './auth'; import.meta.url ??= url.pathToFileURL(__filename).toString(); const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); @@ -120,9 +120,10 @@ async function createDevHandler(project: ToolpadProject) { ); handler.use('/api/data', project.dataManager.createDataHandler()); - const runtimeRpcServer = createRuntimeRpcServer(project); - handler.use('/api/runtime-rpc', createRpcHandler(runtimeRpcServer)); + const authRpcMiddleware = await createAuthRpcMiddleware(project); + const runtimeRpcServer = createRuntimeRpcServer(project); + handler.use('/api/runtime-rpc', authRpcMiddleware, createRpcHandler(runtimeRpcServer)); if (process.env.TOOLPAD_AUTH_SECRET) { const authHandler = createAuthHandler(project); diff --git a/packages/toolpad-app/src/server/toolpadAppServer.ts b/packages/toolpad-app/src/server/toolpadAppServer.ts index 6fcb7eefb3b..a806af0772c 100644 --- a/packages/toolpad-app/src/server/toolpadAppServer.ts +++ b/packages/toolpad-app/src/server/toolpadAppServer.ts @@ -12,7 +12,7 @@ import { RUNTIME_CONFIG_WINDOW_PROPERTY, INITIAL_STATE_WINDOW_PROPERTY } from '. import createRuntimeState from '../runtime/createRuntimeState'; import type { RuntimeConfig } from '../types'; import type { RuntimeState } from '../runtime'; -import { createAuthHandler } from './auth'; +import { createAuthRpcMiddleware, createAuthHandler } from './auth'; export interface PostProcessHtmlParams { config: RuntimeConfig; @@ -65,8 +65,9 @@ export async function createProdHandler(project: ToolpadProject) { handler.use('/api/data', project.dataManager.createDataHandler()); + const authRpcMiddleware = await createAuthRpcMiddleware(project); const runtimeRpcServer = createRpcServer(project); - handler.use('/api/runtime-rpc', createRpcHandler(runtimeRpcServer)); + handler.use('/api/runtime-rpc', authRpcMiddleware, createRpcHandler(runtimeRpcServer)); if (process.env.TOOLPAD_AUTH_SECRET) { const authHandler = createAuthHandler(project);