Server uses TLS, sign all requests to cloudapi, and enable SSO.

This commit is contained in:
Marsell Kukuljevic 2021-01-26 17:22:37 +01:00
parent 9abb845eec
commit 6aea7758ee
6 changed files with 335 additions and 31 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
node_modules
cfg/key.pem
cfg/cert.pem

View File

@ -0,0 +1,45 @@
# Installation
npm install
# Generate keys
From within the config/ directory:
openssl genrsa -out key.pem
openssl req -new -key key.pem -out csr.pem
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
rm csr.pem
# Configuration
Ensure the config file in config/ matches your details.
# Running the server
node bin/server.js config/prod.json
# Endpoints
## GET /static/*
This is where all the front-end code goes. All files will be served as-is as
found in that directory. The default is static/index.html. There is no
authentication; all files are public.
## GET /login
Call this endpoint to begin the login cycle. It will redirect you to the SSO
login page.
## GET /token
Upon successful login, the SSO login page will redirect to this endpoint. This
endpoint will return a 204, along with a X-Auth-Token header that must be saved
by the front-end code. All subsequent calls should provide this X-Auth-Token
header.
## Other
All other calls will be passed through to cloudapi. For these calls to succeed,
they MUST provide the X-Auth-Token header that the /token endpoint returns.

View File

@ -1,25 +1,26 @@
'use strict';
// Copyright 2021 Spearhead Systems S.R.L.
const mod_restify = require('restify');
const mod_cueball = require('cueball');
const mod_crypto = require('crypto');
const mod_fs = require('fs');
const mod_sdcauth = require('smartdc-auth');
const cloudapi_client = mod_restify.createStringClient({
url: 'https://eu-ro-1.api.spearhead.cloud',
agent: new mod_cueball.HttpsAgent({
spares: 4, maximum: 10,
recovery: {
default: {
timeout: 2000,
retries: 5,
delay: 250,
maxDelay: 1000
}
}
})
});
// Globals that are assigned to by main(). They are used by proxy() and login().
let CONFIG = {};
let PRIVATE_KEY = '';
let CLOUDAPI = {};
let SIGNER = {};
// Take any HTTP request that has a token, sign that request with an
// HTTP-Signature header, and pass it along to cloudapi. Return any response
// from cloudapi to our client caller. Effectively this function is a proxy
// that solely signs the request as it passes through.
function proxy(req, res, cb) {
// return data from cloudapi to the client caller
function proxyReturn(err, _, res2, data) {
if (err && !res2.statusCode) {
res.send(500);
@ -34,38 +35,137 @@ function proxy(req, res, cb) {
return cb();
}
const opts = {
path: req.url,
headers: req.headers
};
switch (req.method) {
case 'GET': cloudapi_client.get(opts, proxyReturn); break;
case 'DEL': cloudapi_client.del(opts, proxyReturn); break;
case 'HEAD': cloudapi_client.head(opts, proxyReturn); break;
case 'POST': cloudapi_client.post(opts, req.body, proxyReturn); break;
case 'PUT': cloudapi_client.del(opts, req.body, proxyReturn); break;
// check the X-Auth-Token is present
if (req.header('X-Auth-Token') == undefined) {
res.send({"Error": "X-Auth-Token header missing"});
res.send(401);
return cb();
}
// sign the request before forwarding to cloudapi
let headers = req.headers;
var rs = mod_sdcauth.requestSigner({ sign: SIGNER });
headers.date = rs.writeDateHeader();
rs.writeTarget(req.method, req.url);
rs.sign(function signedCb(err, authz) {
if (err) {
return (cb(err));
}
headers.authorization = authz;
const opts = {
path: req.url,
headers: headers
};
// make the call to cloudapi
switch (req.method) {
case 'GET': CLOUDAPI.get(opts, proxyReturn); break;
case 'DEL': CLOUDAPI.del(opts, proxyReturn); break;
case 'HEAD': CLOUDAPI.head(opts, proxyReturn); break;
case 'POST': CLOUDAPI.post(opts, req.body, proxyReturn); break;
case 'PUT': CLOUDAPI.del(opts, req.body, proxyReturn); break;
}
});
}
// Redirect to SSO with (signed) details that the SSO will need to generate a
// secure token. Once the user successfully logs in, the token is returned
// through an SSO redirect to token() below.
function login(req, res, cb) {
res.send('login');
const query = {
permissions: '{"cloudapi":["/my/*"]}',
returnto: CONFIG.urls.local + '/token',
now: new Date().toUTCString(),
keyid: '/' + CONFIG.key.user + '/keys/' + CONFIG.key.id,
nonce: mod_crypto.randomBytes(15).toString('base64')
};
// the query args MUST be sorted for SSO to validate
const querystr = Object.keys(query).sort().map(function encode(key) {
return key + '=' + encodeURIComponent(query[key]);
}).join('&');
let url = CONFIG.urls.sso + '/login?' + querystr;
const signer = mod_crypto.createSign('sha256');
signer.update(encodeURIComponent(url));
const signature = signer.sign(PRIVATE_KEY, 'base64');
url += '&sig=' + encodeURIComponent(signature);
res.redirect(url, cb);
}
// Once a user successfully logs in, they are redirected to here. We convert
// the token that was returned to use as query arg into an X-Auth-Token header
// that is returned to the client caller. This header must be provided by the
// client from now on in order to communicate with Cloudapi.
function token(req, res, cb) {
const token = decodeURIComponent(req.query().split('=')[1]);
res.header('X-Auth-Token', token);
res.send(204);
return cb();
}
// Start up HTTP server and pool of cloudapi clients.
//
// Read from config file, establish crypto singer needed for requests to
// cloudapi, prepare pool of HTTP clients for communication to cloudapi, and
// start up HTTP server.
function main() {
const server = mod_restify.createServer();
// load config and private key
const configStr = mod_fs.readFileSync(process.argv[2]);
CONFIG = JSON.parse(configStr);
PRIVATE_KEY = mod_fs.readFileSync(CONFIG.key.path);
// signer is used for signing requests made to cloudapi with HTTP-Signature
SIGNER = mod_sdcauth.privateKeySigner({
key: PRIVATE_KEY,
keyId: CONFIG.key.id,
user: CONFIG.key.user
});
// enable pool of clients to cloudapi
CLOUDAPI = mod_restify.createStringClient({
url: CONFIG.urls.cloudapi,
agent: new mod_cueball.HttpsAgent({
spares: 4,
maximum: 10,
recovery: {
default: {
timeout: 2000,
retries: 5,
delay: 250,
maxDelay: 1000
}
}
})
});
// prepare HTTP server
const options = {
key: mod_fs.readFileSync(CONFIG.server.key),
cert: mod_fs.readFileSync(CONFIG.server.cert)
};
const server = mod_restify.createServer(options);
server.use(mod_restify.authorizationParser());
server.use(mod_restify.bodyParser());
// where to server static content from
server.get(/^\/static.*/, mod_restify.plugins.serveStatic({
directory: 'static',
default: 'index.html'
}));
server.post('/login', login);
// route HTTP requests to proper functions
server.get('/login', login);
server.get('/token', token);
server.get(/^/, proxy);
server.put(/^/, proxy);
@ -73,7 +173,8 @@ function main() {
server.post(/^/, proxy);
server.head(/^/, proxy);
server.listen(8080, function () {
// enable HTTP server
server.listen(CONFIG.server.port, function listening() {
console.log('%s listening at %s', server.name, server.url);
});
}

19
cfg/example.json Normal file
View File

@ -0,0 +1,19 @@
{
"server": {
"//port": "if using a non-default port, make sure urls.local matches",
"port": 443,
"key": "cfg/key.pem",
"cert": "cfg/cert.pem"
},
"urls": {
"local": "https://localhost",
"sso": "https://sso.spearhead.cloud",
"cloudapi": "https://eu-ro-1.api.spearhead.cloud"
},
"key": {
"user": "developer_name",
"//id": "replace id below with the signature of the path'd key",
"id": "0b:bf:00:e7:95:7b:e8:13:54:7f:37:00:04:07:64:04",
"path": "/path/to/developer_key"
}
}

136
package-lock.json generated
View File

@ -102,6 +102,11 @@
"safe-json-stringify": "1.2.0"
}
},
"clone": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/clone/-/clone-0.1.5.tgz",
"integrity": "sha1-RvKRQ9B2bWY9vX+At1IKFXg9IEI="
},
"cmdutil": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cmdutil/-/cmdutil-1.1.0.tgz",
@ -538,6 +543,27 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz",
"integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.4",
"isarray": "1.0.0",
"process-nextick-args": "2.0.1",
"safe-buffer": "5.1.2",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
},
"dependencies": {
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"restify": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/restify/-/restify-4.3.4.tgz",
@ -748,6 +774,102 @@
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
"integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo="
},
"smartdc-auth": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/smartdc-auth/-/smartdc-auth-2.5.7.tgz",
"integrity": "sha1-QtRXEOeR3rkt+RMmyO7RvVqELLY=",
"requires": {
"assert-plus": "1.0.0",
"bunyan": "1.8.12",
"clone": "0.1.5",
"dashdash": "1.10.1",
"http-signature": "1.3.5",
"once": "1.3.0",
"sshpk": "1.16.1",
"sshpk-agent": "1.8.1",
"vasync": "1.4.3"
},
"dependencies": {
"bunyan": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "0.8.8",
"moment": "2.29.1",
"mv": "2.1.1",
"safe-json-stringify": "1.2.0"
}
},
"dashdash": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.10.1.tgz",
"integrity": "sha1-Cr8a+JqPUSmoHxjCs1sh3yJiL2A=",
"requires": {
"assert-plus": "0.1.5"
},
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
},
"extsprintf": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.0.tgz",
"integrity": "sha1-TVi4Fazlvr/E6/A8+YsKdgSpm4Y="
},
"json-schema": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz",
"integrity": "sha1-UDVPGfYDkXxpX3C4Wvp3w7DyNQY="
},
"jsprim": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-0.3.0.tgz",
"integrity": "sha1-zRNGbqJIDb2DlqVw1H0x3aR2+LE=",
"requires": {
"extsprintf": "1.0.0",
"json-schema": "0.2.2",
"verror": "1.3.3"
},
"dependencies": {
"verror": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.3.3.tgz",
"integrity": "sha1-impKw6jHdLb2h/7OSb3/14VS4s0=",
"requires": {
"extsprintf": "1.0.0"
}
}
}
},
"once": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.3.0.tgz",
"integrity": "sha1-FRr4a/wfCMS58H0GqyUP/L61ZYE="
},
"vasync": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.4.3.tgz",
"integrity": "sha1-yG1S4rcWE9Ke7fFZ8xNdvnSc7jc=",
"requires": {
"jsprim": "0.3.0",
"verror": "1.1.0"
}
},
"verror": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.1.0.tgz",
"integrity": "sha1-KktOsUogcFHnWm+U7lExW/FzobA=",
"requires": {
"extsprintf": "1.0.0"
}
}
}
},
"sshpk": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz",
@ -764,6 +886,20 @@
"tweetnacl": "0.14.5"
}
},
"sshpk-agent": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sshpk-agent/-/sshpk-agent-1.8.1.tgz",
"integrity": "sha512-YzAzemVrXEf1OeZUpveXLeYUT5VVw/I5gxLeyzq1aMS3pRvFvCeaGliNFjKR3VKtGXRqF9WamqKwYadIG6vStQ==",
"requires": {
"assert-plus": "1.0.0",
"dashdash": "1.14.1",
"getpass": "0.1.7",
"mooremachine": "2.3.0",
"readable-stream": "2.3.7",
"sshpk": "1.16.1",
"verror": "1.10.0"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",

View File

@ -8,6 +8,7 @@
"cueball": "2.10.1",
"http-signature": "1.3.5",
"restify": "4.3.4",
"vasync": "2.2.0"
"vasync": "2.2.0",
"smartdc-auth": "2.5.7"
}
}