diff --git a/package-lock.json b/package-lock.json index d863d4643a20fa314c26d1652e8751b34bd74ce0..2adb6e0d1d7bdb5c3676ca357aad9e595bea52ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -366,6 +366,15 @@ "minimist": "^1.2.0" } }, + "@commander-lol/vault-client": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@commander-lol/vault-client/-/vault-client-0.1.1.tgz", + "integrity": "sha512-fZDlKNIX2vF/JLOqV5zUI7QsOSnmjX8huEIt65yWDk3hMTk6Kyd2lDdcvwdjqOlwmNTRRv9OijDfOayQ0ObDEw==", + "requires": { + "date-fns": "^2.17.0", + "node-fetch": "^2.6.1" + } + }, "@google-cloud/common": { "version": "3.5.0", "resolved": "https://npm.lcr.gr/@google-cloud%2fcommon/-/common-3.5.0.tgz", @@ -2603,6 +2612,11 @@ "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.1.tgz", "integrity": "sha512-M4RggEH5OF2ZuCOxgOU67R6Z9ohjKbxGvAQz48vj53wLmL0bAgumkBvycR32f30pK+Og9pIR+RFDyChbaE4oLA==" }, + "date-fns": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.17.0.tgz", + "integrity": "sha512-ZEhqxUtEZeGgg9eHNSOAJ8O9xqSgiJdrL0lzSSfMF54x6KXWJiOH/xntSJ9YomJPrYH/p08t6gWjGWq1SDJlSA==" + }, "deasync": { "version": "0.1.21", "resolved": "https://npm.lcr.gr/deasync/-/deasync-0.1.21.tgz", diff --git a/package.json b/package.json index 7dd8c9be4650d8d89b58b88411883b172061e868..13d1e659bf5e0630f870966c5bc88af921f894e6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "Louis Capitanchik <louis@microhacks.co.uk>", "license": "GPL-3.0+", "dependencies": { + "@commander-lol/vault-client": "^0.1.1", "@google-cloud/storage": "^5.5.0", "@koa/cors": "^3.1.0", "@koa/multer": "^3.0.0", diff --git a/src/config/vault.js b/src/config/vault.js index 6a1853fd7cb3f8ddfe8e6a6f748eb8d5f8e62567..9454c3ac646fa6e4910860aebbdb15926364d39d 100644 --- a/src/config/vault.js +++ b/src/config/vault.js @@ -3,7 +3,7 @@ const { env } = require('bootstrap') module.exports = { base_url: env('VAULT_BASE_URL'), totp_path: env('VAULT_TOTP_PATH', '/v1/totp'), - kv_path: env('VAULT_KV_PATH', '/v1/kv/data'), + kv_path: env('VAULT_KV_PATH', '/v1/kv'), auth_path: env('VAULT_AUTH_PATH'), credentials: { role_id: env('VAULT_ROLE_ID'), diff --git a/src/services/totp/vault.js b/src/services/totp/vault.js index 2651d792967de96d872e97ba588e9d96191c5f03..eb2fac397f3d1391df100dd2f0b9ad8ac6869fcd 100644 --- a/src/services/totp/vault.js +++ b/src/services/totp/vault.js @@ -1,72 +1,31 @@ const TotpProvider = require('./interface') const { config } = require('bootstrap') -const fetch = require('node-fetch') const { URL } = require('url') -const pathUtil = require('path') -const moment = require('moment') const threadContext = require('core/injection/ThreadContext') +const { VaultClient, VaultSimpleAuth, VaultKVStore, VaultTOTPStore } = require('@commander-lol/vault-client') class VaultTotpProvider extends TotpProvider { constructor() { super() this.config = config('totp') - this.baseUrl = config('vault.base_url') - this.authPath = config('vault.auth_path') - this.totpPath = config('vault.totp_path') - this.kvPath = config('vault.kv_path') - this.credentials = config('vault.credentials') - - this.token = null - } - - createUrl(...path) { - const fullPath = pathUtil.join(...path) - return (new URL(fullPath, this.baseUrl)).toString() - } - - async refreshToken() { - return threadContext.profile('totp.refreshToken', undefined, async () => { - const path = this.createUrl(this.authPath) - const result = await fetch(path, { - method: 'post', - body: JSON.stringify(this.credentials), - headers: { - 'Content-Type': 'application/json', + this.client = new VaultClient(config('vault.base_url'), { + auth: VaultSimpleAuth, + stores: { + kv: VaultKVStore, + totp: VaultTOTPStore, + }, + options: { + auth: { + path: config('vault.auth_path'), + credentials: config('vault.credentials') }, - }) - - if (result.ok) { - const payload = await result.json() - const token = payload?.auth?.client_token - - if (token == null) { - this.token = null - } else { - const expires = moment.utc().add(payload?.auth?.lease_duration ?? 5 * 60, 'seconds') - this.token = { - token, - expires, - } - } - } else { - this.token = null - } - }) - } - - async preflight() { - return threadContext.profile('totp.preflight', undefined, async () => { - if (this.token == null) { - await this.refreshToken() - } else if (this.token.expires.clone().subtract(30, 'seconds').isSameOrBefore(moment.utc())) { // Less than 30 seconds from expiry - await this.refreshToken() - } - - if (this.token == null) { - throw new TypeError('Unable to get vault lease') - } - - return this.token + kv: { + path: config('vault.kv_path'), + }, + totp: { + path: config('vault.totp_path'), + }, + }, }) } @@ -78,28 +37,8 @@ class VaultTotpProvider extends TotpProvider { const id = user.id const email = user.email - const payload = { - account_name: email, - issuer: 'Jetsam', - generate: true, - } - const path = this.createUrl(this.totpPath, 'keys', id) - const { token } = await this.preflight() - - const response = await fetch(path, { - method: 'post', - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json', - 'X-Vault-Token': token, - }, - }) - - if (response.ok) { - const { data } = await response.json() - const barcode = data?.barcode - const url = data?.url - + try { + const { barcode, url } = await this.client.stores.totp.createProvider(id, 'Jetsam', email) const parsed = new URL(url) const secret = parsed.searchParams.get('secret') @@ -108,9 +47,10 @@ class VaultTotpProvider extends TotpProvider { url, secret, } + } catch(e) { + console.error(e) + return null } - - return null }) } @@ -122,71 +62,42 @@ class VaultTotpProvider extends TotpProvider { throw new TypeError('Must provide a TOTP code to verify') } - const path = this.createUrl(this.totpPath, 'code', id) - const payload = { code } - const { token } = await this.preflight() - - const response = await fetch(path, { - method: 'post', - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json', - 'X-Vault-Token': token, - }, - }) - - if (response.ok) { - const { data } = await response.json() - return data?.valid ?? false + try { + const { valid } = await this.client.stores.totp.verify(id, code) + return valid + } catch (e) { + return false } - - return false - } - - async _saveRecoveryCodes(id, codes) { - const path = this.createUrl(this.kvPath, 'totp_recovery', id) - const payload = { data: { codes } } - const { token } = await this.preflight() - - return await fetch(path, { - method: 'post', - body: JSON.stringify(payload), - headers: { - 'Content-Type': 'application/json', - 'X-Vault-Token': token, - }, - }) } async createRecoveryCodes(userid) { - await this.preflight() const crypto = require('core/utils/crypto') let codes = [null, null, null, null, null] codes = await Promise.all(codes.map(c => crypto.secureHexString(16))) const hashes = await Promise.all(codes.map(c => crypto.hash(c))) - const response = await this._saveRecoveryCodes(userid, hashes) - if (response.ok) { - return { codes } + try { + await this.client.stores.kv.write(`totp_recovery/${ userid }`, { codes: hashes }) + } catch (e) { + console.log(e) + return null } - return null + return { codes } } async verifyRecoveryCode(userid, code) { const crypto = require('core/utils/crypto') - const { token } = await this.preflight() - const path = this.createUrl(this.kvPath, 'totp_recovery', userid) + let data = null - const response = await fetch(path, { - headers: { - 'X-Vault-Token': token, - }, - }) + try { + ;({ data } = await this.client.stores.kv.read(`totp_recovery/${ userid }`)) + } catch (e) { + console.log(e) + } - if (response.ok) { - const { data } = await response.json() - const { codes } = data.data + const codes = data?.codes + if (codes) { let found = null search: for (const hash of codes) { if (await crypto.verify(hash, code)) { @@ -197,7 +108,7 @@ class VaultTotpProvider extends TotpProvider { if (found != null) { const newCodes = codes.filter(c => c !== found) - await this._saveRecoveryCodes(userid, newCodes) + await this.client.stores.kv.write(`totp_recovery/${ userid }`, { codes: newCodes }) return true } }