From ac2a1af957f6f869685266539f8483da39e10907 Mon Sep 17 00:00:00 2001 From: Denys Bitaccess Date: Tue, 30 Nov 2021 16:23:20 +0100 Subject: [PATCH 1/4] fix: tokens management * add tokens lifetime as configurable variable * invalidate current token during authentication (login + pass; refresh) * limit scope of tokens to access and refresh only Signed-off-by: Denys Bitaccess --- class/User.js | 84 ++++++++++++++++++++++++++++++++++++---------- config.js | 4 +++ controllers/api.js | 29 ++++++++++++++-- 3 files changed, 97 insertions(+), 20 deletions(-) diff --git a/class/User.js b/class/User.js index a4f8e867..da93b3f3 100644 --- a/class/User.js +++ b/class/User.js @@ -37,7 +37,7 @@ export class User { return this._password; } getAccessToken() { - return this._acess_token; + return this._access_token; } getRefreshToken() { return this._refresh_token; @@ -48,23 +48,34 @@ export class User { let access_token = authorization.replace('Bearer ', ''); let userid = await this._redis.get('userid_for_' + access_token); - if (userid) { - this._userid = userid; - return true; + if (!userid) { + return false } - return false; + let refresh_token = await this._redis.get('refresh_token_for_' + userid) + if (refresh_token === access_token) { + return false + } + + this._userid = userid; + return true; } async loadByRefreshToken(refresh_token) { let userid = await this._redis.get('userid_for_' + refresh_token); - if (userid) { - this._userid = userid; - await this._generateTokens(); - return true; + + if (!userid) { + return false; } - return false; + let access_token = await this._redis.get('access_token_for_' + userid) + if (access_token === refresh_token) { + return false + } + + this._userid = userid; + await this._generateTokens(); + return true; } async create() { @@ -479,16 +490,55 @@ export class User { } async _generateTokens() { - let buffer = crypto.randomBytes(20); - this._acess_token = buffer.toString('hex'); + await this._invalidateCurrentTokens(); - buffer = crypto.randomBytes(20); + await this._generateAccessToken(); + await this._generateRefreshToken(); + } + + async _invalidateCurrentTokens() { + this._access_token = await this._redis.get('access_token_for_' + this._userid); + this._refresh_token = await this._redis.get('refresh_token_for_' + this._userid); + + await this._redis.del('access_token_for_' + this._userid); + await this._redis.del('refresh_token_for_' + this._userid); + await this._redis.del('userid_for_' + this._access_token); + await this._redis.del('userid_for_' + this._refresh_token); + + this._access_token = null + this._refresh_token = null + } + + async _generateAccessToken() { + const buffer = crypto.randomBytes(20); + this._access_token = buffer.toString('hex'); + + const key_UId_AT = 'userid_for_' + this._access_token; + const key_AT_UId = 'access_token_for_' + this._userid; + + await this._redis.set(key_UId_AT, this._userid); + await this._redis.set(key_AT_UId, this._access_token); + + if (config.auth.accessTokenLifeTime) { + await this._redis.expire(key_UId_AT, config.auth.accessTokenLifeTime); + await this._redis.expire(key_AT_UId, config.auth.accessTokenLifeTime); + } + } + + async _generateRefreshToken() { + const buffer = crypto.randomBytes(20); this._refresh_token = buffer.toString('hex'); - await this._redis.set('userid_for_' + this._acess_token, this._userid); - await this._redis.set('userid_for_' + this._refresh_token, this._userid); - await this._redis.set('access_token_for_' + this._userid, this._acess_token); - await this._redis.set('refresh_token_for_' + this._userid, this._refresh_token); + const key_UId_RT = 'userid_for_' + this._refresh_token; + const key_RT_UId = 'refresh_token_for_' + this._userid; + + await this._redis.set(key_UId_RT, this._userid); + await this._redis.set(key_RT_UId, this._refresh_token); + + if (config.auth.refreshTokenLifeTime) { + await this._redis.expire(key_UId_RT, config.auth.refreshTokenLifeTime); + await this._redis.expire(key_RT_UId, config.auth.refreshTokenLifeTime); + } } async _saveUserToDatabase() { diff --git a/config.js b/config.js index 840a265a..7fa1e6d2 100644 --- a/config.js +++ b/config.js @@ -4,6 +4,10 @@ let config = { rateLimit: 200, forwardReserveFee: 0.01, // default 0.01 intraHubFee: 0.003, // default 0.003 + auth: { + accessTokenLifeTime: 3600, + refreshTokenLifeTime: 86400, + }, bitcoind: { rpc: 'http://login:password@1.1.1.1:8332/wallet/wallet.dat', }, diff --git a/controllers/api.js b/controllers/api.js index 2a582e63..8351695e 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -164,15 +164,38 @@ router.post('/auth', postLimiter, async function (req, res) { if (req.body.refresh_token) { // need to refresh token if (await u.loadByRefreshToken(req.body.refresh_token)) { - res.send({ refresh_token: u.getRefreshToken(), access_token: u.getAccessToken() }); + const body = { + refresh_token: u.getRefreshToken(), + access_token: u.getAccessToken() + } + if (config.auth.accessTokenLifeTime) { + body.access_token_expires_in = config.auth.accessTokenLifeTime + } + if (config.auth.refreshTokenLifeTime) { + body.refresh_token_expires_in = config.auth.refreshTokenLifeTime + } + res.send(body); } else { return errorBadAuth(res); } } else { // need to authorize user let result = await u.loadByLoginAndPassword(req.body.login, req.body.password); - if (result) res.send({ refresh_token: u.getRefreshToken(), access_token: u.getAccessToken() }); - else errorBadAuth(res); + if (result) { + const body = { + refresh_token: u.getRefreshToken(), + access_token: u.getAccessToken() + } + if (config.auth.accessTokenLifeTime) { + body.access_token_expires_in = config.auth.accessTokenLifeTime + } + if (config.auth.refreshTokenLifeTime) { + body.refresh_token_expires_in = config.auth.refreshTokenLifeTime + } + res.send(body); + } else { + errorBadAuth(res); + } } }); From dc0abefce7aa052dde3ac384f18e4678309b27f1 Mon Sep 17 00:00:00 2001 From: Denys Bitaccess Date: Wed, 1 Dec 2021 07:53:48 +0100 Subject: [PATCH 2/4] use stronger crypto Signed-off-by: Denys Bitaccess --- class/User.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/class/User.js b/class/User.js index da93b3f3..9c32d13d 100644 --- a/class/User.js +++ b/class/User.js @@ -79,17 +79,10 @@ export class User { } async create() { - let buffer = crypto.randomBytes(10); - let login = buffer.toString('hex'); + this._login = this._generateDigest(); + this._password = this._generateDigest(); + this._userid = this._generateDigest(); - buffer = crypto.randomBytes(10); - let password = buffer.toString('hex'); - - buffer = crypto.randomBytes(24); - let userid = buffer.toString('hex'); - this._login = login; - this._password = password; - this._userid = userid; await this._saveUserToDatabase(); } @@ -510,8 +503,7 @@ export class User { } async _generateAccessToken() { - const buffer = crypto.randomBytes(20); - this._access_token = buffer.toString('hex'); + this._access_token = this._generateDigest(); const key_UId_AT = 'userid_for_' + this._access_token; const key_AT_UId = 'access_token_for_' + this._userid; @@ -526,8 +518,7 @@ export class User { } async _generateRefreshToken() { - const buffer = crypto.randomBytes(20); - this._refresh_token = buffer.toString('hex'); + this._refresh_token = this._generateDigest(); const key_UId_RT = 'userid_for_' + this._refresh_token; const key_RT_UId = 'refresh_token_for_' + this._userid; @@ -546,6 +537,11 @@ export class User { await this._redis.set((key = 'user_' + this._login + '_' + this._hash(this._password)), this._userid); } + _generateDigest() { + const buffer = crypto.randomBytes(256); + return crypto.createHash('sha1').update(buffer).digest('hex'); + } + /** * Fetches all onchain txs for user's address, and compares them to * already imported txids (stored in database); Ones that are not imported - From d09a0d6c07bcf76936a105fb32178aaca1802fa3 Mon Sep 17 00:00:00 2001 From: Denys Bitaccess Date: Wed, 1 Dec 2021 08:11:42 +0100 Subject: [PATCH 3/4] sensible defaults for auth config Signed-off-by: Denys Bitaccess --- class/User.js | 12 ++++-------- controllers/api.js | 22 ++++++++-------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/class/User.js b/class/User.js index 9c32d13d..ad013a71 100644 --- a/class/User.js +++ b/class/User.js @@ -511,10 +511,8 @@ export class User { await this._redis.set(key_UId_AT, this._userid); await this._redis.set(key_AT_UId, this._access_token); - if (config.auth.accessTokenLifeTime) { - await this._redis.expire(key_UId_AT, config.auth.accessTokenLifeTime); - await this._redis.expire(key_AT_UId, config.auth.accessTokenLifeTime); - } + await this._redis.expire(key_UId_AT, accessTokenLifeTime); + await this._redis.expire(key_AT_UId, accessTokenLifeTime); } async _generateRefreshToken() { @@ -526,10 +524,8 @@ export class User { await this._redis.set(key_UId_RT, this._userid); await this._redis.set(key_RT_UId, this._refresh_token); - if (config.auth.refreshTokenLifeTime) { - await this._redis.expire(key_UId_RT, config.auth.refreshTokenLifeTime); - await this._redis.expire(key_RT_UId, config.auth.refreshTokenLifeTime); - } + await this._redis.expire(key_UId_RT, refreshTokenLifeTime); + await this._redis.expire(key_RT_UId, refreshTokenLifeTime); } async _saveUserToDatabase() { diff --git a/controllers/api.js b/controllers/api.js index 8351695e..d239b044 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -21,6 +21,8 @@ redis.monitor(function (err, monitor) { /** GLOBALS */ global.forwardFee = config.forwardReserveFee || 0.01; global.internalFee = config.intraHubFee || 0.003; +global.accessTokenLifeTime = config.auth?.accessTokenLifeTime || 3600; +global.refreshTokenLifeTime = config.auth?.refreshTokenLifeTime || 86400; /****** END SET FEES FROM CONFIG AT STARTUP ******/ let bitcoinclient = require('../bitcoin'); @@ -165,14 +167,10 @@ router.post('/auth', postLimiter, async function (req, res) { // need to refresh token if (await u.loadByRefreshToken(req.body.refresh_token)) { const body = { + token_type: 'bearer', refresh_token: u.getRefreshToken(), - access_token: u.getAccessToken() - } - if (config.auth.accessTokenLifeTime) { - body.access_token_expires_in = config.auth.accessTokenLifeTime - } - if (config.auth.refreshTokenLifeTime) { - body.refresh_token_expires_in = config.auth.refreshTokenLifeTime + access_token: u.getAccessToken(), + expires_in: accessTokenLifeTime, } res.send(body); } else { @@ -183,14 +181,10 @@ router.post('/auth', postLimiter, async function (req, res) { let result = await u.loadByLoginAndPassword(req.body.login, req.body.password); if (result) { const body = { + token_type: 'bearer', refresh_token: u.getRefreshToken(), - access_token: u.getAccessToken() - } - if (config.auth.accessTokenLifeTime) { - body.access_token_expires_in = config.auth.accessTokenLifeTime - } - if (config.auth.refreshTokenLifeTime) { - body.refresh_token_expires_in = config.auth.refreshTokenLifeTime + access_token: u.getAccessToken(), + expires_in: accessTokenLifeTime } res.send(body); } else { From 3ca5c2afc850e1e6f4878d9823808e3a25c5ff0a Mon Sep 17 00:00:00 2001 From: Denys Bitaccess Date: Wed, 1 Dec 2021 08:19:55 +0100 Subject: [PATCH 4/4] refactor auth endpoint Signed-off-by: Denys Bitaccess --- controllers/api.js | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/controllers/api.js b/controllers/api.js index d239b044..2ff447a1 100644 --- a/controllers/api.js +++ b/controllers/api.js @@ -159,38 +159,28 @@ router.post('/create', postLimiter, async function (req, res) { router.post('/auth', postLimiter, async function (req, res) { logger.log('/auth', [req.id]); - if (!((req.body.login && req.body.password) || req.body.refresh_token)) return errorBadArguments(res); - let u = new User(redis, bitcoinclient, lightning); + const u = new User(redis, bitcoinclient, lightning); - if (req.body.refresh_token) { - // need to refresh token - if (await u.loadByRefreshToken(req.body.refresh_token)) { - const body = { - token_type: 'bearer', - refresh_token: u.getRefreshToken(), - access_token: u.getAccessToken(), - expires_in: accessTokenLifeTime, - } - res.send(body); - } else { - return errorBadAuth(res); - } + let authenticated = false + if (req.body.login && req.body.password) { + authenticated = await u.loadByLoginAndPassword(req.body.login, req.body.password); + } else if (req.body.refresh_token) { + authenticated = await u.loadByRefreshToken(req.body.refresh_token) } else { - // need to authorize user - let result = await u.loadByLoginAndPassword(req.body.login, req.body.password); - if (result) { - const body = { - token_type: 'bearer', - refresh_token: u.getRefreshToken(), - access_token: u.getAccessToken(), - expires_in: accessTokenLifeTime - } - res.send(body); - } else { - errorBadAuth(res); - } + return errorBadArguments(res); + } + + if (!authenticated) { + return errorBadAuth(res); } + + return res.send({ + token_type: 'bearer', + refresh_token: u.getRefreshToken(), + access_token: u.getAccessToken(), + expires_in: accessTokenLifeTime, + }); }); router.post('/addinvoice', postLimiter, async function (req, res) {