4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "soul-cli",
|
"name": "soul-cli",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "soul-cli",
|
"name": "soul-cli",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "soul-cli",
|
"name": "soul-cli",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"description": "A SQLite REST and Realtime server",
|
"description": "A SQLite REST and Realtime server",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ module.exports = {
|
|||||||
SALT_ROUNDS: 10,
|
SALT_ROUNDS: 10,
|
||||||
ACCESS_TOKEN_SUBJECT: 'accessToken',
|
ACCESS_TOKEN_SUBJECT: 'accessToken',
|
||||||
REFRESH_TOKEN_SUBJECT: 'refreshToken',
|
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',
|
PASSWORD_UPDATE_SUCCESS: 'Password updated successfully',
|
||||||
USER_UPDATE_SUCCESS: 'User updated successfully',
|
USER_UPDATE_SUCCESS: 'User updated successfully',
|
||||||
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
|
INITIAL_USER_CREATED_SUCCESS: 'Initial user created successfully',
|
||||||
|
LOGOUT_MESSAGE: 'Logout successful',
|
||||||
},
|
},
|
||||||
|
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ const USERS_TABLE = '_users';
|
|||||||
const ROLES_TABLE = '_roles';
|
const ROLES_TABLE = '_roles';
|
||||||
const USERS_ROLES_TABLE = '_users_roles';
|
const USERS_ROLES_TABLE = '_users_roles';
|
||||||
const ROLES_PERMISSIONS_TABLE = '_roles_permissions';
|
const ROLES_PERMISSIONS_TABLE = '_roles_permissions';
|
||||||
|
const REVOKED_REFRESH_TOKENS_TABLE = '_revoked_refresh_tokens';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// db table names
|
||||||
USERS_TABLE,
|
USERS_TABLE,
|
||||||
ROLES_TABLE,
|
ROLES_TABLE,
|
||||||
USERS_ROLES_TABLE,
|
USERS_ROLES_TABLE,
|
||||||
ROLES_PERMISSIONS_TABLE,
|
ROLES_PERMISSIONS_TABLE,
|
||||||
|
REVOKED_REFRESH_TOKENS_TABLE,
|
||||||
|
|
||||||
reservedTableNames: [
|
reservedTableNames: [
|
||||||
USERS_TABLE,
|
USERS_TABLE,
|
||||||
@@ -43,5 +46,9 @@ module.exports = {
|
|||||||
|
|
||||||
// _users_roles fields
|
// _users_roles fields
|
||||||
USER_ID: 'user_id',
|
USER_ID: 'user_id',
|
||||||
|
|
||||||
|
//_revoked_refresh_tokens
|
||||||
|
REFRESH_TOKEN: 'refresh_token',
|
||||||
|
EXPIRES_AT: 'expires_at',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const {
|
|||||||
ROLES_TABLE,
|
ROLES_TABLE,
|
||||||
USERS_ROLES_TABLE,
|
USERS_ROLES_TABLE,
|
||||||
ROLES_PERMISSIONS_TABLE,
|
ROLES_PERMISSIONS_TABLE,
|
||||||
|
REVOKED_REFRESH_TOKENS_TABLE,
|
||||||
constraints,
|
constraints,
|
||||||
tableFields,
|
tableFields,
|
||||||
} = dbConstants;
|
} = dbConstants;
|
||||||
@@ -21,6 +22,9 @@ const createDefaultTables = async () => {
|
|||||||
ROLES_PERMISSIONS_TABLE,
|
ROLES_PERMISSIONS_TABLE,
|
||||||
);
|
);
|
||||||
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE);
|
const usersRolesTable = tableService.checkTableExists(USERS_ROLES_TABLE);
|
||||||
|
const revokedRefreshTokensTable = tableService.checkTableExists(
|
||||||
|
REVOKED_REFRESH_TOKENS_TABLE,
|
||||||
|
);
|
||||||
|
|
||||||
// create _users table
|
// create _users table
|
||||||
if (!usersTable) {
|
if (!usersTable) {
|
||||||
@@ -89,6 +93,14 @@ const createDefaultTables = async () => {
|
|||||||
fields: permissions,
|
fields: permissions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create _revoked_refresh_tokens table
|
||||||
|
if (!revokedRefreshTokensTable) {
|
||||||
|
tableService.createTable(
|
||||||
|
REVOKED_REFRESH_TOKENS_TABLE,
|
||||||
|
schema.revokedRefreshTokensSchema,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -123,11 +123,19 @@ const refreshAccessToken = async (req, res) => {
|
|||||||
#swagger.summary = 'Refresh Access Token'
|
#swagger.summary = 'Refresh Access Token'
|
||||||
#swagger.description = 'Endpoint to refresh access and refresh tokens'
|
#swagger.description = 'Endpoint to refresh access and refresh tokens'
|
||||||
*/
|
*/
|
||||||
|
const refreshTokenFromCookies = req.cookies.refreshToken;
|
||||||
|
|
||||||
try {
|
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
|
// extract the payload from the token and verify it
|
||||||
const payload = await decodeToken(
|
const payload = await decodeToken(
|
||||||
req.cookies.refreshToken,
|
refreshTokenFromCookies,
|
||||||
config.tokenSecret,
|
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 getUsersRoleAndPermission = ({ userId, res }) => {
|
||||||
const userRoles = authService.getUserRoleByUserId({ userId });
|
const userRoles = authService.getUserRoleByUserId({ userId });
|
||||||
|
|
||||||
@@ -233,7 +287,14 @@ const getUsersRoleAndPermission = ({ userId, res }) => {
|
|||||||
return { userRoles, roleIds, permissions };
|
return { userRoles, roleIds, permissions };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRefreshTokenRevoked = ({ refreshToken }) => {
|
||||||
|
const tokens = authService.getRevokedRefreshToken({ refreshToken });
|
||||||
|
return tokens.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
obtainAccessToken,
|
obtainAccessToken,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
|
removeTokens,
|
||||||
|
removeRevokedRefreshTokens,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -115,4 +115,22 @@ module.exports = {
|
|||||||
foreignKey: { table: ROLES_TABLE, column: tableFields.ID },
|
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 {
|
const {
|
||||||
createDefaultTables,
|
createDefaultTables,
|
||||||
createInitialUser,
|
createInitialUser,
|
||||||
|
removeRevokedRefreshTokens,
|
||||||
} = require('./controllers/auth');
|
} = require('./controllers/auth');
|
||||||
|
|
||||||
const { runCLICommands } = require('./commands');
|
const { runCLICommands } = require('./commands');
|
||||||
|
const { authConstants } = require('./constants');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.get('/health', (req, res) => {
|
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
|
// If the user has passed custom CLI commands run the command and exit to avoid running the server
|
||||||
runCLICommands();
|
runCLICommands();
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,10 @@ router.put(
|
|||||||
controllers.changePassword,
|
controllers.changePassword,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/logout',
|
||||||
|
validator(schema.removeAccessTokens),
|
||||||
|
controllers.removeTokens,
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -79,10 +79,21 @@ const updateRolePermissions = Joi.object({
|
|||||||
}).required(),
|
}).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 = {
|
module.exports = {
|
||||||
obtainAccessToken,
|
obtainAccessToken,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
changePassword,
|
changePassword,
|
||||||
registerUser,
|
registerUser,
|
||||||
updateRolePermissions,
|
updateRolePermissions,
|
||||||
|
removeAccessTokens,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ const {
|
|||||||
ROLES_TABLE,
|
ROLES_TABLE,
|
||||||
USERS_ROLES_TABLE,
|
USERS_ROLES_TABLE,
|
||||||
ROLES_PERMISSIONS_TABLE,
|
ROLES_PERMISSIONS_TABLE,
|
||||||
|
REVOKED_REFRESH_TOKENS_TABLE,
|
||||||
tableFields,
|
tableFields,
|
||||||
} = dbConstants;
|
} = dbConstants;
|
||||||
|
|
||||||
module.exports = () => {
|
module.exports = (db) => {
|
||||||
return {
|
return {
|
||||||
getUsersByUsername({ username }) {
|
getUsersByUsername({ username }) {
|
||||||
const users = rowService.get({
|
const users = rowService.get({
|
||||||
@@ -43,6 +44,7 @@ module.exports = () => {
|
|||||||
return users;
|
return users;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: bypass pagination by providing query param for number of rows
|
||||||
getPermissionByRoleIds({ roleIds }) {
|
getPermissionByRoleIds({ roleIds }) {
|
||||||
const permissions = rowService.get({
|
const permissions = rowService.get({
|
||||||
tableName: ROLES_PERMISSIONS_TABLE,
|
tableName: ROLES_PERMISSIONS_TABLE,
|
||||||
@@ -50,6 +52,7 @@ module.exports = () => {
|
|||||||
() => '?',
|
() => '?',
|
||||||
)})`,
|
)})`,
|
||||||
whereStringValues: [...roleIds],
|
whereStringValues: [...roleIds],
|
||||||
|
limit: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return permissions;
|
return permissions;
|
||||||
@@ -74,5 +77,34 @@ module.exports = () => {
|
|||||||
|
|
||||||
return defaultRole;
|
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' },
|
InvalidPasswordErrorResponse: { message: 'Invalid password' },
|
||||||
|
|
||||||
|
RemoveTokensResponse: {
|
||||||
|
message: 'Logout successful',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"info": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.2",
|
||||||
"title": "Soul API",
|
"title": "Soul API",
|
||||||
"description": "API Documentation for <b>Soul</b>, a SQLite REST and realtime server. "
|
"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": {
|
"definitions": {
|
||||||
@@ -985,6 +996,15 @@
|
|||||||
"example": "Invalid password"
|
"example": "Invalid password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"RemoveTokensResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Logout successful"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user