4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "soul-cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "soul-cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soul-cli",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.3",
|
||||
"description": "A SQLite REST and Realtime server",
|
||||
"main": "src/server.js",
|
||||
"bin": {
|
||||
|
||||
@@ -2,4 +2,5 @@ module.exports = {
|
||||
SALT_ROUNDS: 10,
|
||||
ACCESS_TOKEN_SUBJECT: 'accessToken',
|
||||
REFRESH_TOKEN_SUBJECT: 'refreshToken',
|
||||
REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE: 3 * 24 * 60 * 60 * 1000, // 3 days * 24 hours * 60 minutes * 60 seconds * 1000 milliseconds =
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
PASSWORD_UPDATE_SUCCESS: 'Password updated successfully',
|
||||
USER_UPDATE_SUCCESS: 'User updated successfully',
|
||||
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
|
||||
LOGOUT_MESSAGE: 'Logout successful',
|
||||
},
|
||||
|
||||
errorMessage: {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -123,11 +123,19 @@ const refreshAccessToken = async (req, res) => {
|
||||
#swagger.summary = 'Refresh Access Token'
|
||||
#swagger.description = 'Endpoint to refresh access and refresh tokens'
|
||||
*/
|
||||
const refreshTokenFromCookies = req.cookies.refreshToken;
|
||||
|
||||
try {
|
||||
// check if the refresh token is revoked
|
||||
if (isRefreshTokenRevoked({ refreshToken: refreshTokenFromCookies })) {
|
||||
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,
|
||||
refreshTokenFromCookies,
|
||||
config.tokenSecret,
|
||||
);
|
||||
|
||||
@@ -217,6 +225,52 @@ const refreshAccessToken = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const removeTokens = async (req, res) => {
|
||||
/*
|
||||
#swagger.tags = ['Auth']
|
||||
#swagger.summary = 'Remove Tokens'
|
||||
#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);
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.send({ message: responseMessages.successMessage.LOGOUT_MESSAGE });
|
||||
|
||||
/*
|
||||
#swagger.responses[200] = {
|
||||
description: 'Tokens Removed',
|
||||
schema: {
|
||||
$ref: '#/definitions/RemoveTokensResponse'
|
||||
}
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
res.status(500).send({ message: errorMessage.SERVER_ERROR });
|
||||
}
|
||||
};
|
||||
|
||||
const removeRevokedRefreshTokens = () => {
|
||||
authService.deleteRevokedRefreshTokens({
|
||||
lookupField: `WHERE expires_at < CURRENT_TIMESTAMP`,
|
||||
});
|
||||
};
|
||||
|
||||
const getUsersRoleAndPermission = ({ userId, res }) => {
|
||||
const userRoles = authService.getUserRoleByUserId({ userId });
|
||||
|
||||
@@ -233,7 +287,14 @@ const getUsersRoleAndPermission = ({ userId, res }) => {
|
||||
return { userRoles, roleIds, permissions };
|
||||
};
|
||||
|
||||
const isRefreshTokenRevoked = ({ refreshToken }) => {
|
||||
const tokens = authService.getRevokedRefreshToken({ refreshToken });
|
||||
return tokens.length > 0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
obtainAccessToken,
|
||||
refreshAccessToken,
|
||||
removeTokens,
|
||||
removeRevokedRefreshTokens,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -22,9 +22,11 @@ const { setupExtensions } = require('./extensions');
|
||||
const {
|
||||
createDefaultTables,
|
||||
createInitialUser,
|
||||
removeRevokedRefreshTokens,
|
||||
} = require('./controllers/auth');
|
||||
|
||||
const { runCLICommands } = require('./commands');
|
||||
const { authConstants } = require('./constants');
|
||||
|
||||
const app = express();
|
||||
app.get('/health', (req, res) => {
|
||||
@@ -93,6 +95,12 @@ if (config.auth) {
|
||||
);
|
||||
}
|
||||
|
||||
// remove revoked refresh tokens every X days
|
||||
setInterval(
|
||||
removeRevokedRefreshTokens,
|
||||
authConstants.REVOKED_REFRESH_TOKENS_REMOVAL_TIME_RANGE,
|
||||
);
|
||||
|
||||
// If the user has passed custom CLI commands run the command and exit to avoid running the server
|
||||
runCLICommands();
|
||||
|
||||
|
||||
@@ -26,4 +26,10 @@ router.put(
|
||||
controllers.changePassword,
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/logout',
|
||||
validator(schema.removeAccessTokens),
|
||||
controllers.removeTokens,
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -79,10 +79,21 @@ const updateRolePermissions = Joi.object({
|
||||
}).required(),
|
||||
});
|
||||
|
||||
const removeAccessTokens = Joi.object({
|
||||
query: Joi.object().required(),
|
||||
params: Joi.object({}).required(),
|
||||
body: Joi.object({}).required(),
|
||||
cookies: Joi.object({
|
||||
refreshToken: Joi.string().required(),
|
||||
accessToken: Joi.string().required(),
|
||||
}).required(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
obtainAccessToken,
|
||||
refreshAccessToken,
|
||||
changePassword,
|
||||
registerUser,
|
||||
updateRolePermissions,
|
||||
removeAccessTokens,
|
||||
};
|
||||
|
||||
@@ -8,10 +8,11 @@ const {
|
||||
ROLES_TABLE,
|
||||
USERS_ROLES_TABLE,
|
||||
ROLES_PERMISSIONS_TABLE,
|
||||
REVOKED_REFRESH_TOKENS_TABLE,
|
||||
tableFields,
|
||||
} = dbConstants;
|
||||
|
||||
module.exports = () => {
|
||||
module.exports = (db) => {
|
||||
return {
|
||||
getUsersByUsername({ username }) {
|
||||
const users = rowService.get({
|
||||
@@ -43,6 +44,7 @@ module.exports = () => {
|
||||
return users;
|
||||
},
|
||||
|
||||
// TODO: bypass pagination by providing query param for number of rows
|
||||
getPermissionByRoleIds({ roleIds }) {
|
||||
const permissions = rowService.get({
|
||||
tableName: ROLES_PERMISSIONS_TABLE,
|
||||
@@ -50,6 +52,7 @@ module.exports = () => {
|
||||
() => '?',
|
||||
)})`,
|
||||
whereStringValues: [...roleIds],
|
||||
limit: 10000,
|
||||
});
|
||||
|
||||
return permissions;
|
||||
@@ -74,5 +77,34 @@ 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;
|
||||
},
|
||||
|
||||
deleteRevokedRefreshTokens({ lookupField }) {
|
||||
const query = `DELETE FROM ${REVOKED_REFRESH_TOKENS_TABLE} ${lookupField}`;
|
||||
const statement = db.prepare(query);
|
||||
const result = statement.run();
|
||||
return result;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -189,6 +189,10 @@ const doc = {
|
||||
},
|
||||
|
||||
InvalidPasswordErrorResponse: { message: 'Invalid password' },
|
||||
|
||||
RemoveTokensResponse: {
|
||||
message: 'Logout successful',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"title": "Soul API",
|
||||
"description": "API Documentation for <b>Soul</b>, a SQLite REST and realtime server. "
|
||||
},
|
||||
@@ -535,6 +535,17 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/auth/logout": {
|
||||
"get": {
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@@ -985,6 +996,15 @@
|
||||
"example": "Invalid password"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RemoveTokensResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Logout successful"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user