Server uses TLS, sign all requests to cloudapi, and enable SSO.
This commit is contained in:
parent
9abb845eec
commit
6aea7758ee
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
||||
node_modules
|
||||
cfg/key.pem
|
||||
cfg/cert.pem
|
||||
|
45
README.md
45
README.md
@ -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.
|
161
bin/server.js
161
bin/server.js
@ -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
19
cfg/example.json
Normal 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
136
package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user