From 0bf3778d23abc45955f9a452543383ee2d42902a Mon Sep 17 00:00:00 2001 From: AbegaM Date: Mon, 1 Apr 2024 14:10:29 +0300 Subject: [PATCH] Add a feature to handle revoked refresh tokens --- src/constants/tables.js | 7 +++++++ src/controllers/auth/tables.js | 12 ++++++++++++ src/controllers/auth/token.js | 30 ++++++++++++++++++++++++++---- src/db/schema.js | 18 ++++++++++++++++++ src/services/authService.js | 23 +++++++++++++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/constants/tables.js b/src/constants/tables.js index d2c1772..26a06b6 100644 --- a/src/constants/tables.js +++ b/src/constants/tables.js @@ -2,12 +2,15 @@ const USERS_TABLE = '_users'; const ROLES_TABLE = '_roles'; const USERS_ROLES_TABLE = '_users_roles'; const ROLES_PERMISSIONS_TABLE = '_roles_permissions'; +const REVOKED_REFRESH_TOKENS_TABLE = '_revoked_refresh_tokens'; module.exports = { + // db table names USERS_TABLE, ROLES_TABLE, USERS_ROLES_TABLE, ROLES_PERMISSIONS_TABLE, + REVOKED_REFRESH_TOKENS_TABLE, reservedTableNames: [ USERS_TABLE, @@ -43,5 +46,9 @@ module.exports = { // _users_roles fields USER_ID: 'user_id', + + //_revoked_refresh_tokens + REFRESH_TOKEN: 'refresh_token', + EXPIRES_AT: 'expires_at', }, }; diff --git a/src/controllers/auth/tables.js b/src/controllers/auth/tables.js index 3de0019..550cd46 100644 --- a/src/controllers/auth/tables.js +++ b/src/controllers/auth/tables.js @@ -7,6 +7,7 @@ const { ROLES_TABLE, USERS_ROLES_TABLE, ROLES_PERMISSIONS_TABLE, + REVOKED_REFRESH_TOKENS_TABLE, constraints, tableFields, } = dbConstants; @@ -21,6 +22,9 @@ const createDefaultTables = async () => { ROLES_PERMISSIONS_TABLE, ); const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE); + const revokedRefreshTokensTable = tableService.checkTableExists( + REVOKED_REFRESH_TOKENS_TABLE, + ); // create _users table if (!usersTable) { @@ -89,6 +93,14 @@ const createDefaultTables = async () => { fields: permissions, }); } + + // create _revoked_refresh_tokens table + if (!revokedRefreshTokensTable) { + tableService.createTable( + REVOKED_REFRESH_TOKENS_TABLE, + schema.revokedRefreshTokensSchema, + ); + } }; module.exports = { diff --git a/src/controllers/auth/token.js b/src/controllers/auth/token.js index 2272af9..155e501 100644 --- a/src/controllers/auth/token.js +++ b/src/controllers/auth/token.js @@ -123,13 +123,18 @@ const refreshAccessToken = async (req, res) => { #swagger.summary = 'Refresh Access Token' #swagger.description = 'Endpoint to refresh access and refresh tokens' */ + const refToken = req.cookies.refreshToken; try { + // check if the refresh token is revoked + if (isRefreshTokenRevoked({ refreshToken: refToken })) { + return res + .status(403) + .send({ message: errorMessage.INVALID_REFRESH_TOKEN_ERROR }); + } + // extract the payload from the token and verify it - const payload = await decodeToken( - req.cookies.refreshToken, - config.tokenSecret, - ); + const payload = await decodeToken(refToken, config.tokenSecret); // find the user const users = authService.getUsersById({ userId: payload.userId }); @@ -224,7 +229,19 @@ const removeTokens = async (req, res) => { #swagger.description = 'Endpoint to remove access and refresh tokens' */ + const refreshToken = req.cookies.refreshToken; + try { + // decode the token + const payload = await decodeToken(refreshToken, config.tokenSecret); + + // store the refresh token in the _revoked_refresh_tokens table + authService.saveRevokedRefreshToken({ + refreshToken, + expiresAt: payload.exp, + }); + + // remove the token from the cookie res.clearCookie(authConstants.ACCESS_TOKEN_SUBJECT); res.clearCookie(authConstants.REFRESH_TOKEN_SUBJECT); @@ -261,6 +278,11 @@ const getUsersRoleAndPermission = ({ userId, res }) => { return { userRoles, roleIds, permissions }; }; +const isRefreshTokenRevoked = ({ refreshToken }) => { + const tokens = authService.getRevokedRefreshToken({ refreshToken }); + return tokens.length > 0; +}; + module.exports = { obtainAccessToken, refreshAccessToken, diff --git a/src/db/schema.js b/src/db/schema.js index 2b51f88..b26b394 100644 --- a/src/db/schema.js +++ b/src/db/schema.js @@ -115,4 +115,22 @@ module.exports = { foreignKey: { table: ROLES_TABLE, column: tableFields.ID }, }, ], + + revokedRefreshTokensSchema: [ + { + name: tableFields.REFRESH_TOKEN, + type: 'TEXT', + primaryKey: false, + notNull: true, + unique: false, + }, + + { + name: tableFields.EXPIRES_AT, + type: 'NUMERIC', + primaryKey: false, + notNull: true, + unique: false, + }, + ], }; diff --git a/src/services/authService.js b/src/services/authService.js index 7337215..f0bcac0 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -8,6 +8,7 @@ const { ROLES_TABLE, USERS_ROLES_TABLE, ROLES_PERMISSIONS_TABLE, + REVOKED_REFRESH_TOKENS_TABLE, tableFields, } = dbConstants; @@ -75,5 +76,27 @@ module.exports = () => { return defaultRole; }, + + saveRevokedRefreshToken({ refreshToken, expiresAt }) { + const { lastInsertRowid } = rowService.save({ + tableName: REVOKED_REFRESH_TOKENS_TABLE, + fields: { + refresh_token: refreshToken, + expires_at: expiresAt, + }, + }); + + return { id: lastInsertRowid }; + }, + + getRevokedRefreshToken({ refreshToken }) { + const token = rowService.get({ + tableName: REVOKED_REFRESH_TOKENS_TABLE, + whereString: `WHERE ${tableFields.REFRESH_TOKEN}=?`, + whereStringValues: [refreshToken], + }); + + return token; + }, }; };