Skip to content

Commit

Permalink
Protect runtime RPC from unauthorized requests
Browse files Browse the repository at this point in the history
  • Loading branch information
apedroferreira committed Dec 27, 2023
1 parent 0565d71 commit 2fd2899
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 26 deletions.
77 changes: 56 additions & 21 deletions packages/toolpad-app/src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -132,18 +133,43 @@ export function createAuthHandler(project: ToolpadProject): Router {
return router;
}

async function getAuthProviders(project: ToolpadProject): Promise<AuthProviderConfig[]> {
const dom = await project.loadDom();
const app = appDom.getApp(dom);

const authProviders = app.attributes.authentication?.providers ?? [];

return authProviders;
}

async function getHasAuthentication(project: ToolpadProject): Promise<boolean> {
const authProviders = await getAuthProviders(project);
return authProviders.length > 0;
}

async function getUserToken(req: express.Request): Promise<JWT | null> {
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`;

Expand All @@ -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;
}
Expand All @@ -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();
}
};
}
7 changes: 4 additions & 3 deletions packages/toolpad-app/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions packages/toolpad-app/src/server/toolpadAppServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 2fd2899

Please sign in to comment.