@keeex/utils-express
v4.2.7
Published
Shared code for express servers
Downloads
5
Readme
Utility functions for express
Some tools for express. For now there's a handler for promise-based requests and an authentication middleware.
For developers
When working on this library, run npm run setup to prepare generated files.
Promise-based request
Express does not handle promises. This library promise a helper to handle requests as promises.
Handling a request is done in two parts:
- handling the request (requestHandler)
- sending the reply (responseHandler)
The request handling is done by writing a requestHandler taking req and res as parameters.
These handlers can return anything; that return value will be handled by a responseHandler.
You write as many requestHandler as type of requests.
The responseHandler can be reused for multiple requests, and take as parameter data, req,
res, next and extraData.
data is the result of the requestHandler, and extraData are the value passed to the caller.
First you can create multiple "middlewares" that take the responseHandler as parameter, and these can then be used with requestHandler to process requests.
Both the responseHandler and the requestHandler can return promises.
Example:
import express from "express";
import {createAsyncHandler} from "@keeex/utils-express/lib/promise.js";
const testDataFunc = () => new Promise(resolve => {
const startTime = Date.now();
setTimeout(() => {
resolve({startTime, replyTime: Date.now()});
}, 1000);
});
const responseHandler = (data, req, res, next) => {
if (data === "failure") next(new Error("Failure error"));
if (data) {
res.json({
timestamp: Date.now(),
...data,
});
return;
}
res.sendStatus(500);
};
const handler = createAsyncHandler(responseHandler);
const router = express.Router();
router.get("/failure", handler((req, res) => {
return "failure";
}));
router.get("/data", handler(async (req, res) => {
const res = await testDataFunc();
res.valid = true;
return res;
}));In the above example, the "/failure" route will use the set error handlers and the "/data" route
will resolve with {startTime, replyTime, valid, timestamp} as a JSON object.
Alternatively, the legacy behavior described below is available as a default middleware
exported as promiseHandler().
You can use promiseHandler(yourfunction) in the express router.
Note that res is provided only for header manipulations; no data is expected to
be sent by the handling promise.
When the promise resolve, the result is inspected and depending on it three outcome can happen:
- an object is returned: in that case, the object is used in the reply as-is. It is replied as a JSON.
- null is returned: an object with a single
okproperty set to true is replied as a JSON. - undefined is returned: the next handle will be called on the request
If the promise reject, the error is left to the error handlers.
Express-validator
The library provides a handful of helpers to take advantage of express-validator.
Namely, a dedicated middleware that will run all the validators in an array then checks the result
and reply with the appropriate status code.
router.get(
"/someRoute",
validate([
body("someField").isString().isLength({ min: 1 }),
body("someOtherField").isInt().isLength({ min: 1 }),
]),
(req, res) => {
// ...
}
);In addition, a promise-based validator handler is available:
router.get(
"/someRoute",
validatedHandler(
[
body("someField").isString().isLength({ min: 1 }),
body("someOtherField").isInt().isLength({ min: 1 }),
],
async (data: {someField: string, someOtherField: number}): Promise<unknown> => {
// ...
},
),
);That function behaves like promiseHandler() but will receive all the validated data as a single
argument before req and res.
Authentication middlewares
Some basic authentication-handling middlewares and functions are provided. They are based on JWT used to keep the user's identifier throughout multiple requests.
They are:
- authMiddleware(): retrieve the token from the request and converts it to a UserType value
- requireUserMiddleware(): fails if no user is connected or if the user does not pass the provided check
- setToken(): helper function to generate/set the token used for authentication
- clearToken(): helper function to unset a cookie (if a cookie is used)
- getUser(): helper function to get the logged-in user, if applicable
When using a cookie, the process is mostly automatic (as long as the client uses a compliant
browser).
When using a header, the client-side is responsible for providing the appropriate header with each
request and the server is responsible for passing back the token to the client.
The header's value is in the form Bearer <token> with the default settings.
Configuration
The auth middlewares and functions all depends on a configuration object implementing the
AuthSettings interface.
It's properties are:
- authName (optional): name for that authentication scheme. Multiple authentication scheme can be used separately.
- authTokenLocation: either
TokenLocation.cookieorTokenLocation.header - authTokenName (optional): name of the cookie/header. Defaults to "authtoken" or "Authorization" for cookie or header.
- authTokenSecret: secret string used to authenticate the authentication token
- getUserFromIdentifier: function that resolve or return with a user from a string identifier
- getIdentifierFromUser: function that resolve or return with a string identifier from a user
- cookieOptions (optional): used only with cookie auth, allow setting some extra cookie options. will enforce httpOnly and secure to true, and defaults to SameSite: strict
The JWT can use ECDSA for signature.
To do so, instead of providing a string for authTokenSecret, provide an object:
interface AuthTokenSecretAsymetric {
privateKey: string;
publicKey: string;
}Both should be in PEM format, using the curve256p1 elliptic curve. Such keys can be generated using openssl:
openssl ecparam -name prime256v1 -genkey -noout > key.priv
openssl ec -pubout < key.priv > key.pubExample
Assuming a server where user's are an object with an "admin" boolean property, the following example shows how to handle authentication in a basic way:
import express from "express";
import {promiseHandler} from "@keeex/utils-express/lib/promise.js";
import * as authMiddleware from "@keeex/utils-express/lib/middleware/auth.js";
import db from "./db/index.js";
const authConfig = {
authTokenLocation: authMiddleware.TokenLocation.cookie,
authTokenSecret: "somerandomestring",
getAuthDataFromIdentifier: identifier => db.models.User.findByPk(parseInt(identifier, 26)),
getIdentifierFromAuthData: authData => authData.id.toString(26),
cookie: true,
header: true,
};
const mwAuthentication = authMiddleware.createAuthMiddleware(authConfig);
const mwRequire = authMiddleware.createRequireMiddleware(authConfig);
const setToken = authMiddleware.createSetToken(authConfig, authMiddleware.TokenLocation.cookie);
const getHeaderToken = authMiddleware.createGetToken(authConfig);
const clearToken = authMiddleware.createClearToken(authConfig);
const router = express.Router();
router.use(mwAuthentication());
router.post(
"/login",
promiseHandler(async (req, res) => {
// Check user credentials
const user = getUserFromLogin(req.body.login);
await setToken(user, req, res);
// Useless, return the user token
return {
headerName: "Authorization",
token: getHeaderToken(user),
};
}),
);
router.get(
"/authenticated",
mwRequire(),
promiseHandler((async req, res) => {
const user = authMiddleware.getUser(req);
const tokenLocation = authMiddleware.getTokenLocation(req);
if (tokenLocation) console.log("Token location:", tokenLocation);
return getSomeData(user);
}),
);
router.get(
"/adminonly",
mwRequire(authdata => authdata.admin),
promiseHandler((async req, res) => {
const user = authMiddleware.getUser(req);
return getSomeData(user);
}),
);
router.get(
"/logout",
(req, res) => {
clearToken(res);
res.send({ok: true});
},
);For convenience, it is possible to use a single function to retrieve all authentication functions in one call, using default values were applicable (note that the auth middleware, taking no parameters, is not a function in this case):
import express from "express";
import {promiseHandler} from "@keeex/utils-express/lib/promise.js";
import {getAuthFunctions} from "@keeex/utils-express/lib/middleware/auth.js";
import db from "./db/index.js";
const authConfig = {
authTokenLocation: authMiddleware.TokenLocation.cookie,
authTokenSecret: "somerandomestring",
getAuthDataFromIdentifier: identifier => db.models.User.findByPk(parseInt(identifier, 26)),
getIdentifierFromAuthData: authData => authData.id.toString(26),
cookie: true,
header: true,
};
const auth = getAuthFunctions(authConfig);
const router = express.Router();
router.use(auth.mwAuth);
router.post(
"/login",
promiseHandler(async (req, res) => {
// Check user credentials
const user = getUserFromLogin(req.body.login);
await auth.setToken(user, req, res);
// Useless, return the user token
return {
headerName: "Authorization",
token: auth.getHeaderToken(user),
};
}),
);
router.get(
"/authenticated",
auth.mwRequire(),
promiseHandler((async req, res) => {
const user = auth.getAuthDataSafe(req);
const tokenLocation = auth.getTokenLocationSafe(req);
console.log("Token location:", tokenLocation);
return getSomeData(user);
}),
);
router.get(
"/adminonly",
auth.mwRequire(authdata => authdata.admin),
promiseHandler((async req, res) => {
const user = auth.getAuthDataSafe(req);
return getSomeData(user);
}),
);
router.get(
"/logout",
(req, res) => {
auth.clearToken(res);
res.send({ok: true});
},
);Header configuration
The default configuration for tokens in HTTP header is to use the form Bearer <token base64>.
This can be customised by providing an object for the header configuration property.
The interface is HeaderSettings.
Authenticated promise-based handlers
Analogous to promiseHandler() and validatedHandler(), the functions promiseAuthHandler() and
validatedAuthHandler() are available.
They take an additional parameter which is the authFunctions object from the auth middleware, and
their handling functions receive the authenticated user as the first argument.
They otherwise behave the same.
const auth = getAuthFunctions(/* ... */);
router.get(
"/someRoute",
auth.mwRequire(),
validatedAuthHandler(
auth,
[
body("someField").isString().isLength({ min: 1 }),
body("someOtherField").isInt().isLength({ min: 1 }),
],
async (user: UserInfo, data: {someField: string, someOtherField: number}): Promise<unknown> => {
// ...
},
),
);Basic filter middleware
A low-level IP/token based authorization middleware is available as /lib/middleware/basicfilter.js
Here's a short example of usage:
import basicfilter from "@keeex/utils-express/lib/middleware/basicfilter.js";
router.get(
"/status",
basicfilter({
allowedIP: ["1.2.3.4", "6.5.7.8"],
allowedToken: "ArthurKingOfTheBriton",
}),
(req, res) => res.send({status: "ok"}),
);The above example will accept all requests from the two given IP (make sure "trust proxy" is set
as it should) or for any request with the header Authorization: Bearer ArthurKingOfTheBriton.
Both properties can be either array or string.
Swagger-UI middleware
To serve a swagger UI interface based on a pre-built JSON file, you can use the
@keeex/utils-express/lib/middleware/apidoc.js middleware.
File cleanup middleware
In case you use multer to receive file you must cleanup the file you receive after processing
them. Wrap you multer middleware with this to ensure file is always deleted at the end of the http
call.
⚠️ The file is deleted when the res.end function is called; if you handle the file asynchronously
after the http call has been resolve the file won't exists anymore.
import fileCleanupWrapper from "@keeex/utils-express/lib/middleware/filecleanup.js";
import multer from "multer";
/**
* fileCleanupWrapper
* @param {RequestHandler} requestHandler
* @param {(error: unkown) => void | Promise<void>} errorHandler
* @returns RequestHandler
*/
const storage = multer.diskStorage({
destination: "./",
filename: (_req, file, cb) => cb(
null,
path.basename(tmp.tmpNameSync({
prefix: `${Date.now()}-upload`,
postfix: path.extname(file.originalname),
})),
),
});
export const groupLogoUpload = (
file: string = "file",
): RequestHandler => [
fileCleanupWrapper(console.error(error)),
multer({limits: {fileSize: 1_000_000}, storage}).single(file),
];Token middleware
Simple middleware that extract a string token from an Authorization header and compare it to known
values (no deconstruction, structure or anything).
Usage:
import {
tokenMiddleware,
getTokenName,
getTokenValue,
} from "@keeex/utils-express/middleware/token.js";
/** Hardcoded list of tokens */
const knownTokens = {
serviceA: "token1234",
serviceB: "token5678",
serviceC: ["tokenABCD", "tokenEFGH"],
monitoring: "abcdef",
};
/** Custom functions to match a token with a service name */
const getTokenService = async (token: string): Awaitable<string | undefined> => {
if (token === "someOtherToken1234") return "someRealService";
}
/** Create the middleware function */
const tokenCheck = tokenMiddleware({
dynamicTokens: getTokenService,
header: "Authorization",
tokens: knownTokens,
tokenType: "Bearer",
});
/** Will detect any known service, but still proceed if no known token is present. */
route.get(
"/checkToken",
tokenCheck(),
(req, res) => {
const service = getTokenName(req);
if (service === false) {
res.send("No token present");
} else if (service === null) {
res.send(`Unknown token present: ${getTokenValue(req)}`);
} else {
res.send(`Detected service: ${service}`);
}
},
);
/** Will require a known service, any of them */
route.get(
"/checkToken",
tokenCheck(true),
(req, res) => {
const service = getTokenName(req);
res.send(`Detected service: ${service}`);
},
);
/** Will require the "monitoring" service */
route.get(
"/status",
tokenCheck("monitoring"),
(req, res) => {
res.send("Ok");
},
);