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..b070b264 --- /dev/null +++ b/src/http/csrf/create.js @@ -0,0 +1,10 @@ +const crypto = require('node:crypto') +const fiveMinutes = 300000 + +/** creates a signed token [rando].[timestamp].[sig] */ +module.exports = function create (data, ts) { + data = data || Buffer.from(crypto.randomUUID().replace(/-/g, '')) + 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 new file mode 100644 index 00000000..ea4f6b1d --- /dev/null +++ b/src/http/csrf/verify.js @@ -0,0 +1,10 @@ +let create = require('./create') + +/** ensures payload is valid token that hasn't expired */ +module.exports = function verify (payload) { + const [ data, ts, sig ] = payload.split('.') + if (Date.now() > ts) return false + const gen = create(data, ts) + 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') +}) + +