From c7c47121fc7bce6cd36eafbb16ca8ccce9f198b5 Mon Sep 17 00:00:00 2001 From: brianleroux Date: Wed, 13 Mar 2024 11:31:18 -0700 Subject: [PATCH 1/2] first pass at adding basic csrf tokens --- package.json | 1 + readme.md | 1 + src/http/csrf/create.js | 9 +++++ src/http/csrf/verify.js | 12 +++++++ src/http/index.js | 5 +++ .../src/http/csrf/create-and-verify-test.js | 34 +++++++++++++++++++ 6 files changed, 62 insertions(+) create mode 100644 src/http/csrf/create.js create mode 100644 src/http/csrf/verify.js create mode 100644 test/unit/src/http/csrf/create-and-verify-test.js diff --git a/package.json b/package.json index 4c0d0363..31a953c2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "main": "src/index", "types": "types/index.d.ts", "scripts": { + "test:one": "cross-env tape 'test/unit/src/http/csrf/*-test.js' | tap-arc", "lint": "eslint --fix .", "test:unit": "cross-env tape 'test/unit/**/*-test.js' | tap-arc", "test:integration": "cross-env tape 'test/integration/**/*-test.js' | tap-arc", diff --git a/readme.md b/readme.md index 59d43768..b8b043c5 100644 --- a/readme.md +++ b/readme.md @@ -46,6 +46,7 @@ let { - [`http()`](https://arc.codes/docs/en/reference/runtime-helpers/node.js#arc.http) - [`http` middleware](https://arc.codes/docs/en/reference/runtime-helpers/node.js#middleware) - [`http.session`](https://arc.codes/docs/en/reference/runtime-helpers/node.js#arc.http.session) +- [`http.csrf`](https://arc.codes/docs/en/reference/runtime-helpers/node.js#arc.http.csrf) **[`@queues` methods](https://arc.codes/docs/en/reference/runtime-helpers/node.js#arc.queues)** - [`queues.subscribe()`](https://arc.codes/docs/en/reference/runtime-helpers/node.js#arc.queues.subscribe()) diff --git a/src/http/csrf/create.js b/src/http/csrf/create.js new file mode 100644 index 00000000..5d7a06eb --- /dev/null +++ b/src/http/csrf/create.js @@ -0,0 +1,9 @@ +let crypto = require('node:crypto') + +/** creates a signed token [rando].[timestamp].[sig] */ +module.exports = function create (data) { + data = data || Buffer.from(crypto.randomUUID().replace(/-/g, '')) + const secret = 'changeme' || process.env.ARC_APP_SECRET + const ts = Date.now() + return `${data}.${ts}.${crypto.createHmac('sha256', secret).update(data).digest('hex').toString()}` +} diff --git a/src/http/csrf/verify.js b/src/http/csrf/verify.js new file mode 100644 index 00000000..538a0255 --- /dev/null +++ b/src/http/csrf/verify.js @@ -0,0 +1,12 @@ +let create = require('./create') + +/** ensures payload is valid token that hasn't expired */ +module.exports = function verify (payload) { + const [ data, ts, sig ] = payload.split('.') + const elapsed = Date.now() - ts + const fiveMinutes = 300000 + if (elapsed > fiveMinutes) return false + const gen = create(data) + const sig2 = gen.split('.').pop() + return sig2 === sig +} diff --git a/src/http/index.js b/src/http/index.js index 8f94c119..7a6480a1 100644 --- a/src/http/index.js +++ b/src/http/index.js @@ -4,6 +4,8 @@ let bodyParser = require('./helpers/body-parser') let interpolate = require('./helpers/params') let url = require('./helpers/url') let responseFormatter = require('./_res-fmt') +let create = require('./csrf/create') +let verify = require('./csrf/verify') // Unified async / callback HTTP handler function httpHandler (isAsync, ...fns) { @@ -95,6 +97,9 @@ http.helpers = { bodyParser, interpolate, url } // Session http.session = { read, write } +// CSRF +http.csrf = { create, verify } + module.exports = http /** diff --git a/test/unit/src/http/csrf/create-and-verify-test.js b/test/unit/src/http/csrf/create-and-verify-test.js new file mode 100644 index 00000000..72690c17 --- /dev/null +++ b/test/unit/src/http/csrf/create-and-verify-test.js @@ -0,0 +1,34 @@ +let http = require('../../../../../src/http') +let test = require('tape') + +test('exists', t => { + t.plan(2) + t.ok(http.csrf.create, 'create') + t.ok(http.csrf.verify, 'verify') +}) + +test('create a value', t => { + t.plan(1) + let val = http.csrf.create() + t.ok(val, 'created value') +}) + +test('verify a value', t => { + t.plan(1) + let val = http.csrf.create() + t.ok(http.csrf.verify(val), 'value verified') +}) + +test('tampered token is falsy', t => { + t.plan(1) + let tamperedToken = "3d879d515ab241429c97dfea6d1e1927.1584118407000.b0b34563d569030cbe9a4ea63312f23729813b838478420e3811c0bfeaf3add1" + t.ok(http.csrf.verify(tamperedToken) === false, 'value falsy') +}) + +test('token expired is falsy', t => { + t.plan(1) + let expiredToken = "3d879d515ab241419c97dfea6d1e1927.1584118407000.b0b34563d569030cbe9a4ea63312f23729813b838478420e3811c0bfeaf3add1" + t.ok(http.csrf.verify(expiredToken) === false, 'value falsy') +}) + + From d6c7cb3c65853e42924209351ff5dded525a5644 Mon Sep 17 00:00:00 2001 From: brianleroux Date: Fri, 15 Mar 2024 11:05:23 -0700 Subject: [PATCH 2/2] =?UTF-8?q?ammended=20w=20Simon=20feedback=20?= =?UTF-8?q?=F0=9F=99=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/http/csrf/create.js | 9 +++++---- src/http/csrf/verify.js | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/http/csrf/create.js b/src/http/csrf/create.js index 5d7a06eb..b070b264 100644 --- a/src/http/csrf/create.js +++ b/src/http/csrf/create.js @@ -1,9 +1,10 @@ -let crypto = require('node:crypto') +const crypto = require('node:crypto') +const fiveMinutes = 300000 /** creates a signed token [rando].[timestamp].[sig] */ -module.exports = function create (data) { +module.exports = function create (data, ts) { data = data || Buffer.from(crypto.randomUUID().replace(/-/g, '')) - const secret = 'changeme' || process.env.ARC_APP_SECRET - const ts = Date.now() + ts = ts || Date.now() + fiveMinutes + const secret = process.env.ARC_APP_SECRET || process.env.ARC_APP_NAME || 'fallback' return `${data}.${ts}.${crypto.createHmac('sha256', secret).update(data).digest('hex').toString()}` } diff --git a/src/http/csrf/verify.js b/src/http/csrf/verify.js index 538a0255..ea4f6b1d 100644 --- a/src/http/csrf/verify.js +++ b/src/http/csrf/verify.js @@ -3,10 +3,8 @@ let create = require('./create') /** ensures payload is valid token that hasn't expired */ module.exports = function verify (payload) { const [ data, ts, sig ] = payload.split('.') - const elapsed = Date.now() - ts - const fiveMinutes = 300000 - if (elapsed > fiveMinutes) return false - const gen = create(data) + if (Date.now() > ts) return false + const gen = create(data, ts) const sig2 = gen.split('.').pop() return sig2 === sig }