backtrace-service
Advanced tools
Comparing version 1.0.3 to 1.0.4
160
index.js
const bodyParser = require('body-parser') | ||
const request = require('request') | ||
const crypto = require('crypto'); | ||
function apiServiceRegister(options, req_obj, res) { | ||
function genHmac(secret, nonce) { | ||
return crypto.createHmac('sha256', secret).update(nonce).digest('hex'); | ||
} | ||
function checkHmac(secret, nonce, proposed_hmac) { | ||
return genHmac(secret, nonce) === proposed_hmac; | ||
} | ||
function log(svcOpts, level, msg) { | ||
if (svcOpts.logger && svcOpts.logger.log) | ||
svcOpts.logger.log(level, msg); | ||
} | ||
function sendError(svcOpts, req, res, code, result) { | ||
const res_obj = Object.assign({status: 'error'}, result); | ||
log(svcOpts, 'warn', `${req.ip}: error ${code}: ${JSON.stringify(res_obj)}`); | ||
res.status(code).send(res_obj); | ||
} | ||
function missingParam(svcOpts, req, res, param) { | ||
sendError(svcOpts, req, res, 400, {error: `missing ${param} parameter`}); | ||
} | ||
function missingInternalParam(svcOpts, req, res, param) { | ||
log(svcOpts, 'error', `${req.ip}: error: missing internal param ${param}`); | ||
sendError(svcOpts, req, res, 500, {error: "internal error"}); | ||
} | ||
function apiServiceRegister(svcOpts, req, req_obj, res) { | ||
var stoken; | ||
if (!req_obj.url) { | ||
res.status(400).send({status: "error", error: "missing url parameter"}); | ||
return; | ||
} | ||
if (!req_obj.url) | ||
return missingParam(svcOpts, req, res, "url"); | ||
if (!req_obj.nonce) | ||
return missingParam(svcOpts, req, res, "nonce"); | ||
if (!req_obj.hmac) | ||
return missingParam(svcOpts, req, res, "hmac"); | ||
if (options.authTokens.includes(req_obj.token) === false) { | ||
res.status(403).send({status: "error", error: "invalid token"}); | ||
if (!checkHmac(svcOpts.secret, req_obj.nonce, req_obj.hmac)) { | ||
sendError(svcOpts, req, res, 403, {error: "unauthenticated request"}); | ||
return; | ||
} | ||
options.coronerdCallback(req_obj.url); | ||
if (svcOpts.coronerdCallback) | ||
svcOpts.coronerdCallback(req_obj.url); | ||
res.json({ | ||
@@ -22,2 +56,14 @@ status: "ok", | ||
function checkOptions(svcOpts, caller) { | ||
if (!svcOpts) { | ||
throw new TypeError(`${caller}() requires options`); | ||
} | ||
if (typeof(svcOpts.name) !== 'string' || svcOpts.name.length === 0) { | ||
throw new TypeError(`${caller}() requires options.name string`); | ||
} | ||
if (typeof(svcOpts.secret) !== 'string' || svcOpts.secret.length === 0) { | ||
throw new TypeError(`${caller}() requires options.secret string`); | ||
} | ||
} | ||
/* | ||
@@ -27,16 +73,8 @@ * Generate a middleware handler for Express.js, using options to hook the | ||
*/ | ||
function serviceRequest(options) { | ||
if (!options) { | ||
throw new TypeError("serviceRequest() requires options"); | ||
} | ||
if (!Array.isArray(options.authTokens)) { | ||
throw new TypeError("serviceRequest() requires options.authTokens array"); | ||
} | ||
if (typeof(options.coronerdCallback) !== 'function') { | ||
throw new TypeError("serviceRequest() requires options.coronerdCallback"); | ||
} | ||
function serviceRequest(svcOpts) { | ||
checkOptions(svcOpts, 'serviceRequest'); | ||
/* | ||
* Handle <prefix>/service requests. These require action and token QS. | ||
* The token serves as an authentication mechanism. | ||
* Handle <prefix>/service requests. These require action QS. Other QS | ||
* are defined per action. | ||
*/ | ||
@@ -46,14 +84,8 @@ return [ bodyParser.json(), (req, res, next) => { | ||
if (!req_obj.action) { | ||
res.status(400).send({status: "error", error: "missing action parameter"}); | ||
return; | ||
} | ||
if (!req_obj.token) { | ||
res.status(400).send({status: "error", error: "missing token parameter"}); | ||
return; | ||
} | ||
if (!req_obj.action) | ||
return missingParam(svcOpts, req, res, "action"); | ||
switch (req_obj.action) { | ||
case "register": | ||
return apiServiceRegister(options, req_obj, res); | ||
return apiServiceRegister(svcOpts, req, req_obj, res); | ||
default: | ||
@@ -69,6 +101,76 @@ break; | ||
/* | ||
* Generate a middleware for Express.js, which authenticates a request given | ||
* its URL and session token, provided by an earlier callback in | ||
* req.coronerAuth. The parameters required are: | ||
* | ||
* - url: the full URL to the coronerd (https://foo.sp.backtrace.io/) | ||
* - token: the user's session token (aka X-Coroner-Token) | ||
* | ||
* Upon success, the configuration data will be in req.coronerAuth.cfg. | ||
*/ | ||
function authenticateRequest(svcOpts) { | ||
checkOptions(svcOpts, 'authenticateRequest'); | ||
return [ (req, res, next) => { | ||
if (!req.coronerAuth) | ||
return missingInternalParam(svcOpts, req, res, "req.coronerAuth"); | ||
if (!req.coronerAuth.url) | ||
return missingInternalParam(svcOpts, req, res, "req.coronerAuth.url"); | ||
if (!req.coronerAuth.token) | ||
return missingInternalParam(svcOpts, req, res, "req.coronerAuth.token"); | ||
const req_opts = { | ||
url: `${req.coronerAuth.url}/api/config`, | ||
headers: { | ||
'X-Service-Name': svcOpts.name, | ||
'X-Coroner-Token': req.coronerAuth.token, | ||
'X-Service-HMAC': genHmac(svcOpts.secret, req.coronerAuth.token), | ||
} | ||
}; | ||
request(req_opts, (error, response, body) => { | ||
if (error) { | ||
next(error); | ||
return; | ||
} | ||
if (response.statusCode !== 200) { | ||
next(new Error(`Invalid server response code ${response.statusCode}`)); | ||
return; | ||
} | ||
/* Verify the coronerd knows the service secret. */ | ||
const coronerd_nonce = response.headers['x-service-nonce']; | ||
const coronerd_hmac = response.headers['x-service-hmac']; | ||
if (!coronerd_nonce || !coronerd_hmac) { | ||
next(new Error("Invalid server response, missing headers")); | ||
return; | ||
} | ||
if (!checkHmac(svcOpts.secret, coronerd_nonce, coronerd_hmac)) { | ||
next(new Error("Invalid server generated HMAC")); | ||
return; | ||
} | ||
/* Success, parse the response body and forward result to next. */ | ||
var bodyj; | ||
try { | ||
bodyj = JSON.parse(body); | ||
} catch (e) { | ||
next(new Error("auth parse failure")); | ||
} | ||
req.coronerAuth.cfg = bodyj; | ||
if (!bodyj.user) { | ||
const error = bodyj.error ? bodyj.error : new Error('auth failure'); | ||
next(error); | ||
} else { | ||
next(); | ||
} | ||
}); | ||
} ]; | ||
} | ||
module.exports = { | ||
serviceRequest: serviceRequest, | ||
authenticateRequest: authenticateRequest, | ||
}; | ||
// vim:ts=2:sw=2:et |
{ | ||
"name": "backtrace-service", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"description": "Backtrace Service middleware", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -10,5 +10,5 @@ # Backtrace Service Layer nodejs library | ||
const btservice = require('backtrace-service'); | ||
const atokens = ["asdfghjk"]; | ||
app.post("${url_prefix}/service", btservice.serviceRequest({ | ||
authTokens: atokens, | ||
name: 'simple', | ||
secret: 'asdfghjk', | ||
coronerdCallback: (url) => { | ||
@@ -20,7 +20,8 @@ console.log('heard from coronerd at: ' + url); | ||
## Options for serviceRequest | ||
### Options for serviceRequest | ||
The following options are accepted as the sole argument for the call: | ||
* authTokens: The list of authentication tokens to accept from coronerd | ||
instances attempting to register themselves with the service. | ||
* name: Name of the service (usually its type). | ||
* secret: The shared secret that the service will use to authenticate | ||
incoming requests and config replies from a coronerd. | ||
* coronerdCallback: A callback that takes an URL parameter, and performs any | ||
@@ -43,1 +44,28 @@ service specific setup associated with integrating a new coronerd instance. | ||
`/api/config`, to reach that resource on the coronerd instance. | ||
## authenticateRequest usage | ||
This function is intended as an additional middleware which may be used in | ||
application routes to validate requests that involve a session token issued | ||
by a remote coronerd. The actual call can reuse the same options argument | ||
used for `serviceRequest`, although it does not use `coronerdCallback`. | ||
In the route middleware list, prior to `authenticateRequest`, the | ||
application must attach a `req.coronerAuth` object which contains: | ||
* url: The full URL to the remote coronerd instance. | ||
* token: The user's session token to be validated. | ||
For example: | ||
``` | ||
req.coronerAuth = { | ||
url: "https://backtrace.sp.backtrace.io/", | ||
token: "f5af46b8eb32adb860ef46a9e714cfde", | ||
} | ||
``` | ||
This normalized form is used due to the fact that different services take | ||
these parameters from clients in different ways. | ||
Middlewares that come after `authenticateRequest` will have access to the | ||
validated coronerd `/api/config` response in `req.coronerAuth.cfg`. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
42336
1021
69