This module offers these features:
- Query parser (req.query),
initially made to work with Mongoose
- Express Rate Limiter
- joi validators
- body parser
- cookie parser
- cors
- Response Headers
- helmet
- compression
- x-powered-by
- trust proxy
- api-key simple implementation
This module is built to be use with Express.
npm install --save @studiowebux/security
Key | Value | Description | More info |
---|---|---|---|
bodyParser |
limit and extended
|
https://www.npmjs.com/package/body-parser | |
cookieParser | secret |
https://www.npmjs.com/package/cookie-parser | |
cors |
whitelist the array of URLs authorized |
Use [] to disable the cors |
https://www.npmjs.com/package/cors |
server | See below, The response headers and the proxy configuration | ||
rateLimiters | See below, An array of objects | https://www.npmjs.com/package/express-rate-limiter |
Example:
const opts = {
bodyParser: {
limit: "1mb",
extended: false,
},
cookieParser: {
secret: process.env.COOKIE_SECRET || "CookieSecret",
},
cors: {
whitelist: ["https://webuxlab.com", "http://127.0.0.1"], // or [] to disable cors
},
server: {
trustProxy: true,
allowedMethods: "GET, POST, PUT, DELETE, PATCH, OPTIONS",
allowedCredentials: false,
allowedHeaders:
"Origin, X-Requested-with, Accept, Authorization, Content-Type, Accept-Language",
},
helmet: undefined, // See official helmet documentation
rateLimiters: [
{
name: "Authentication",
time: 3600, // blocked for 1 hour
maxReq: 3, // after 3 tries
pattern: "/auth", // The route prefix to apply this limiter
},
{
name: "Global",
time: 60, // blocked for 1 minute
maxReq: 5, // after 5 tries the requester will be blocked for 1 minute
pattern: "", // It applies globally
},
],
};
Initialize the configurations globally.
const WebuxSecurity = require("@studiowebux/security");
const Security = new WebuxSecurity(opts, console);
The opts
parameter is mandatory, it configures the security module.
The log
parameter allows to use a custom logger, by default it uses the console.
This property has all joi
based validators attached.
To access a validator, Security.validators.Body(...)
For more information, please read the official joi
documentation,
Schemas
const Joi = require("joi");
const Create = Joi.object()
.keys({
user: {
username: Joi.string().required(),
premium: Joi.boolean().required(),
},
})
.required();
const Update = Joi.object({
user: {
premium: Joi.boolean().required(),
},
}).required();
const ID = Joi.string()
.pattern(/^[0-9]*$/)
.required();
const Something = Joi.object({
items: Joi.array().required(),
}).required();
Usage
app.post("/something", async (req, res) => {
await Security.validators
.Custom(Something, req.body)
.then((value) => {
return res.status(200).json({ msg: "Bonjour !" });
})
.catch((err) => {
console.error(err);
return res.status(400).json({ msg: "BAD_REQUEST", reason: err.message });
});
});
app.post(
"/account/:id",
Security.validators.Id(ID),
Security.validators.Body(Update),
(req, res) => {
console.info("Hello World !");
return res.status(200).json({ msg: "Bonjour !" });
}
);
Where the
...
is the Joi validator code. These functions will return nothing if the data is valid. Or a structured error using theerrorHandler
function.
Body(Schema)(req, res, next)=>{...};
Id(Schema)(req, res, next)=>{...};
MongoID(Schema)(req, res, next)=>{...};
MongoIdOrURL(Schema)(req, res, next)=>{...};
User(Schema)(req, res, next)=>{...};
Headers(Schema)(req, res, next)=>{...};
Files(Schema)(req, res, next)=>{...};
Custom(Schema, object);
You can override this function,
(code, msg, extra, devMsg) => {
let error = new Error();
error.code = code || 500;
error.message = msg || "";
error.extra = extra || {};
error.devMessage = devMsg || "";
return error;
};
It loads the response headers using the res.header(...)
const express = require("express");
const app = express();
Security.SetResponseHeader(app);
The app
parameter is mandatory, it is used to configure the headers.
It configures:
- The
compression
- The
trust proxy
Helmet
- The
x-powered-by
const express = require("express");
const app = express();
Security.SetGlobal(app);
The app
parameter is mandatory, it is used to configure the modules.
It configures the body parser.
const express = require("express");
const app = express();
Security.SetBodyParser(app);
The app
parameter is mandatory, it is used to configure the modules.
It configures the cookie parser.
const express = require("express");
const app = express();
Security.SetCookieParser(app);
The app
parameter is mandatory, it is used to configure the modules.
It configures the cors.
To disable all cors, in the option, specify :
[]
const express = require("express");
const app = express();
Security.SetCors(app);
The app
parameter is mandatory, it is used to configure the modules.
It configures the rate limiters
const express = require("express");
const app = express();
Security.CreateRateLimiters(app);
The app
parameter is mandatory, it is used to configure the modules.
This method is static
It parses the req.query
, if there is blacklisted word present, it will return an error.
Otherwise, the idea behind this function is to facilitate the query with MongoDB.
But it can be use to parse the query only.
let blacklist_fields = ["password", "birthday", "phoneNumber"];
let defaultSelect = "username, email, fullname";
Security.QueryParser(blacklist_fields, defaultSelect);
The blacklist
parameter is optional
The defaultSelect
parameter is optional
The errorHandler
parameter is optional (it uses the default one by default)
Using this code and this request, returns
// http://localhost:1337/account?limit=5&sort=-username&skip=100
app.get(
"/account",
Security.QueryParser(["password"], "username premium"),
(req, res) => {
console.log(req.query);
res.status(200).json({ query: req.query });
}
);
Return: http://localhost:1337/account?limit=5&sort=-username&skip=100
{
"query": {
"filter": {},
"limit": 5,
"sort": [
"-username"
],
"skip": 100,
"projection": "username premium"
}
}
_http://localhost:1337/account?limit=5&sort=-username&skip=100_&filter=username eq 'bonjour'_
{
"query": {
"filter": {
"username": {
"$eq": "bonjour"
}
},
"limit": 5,
"sort": [
"-username"
],
"skip": null,
"projection": "username premium"
}
}
http://localhost:1337/account?limit=5&sort=-username&skip=100&filter=password eq 'something'
{
"code": 400,
"message": "INVALID_REQUEST",
"extra": {},
"devMessage": "Query may contains blacklisted items."
}
options.js
module.exports = {
bodyParser: {
limit: "1mb",
extended: false,
},
cookieParser: {
secret: process.env.COOKIE_SECRET || "CookieSecretNotVerySecure...",
},
cors: {
whitelist: ["https://webuxlab.com", "http://127.0.0.1"], // or [] to disable cors
},
server: {
trustProxy: true,
allowedMethods: "GET, POST, PUT, DELETE, PATCH, OPTIONS",
allowedCredentials: false,
allowedHeaders:
"Origin, X-Requested-with, Accept, Authorization, Content-Type, Accept-Language",
},
rateLimiters: [
{
name: "Authentication",
time: 3600, // blocked for 1 hour
maxReq: 10, // after 10 tries
pattern: "/auth", // The route prefix to apply this limiter
},
{
name: "Global",
time: 60, // blocked for 1 minute
maxReq: 150, // after 5 tries the requester will be blocked for 1 minute
pattern: "", // It applies globally
},
],
};
validators.js
const Joi = require("joi");
const Create = Joi.object()
.keys({
user: {
username: Joi.string().required(),
premium: Joi.boolean().required(),
},
})
.required();
const Update = Joi.object({
user: {
premium: Joi.boolean().required(),
},
}).required();
const ID = Joi.string()
.pattern(/^[0-9]*$/)
.required();
const Something = Joi.object({
items: Joi.array().required(),
}).required();
module.exports = {
Something,
ID,
Update,
Create,
};
app.js
const WebuxSecurity = require("@studiowebux/security");
const express = require("express");
const app = express();
const options = require("./options");
const { Something, ID, Update, Create } = require("./validators");
module.exports = async function loadApp() {
const Security = new WebuxSecurity(options, console);
Security.SetResponseHeader(app);
Security.SetBodyParser(app);
Security.SetCookieParser(app);
Security.SetCors(app);
Security.SetGlobal(app);
Security.CreateRateLimiters(app);
app.get("/", (req, res) => {
console.info("Hello World !");
return res.status(200).json({ msg: "Bonjour !" });
});
// http://localhost:1337/account?limit=5&sort=-username&skip=100
app.get(
"/account",
Security.QueryParser(["password"], "username premium"),
(req, res) => {
console.log(req.query);
res.status(200).json({ query: req.query });
}
);
app.post("/something", async (req, res) => {
await Security.validators
.Custom(Something, req.body)
.then((value) => {
return res.status(200).json({ msg: "Bonjour !" });
})
.catch((err) => {
console.error(err);
return res
.status(400)
.json({ msg: "BAD_REQUEST", reason: err.message });
});
});
app.post(
"/account/:id",
Security.validators.Id(ID),
Security.validators.Body(Update),
(req, res) => {
console.info("Hello World !");
return res.status(200).json({ msg: "Bonjour !" });
}
);
app.post("/account", Security.validators.Body(Create), (req, res) => {
console.info("Hello World !");
return res.status(200).json({ msg: "Bonjour !" });
});
app.post("/", (req, res) => {
console.info("Hello World !");
console.log(req.cookies);
return res.status(200).json({ msg: "Bonjour !" });
});
app.use("*", (error, req, res, next) => {
console.error(error);
res.status(error.code || 500).json(error || "An error occured");
});
app.listen(1337, () => {
console.log("Server listening on port 1337");
});
};
server.js
const loadApp = require("./app.js");
try {
loadApp();
} catch (e) {
console.error(e);
process.exit(1);
}
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
SEE LICENSE IN license.txt