Skip to content

Commit

Permalink
PRI-46: Implement CSRF
Browse files Browse the repository at this point in the history
Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF

Implement CSRF check
PRI-46: Implement CSRF check

PRI-46: Implement CSRF check
PRI-46: Implement CSRF check

PRI-46: Implement CSRF check
PRI-46: Implement CSRF check

Implement CSRF check
PRI-46: Implementing CSRF Token

Implementing CSRF Token
PRI-46: Implementing CSRF Token

Implementing CSRF Token
PRI-46: Implementing CSRF Token

Implementing CSRF Token
  • Loading branch information
mhanson-github committed Sep 9, 2024
1 parent 6292267 commit 687f97e
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 16 deletions.
5 changes: 3 additions & 2 deletions api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Request, Response } from 'express';
import asyncRouteHandler from './handlers/async-router-handler'
import { fetchWithTimeout } from "./handlers/util";
const yaml = require('js-yaml')
import { doubleCsrfProtection } from './csrf';

const ACCOUNT_INFO_URL = process.env.ACCOUNT_INFO_URL ?? "https://explorer-atomium.shardeum.org/api/account";
// const FAUCET_CLAIM_URL =
Expand Down Expand Up @@ -70,7 +71,7 @@ apiRouter.get('/node/performance', (req, res) => {
])
})

apiRouter.post('/log/stake', (req, res) => {
apiRouter.post('/log/stake', doubleCsrfProtection, (req, res) => {
console.log('Writing Stake TX logs')
fs.appendFile(path.join(__dirname, '../stakeTXs.log'), JSON.stringify(req.body, undefined, 3), err => {
if (err) {
Expand All @@ -84,7 +85,7 @@ apiRouter.post('/log/stake', (req, res) => {
res.status(200).json({ status: 'ok' })
})

apiRouter.post('/log/unstake', (req, res) => {
apiRouter.post('/log/unstake', doubleCsrfProtection, (req, res) => {
console.log('Writing Unstake TX logs')
fs.appendFile(path.join(__dirname, '../unstakeTXs.log'), JSON.stringify(req.body, undefined, 3), err => {
if (err) {
Expand Down
6 changes: 4 additions & 2 deletions api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as crypto from '@shardus/crypto-utils';
import rateLimit from 'express-rate-limit';
const yaml = require('js-yaml')
const jwt = require('jsonwebtoken')
import { doubleCsrfProtection } from './csrf';

function isValidSecret(secret: unknown) {
return typeof secret === 'string' && secret.length >= 32;
Expand All @@ -19,7 +20,7 @@ const jwtSecret = (isValidSecret(process.env.JWT_SECRET))
: generateRandomSecret();
crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347');

export const loginHandler = (req: Request, res: Response) => {
export const loginHandler = [doubleCsrfProtection,(req: Request, res: Response) => {
const password = req.body && req.body.password
const hashedPass = crypto.hash(password);
// Exec the CLI validator login command
Expand Down Expand Up @@ -49,10 +50,11 @@ export const loginHandler = (req: Request, res: Response) => {
res.send({ status: 'ok' })
})
console.log('executing operator-cli gui login...')
}
}]

export const logoutHandler = (req: Request, res: Response) => {
res.clearCookie("accessToken");
res.clearCookie("csrfToken");
res.send({ status: 'ok' })
}

Expand Down
30 changes: 30 additions & 0 deletions api/csrf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { doubleCsrf, type DoubleCsrfCookieOptions } from 'csrf-csrf';
import * as crypto from '@shardus/crypto-utils';
import express, { Request, Response, NextFunction } from 'express'

crypto.init('64f152869ca2d473e4ba64ab53f49ccdb2edae22da192c126850970e788af347');

const csrfSecret = Buffer.from(crypto.randomBytes(32)).toString('hex');
const cookieOptions: DoubleCsrfCookieOptions = {
maxAge: 24 * 60 * 60 * 1000,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/'
};

export const { doubleCsrfProtection, generateToken } = doubleCsrf({
size: 4 * 8,
getSecret: () => csrfSecret,
cookieName: 'csrfToken',
cookieOptions,
});

export const generateTokenHandler = (req: Request, res: Response) => {
const generatedToken = generateToken(req, res);
res.set('Content-Type', 'text/plain');
if (!generatedToken) {
return res.status(500).send('Cannot generate the requested content.');
}

return res.send(generatedToken);
}
15 changes: 8 additions & 7 deletions api/handlers/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { existsSync } from 'fs';
import asyncRouteHandler from './async-router-handler';
import fs from 'fs';
import * as crypto from '@shardus/crypto-utils';
import { doubleCsrfProtection } from '../csrf';

const yaml = require('js-yaml')

Expand All @@ -29,14 +30,14 @@ export const nodeVersionHandler = asyncRouteHandler(async (req: Request, res: Re

export default function configureNodeHandlers(apiRouter: Router) {
let lastActiveNodeState: NodeStatus;
apiRouter.post('/node/start', asyncRouteHandler(async (req: Request, res: Response) => {
apiRouter.post('/node/start', doubleCsrfProtection, asyncRouteHandler(async (req: Request, res: Response) => {
// Exec the CLI validator start command
console.log('executing operator-cli start...');
execFileSync('operator-cli', ['start']);
res.status(200).json({ status: "ok" })
}));

apiRouter.post('/node/stop', asyncRouteHandler(async (req: Request, res: Response) => {
apiRouter.post('/node/stop', doubleCsrfProtection, asyncRouteHandler(async (req: Request, res: Response) => {
// Exec the CLI validator stop command
console.log('executing operator-cli stop...');
execFileSync('operator-cli', ['stop', '-f'])
Expand Down Expand Up @@ -90,8 +91,8 @@ export default function configureNodeHandlers(apiRouter: Router) {
});
}));

apiRouter.delete('/node/logs', asyncRouteHandler(async (req: Request, res: Response<NodeClearLogsResponse>) => {
let logsPath = path.join(__dirname, '../../../cli/build/logs');
apiRouter.delete('/node/logs', doubleCsrfProtection, asyncRouteHandler(async (req: Request, res: Response<NodeClearLogsResponse>) => {
let logsPath = path.join(__dirname, '../../../validator-cli/build/logs');
if (!existsSync(logsPath)) {
res.json({ logsCleared: [] });
return;
Expand Down Expand Up @@ -135,7 +136,7 @@ export default function configureNodeHandlers(apiRouter: Router) {
);

apiRouter.post(
'/node/update',
'/node/update', doubleCsrfProtection,
asyncRouteHandler(async (req: Request, res: Response) => {
const outUpdate = execFileSync('operator-cli', ['update']);
console.log('operator-cli update: ', outUpdate);
Expand All @@ -158,7 +159,7 @@ export default function configureNodeHandlers(apiRouter: Router) {
}));

apiRouter.post(
'/password',
'/password', doubleCsrfProtection,
asyncRouteHandler(async (req: Request<{
currentPassword: string;
newPassword: string;
Expand Down Expand Up @@ -189,7 +190,7 @@ export default function configureNodeHandlers(apiRouter: Router) {
}


apiRouter.post('/settings', asyncRouteHandler(async (req: Request, res: Response) => {
apiRouter.post('/settings', doubleCsrfProtection, asyncRouteHandler(async (req: Request, res: Response) => {
if (!req.body) {
badRequestResponse(res, 'Invalid body');
return;
Expand Down
2 changes: 2 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dotenv from 'dotenv';
import { cacheStaticFiles, preventBrowserCacheForDynamicContent, setSecurityHeaders } from './security-headers';
import { errorMiddleware } from './error-middleware';
import { nodeVersionHandler } from './handlers/node';
import { generateTokenHandler } from './csrf';

dotenv.config()
const port = process.env.PORT ? +process.env.PORT : 8081
Expand Down Expand Up @@ -41,6 +42,7 @@ if (isDev) {
app.use(apiLimiter)
app.use(cookieParser());
setSecurityHeaders(app);
app.get('/csrf-token', generateTokenHandler)
app.post('/auth/login', loginHandler)
app.post('/auth/logout', logoutHandler)
app.use('/api', jwtMiddleware, apiRouter)
Expand Down
27 changes: 23 additions & 4 deletions hooks/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@ import { authService } from '../services/auth.service'
import { useGlobals } from '../utils/globals'

const { apiBase } = useGlobals()
export async function getCsrfToken(): Promise<string> {
const response = await fetch(`/csrf-token`, {
mode: 'cors',
signal: AbortSignal.timeout(2000),
credentials: 'include',
});

if (!response.ok) {
throw new Error('Token was not received.');
}

export const fetcher = <T>(input: RequestInfo | URL,
return await response.text();
}
const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
export const fetcher = async <T>(input: RequestInfo | URL,
init: RequestInit,
showErrorMessage: (msg: string) => void): Promise<T> => {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const isUnsafeMethod = unsafeMethods.includes(init?.method ?? '');
if (isUnsafeMethod) {
const csrfToken = await getCsrfToken();
headers['X-Csrf-Token'] = csrfToken;
}
return fetch(input, {
headers: {
'Content-Type': 'application/json',
},
headers: headers,
credentials: 'include', // Send cookies
...(init ?? {}),
}).then(async (res) => {
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@walletconnect/modal": "^2.6.2",
"chart.js": "4.3.0",
"cookie-parser": "^1.4.6",
"csrf-csrf": "^3.0.6",
"daisyui": "2.52.0",
"dotenv": "16.3.1",
"ethers": "5.7.2",
Expand Down
16 changes: 15 additions & 1 deletion services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@ const isLoggedInKey = 'isLoggedIn'
export const wasLoggedOutKey = 'wasLoggedOut'
export const isFirstTimeUserKey = 'isFirstTimeUser'

export async function getCsrfToken(): Promise<string> {
document.cookie = "csrf-token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;";
const response = await fetch(`/csrf-token`, {
signal: AbortSignal.timeout(2000),
});

if (!response.ok) {
throw new Error('Token was not received.');
}

return await response.text();
}
const login = async (apiBase: string, password: string) => {
const sha256digest = await hashSha256(password);
const csrfToken = await getCsrfToken();
const res = await fetch(`${apiBase}/auth/login`, {
headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' , 'X-Csrf-Token': csrfToken},
method: 'POST',
body: JSON.stringify({ password: sha256digest }),
credentials: 'include',
});
await res.json();
if (!res.ok) {
Expand Down

0 comments on commit 687f97e

Please sign in to comment.