authorify-client
Version:
Client for Authorify authorization and authentication system for REST server
1,519 lines (1,471 loc) • 49.7 kB
JavaScript
/**
* A client (for node and browser) for {@link https://www.npmjs.org/package/authorify Authorify}
* authorization and authentication system for REST server.
*
*
* @class node_modules.authorify_client
*
* @author Marcello Gesmundo
*
* ## Usage
*
* You can use `authorify-client` both in node and in browser environment.
*
* #### Node
*
* This client has the same approach of [superagent][1] and you can use it as shown below:
*
* // dependencies
* var fs = require('fs'),
* authorify = require('authorify-client')({
* host: 'localhost',
* debug: true,
* key: fs.readFileSync('clientCert.key'), 'utf8'),
* cert: fs.readFileSync('clientCert.cer'), 'utf8'),
* ca: fs.readFileSync('serverCA.cer'), 'utf8')
* }),
* uuid = require('node-uuid'),
* id = uuid.v4(),
* app = uuid.v4();
*
* // use a configuration
* authorify.set({
* host: 'localhost', // host of your server
* port: 3000, // port of your server
* id: id, // a valid uuid
* app: app // another valid uuid
* });
*
* // login
* authorify.login('username', 'password', function(err, res) {
* authorify.post('/test')
* // send a message into the body
* .send({ name: 'alex', surname: 'smith' })
* .end(function(err, res) {
* if (!err) {
* // your logic here
* }
* });
* });
*
* #### Browser
*
* To create a single file to use in browser environment use a simple script that uses `browserify`:
*
* $ ./build.sh
*
* and add the obtained file to your `html` file:
*
* <!DOCTYPE html>
* <html>
* <head>
* <meta charset="utf-8">
* <title>authorify-client example</title>
* </head>
* <body>
* <script src="authorify.js"></script>
* <script src="example.js"></script>
* </body>
* </html>
*
* The script `example.js` contanins your example code:
*
* // you have a global authorify variable
* authorify.set({
* host: 'localhost', // host of your server
* port: 3000, // port of your server
* id: 'ae92d22b-a9ab-458a-9850-0025dbf11fad', // a valid uuid
* app: 'c983659a-9572-4471-a3a2-7d45b591d315' // another valid uuid
* });
*
* // login
* authorify.login('username', 'password', function(err, res) {
* authorify.post('/test')
* // send a message into the body
* .send({ name: 'alex', surname: 'smith' }))
* .end(function(err, res) {
* if (!err) {
* // your logic here
* }
* });
* });
*
* See [Authorify][2] `test/browser` folder to see more examples.
*
* [1]: https://www.npmjs.org/package/superagent
* [2]: https://www.npmjs.org/package/authorify
*
* # License
*
* Copyright (c) 2012-2014 Yoovant by Marcello Gesmundo. All rights reserved.
*
* This program is released under a GNU Affero General Public License version 3 or above, which in summary means:
*
* - You __can use__ this program for __no cost__.
* - You __can use__ this program for __both personal and commercial reasons__.
* - You __do not have to share your own program's code__ which uses this program.
* - You __have to share modifications__ (e.g bug-fixes) you've made to this program.
*
* For more convoluted language, see the LICENSE file.
*
*/
module.exports = function(app) {
'use strict';
//TODO: create a plugin to ban the IP with an high number of failing requests (add as last middleware)
// dependencies
var _ = app._,
mixin = app.mixin,
agent = app.superagent,
Class = app.jsface.Class,
Store = app.class.Store,
Header = app.class.Header,
Handshake = app.class.Handshake,
Authentication = app.class.Authentication,
Authorization = app.class.Authorization,
util = app.util,
async = app.async,
log = app.logger,
sessionStore = app.sessionStore,
config = app.config,
debug = config.debug,
authHeaderKey = config.authHeader,
errors = app.errors;
var CError = errors.InternalError,
wsPlugin;
errors.set({
format: function(e, mode) {
mode = mode || 'msg';
if (mode === 'msg') {
return util.format('%s %s', app.name, e.message);
}
return util.format('%s %s', app.name, e.body);
}
});
// namespace
var my = {
'class' : app.class,
'helper' : app.helper,
'mixin' : app.mixin,
'config' : {}, // the active configuration
'configs': {}, // all available configurations
'plugin' : {} // all loaded plugins
};
var defaultConfigName = 'config';
/**
* Add a name field if missing and if opts is an object.
* Make a new object with name field (set to opts) if opts is a string.
*
* @param {Object/String} opts The options object or string with the required name
* @returns {Object} The configuration
* @private
* @ignore
*/
function formatOptions(opts) {
if (opts) {
if (_.isObject(opts)) {
opts.name = opts.name || defaultConfigName;
} else if ('string' === typeof opts) {
opts = {
name: opts
};
}
} else {
opts = {
name: defaultConfigName
};
}
return opts;
}
/**
* Get all options with default values if empty
*
* @param {Object/String} opts The options object or string with the required name
* @returns {Object} The configuration with default values
* @private
* @ignore
*/
function getConfigOptions(opts) {
opts = formatOptions(opts);
_.forEach(config, function(value, key) {
opts[key] = opts[key] || value;
});
opts.headers = opts.headers || {};
return opts;
}
/**
* Make an object with all params
*
* @param {String} method The required method
* @param {String} path The required path
* @param {String} [transport = 'http'] The transport protocol ('http' or 'ws')
* @param {Boolean} [plain = false] The required plain
* @param {Function} callback The required callback
* @return {Object} An object with all params as properties
* @private
* @ignore
*/
function fixRequestOptions(method, path, transport, plain, callback) {
var opts = {
method: method,
path: path
};
if (_.isFunction(transport)) {
opts.callback = transport;
opts.plain = false;
opts.transport = 'http';
} else {
if (_.isString(transport)) {
opts.transport = transport;
} else if (_.isBoolean(transport)) {
opts.plain = transport;
}
if (_.isFunction(plain)) {
opts.callback = plain;
opts.plain = opts.plain || false;
} else if (_.isBoolean(plain)) {
opts.callback = callback;
opts.plain = plain;
}
}
return opts;
}
/**
* Get the authorify-websocket plugin instance if loaded
* @return {Object}
* @private
* @ignore
*/
function getWebsocketPlugin() {
if (!wsPlugin) {
wsPlugin = _.findWhere(app.plugin, { name: 'authorify-websocket' });
}
return wsPlugin;
}
/**
* Log the response
*
* @param {Object} err The error if occurred
* @param {Object} res The response
* @private
* @ignore
*/
function logResponse(err, res) {
if (err || (res && !res.ok)) {
if (err) {
log.warn('%s on response -> read plaintext body due an error (%s)', app.name, err.message);
} else {
log.warn('%s on response -> read plaintext body due an error (%s)', app.name, res.error);
}
} else if (res && !_.isEmpty(res.body)) {
if (res.body[my.config.encryptedBodyName]) {
log.info('%s on response -> read encrypted body', app.name);
} else {
log.info('%s on response -> read plaintext body', app.name);
}
}
}
/**
* Formats the name of the module loaded into the browser
*
* @param module {String} The original module name
* @return {String} Name of the module
* @private
* @ignore
*/
function getModuleName(module) {
var script = module.replace(/[^a-zA-Z0-9]/g, '.'),
parts = script.split('.'),
name = parts.shift();
if (parts.length) {
for (var p in parts) {
name += parts[p].charAt(0).toUpperCase() + parts[p].substr(1, parts[p].length);
}
}
return name;
}
var Crypter = Class(mixin.WithCrypto, {
constructor: function(opts) {
opts = opts || {};
this.setKey(opts.key);
this.setCert(opts.cert);
this.setCa(opts.ca);
}
});
var Config = Class({
constructor: function(initConfig) {
var self = this;
initConfig = getConfigOptions(initConfig);
_.forEach(initConfig, function(value, key) {
self[key] = value;
});
this.urlBase = util.format('%s://%s:%s', this.protocol, this.host, this.port);
this.crypter = new Crypter({
key : this.key,
cert: this.cert,
ca : this.ca
});
}
});
// TODO: add option to use the last successful protocol used. Add also a timer to restore the default order; if the default first protocol fails again, set a new long timer (like primus reconnection strategy)
var Client = Class({
/**
* The constructor
*
* @param {Object} [opts] The construction options
* @param {Object} opts.name The name of the required configuration
* @param {Object} opts.request The request object
* @param {String} opts.path The required route
* @param {String} opts.method = 'GET' The http verb
* @param {Boolean} opts.plain = false True if the request body is in plaintext
* @param {String} opts.transport = 'http' The transport protocol ('http' or 'ws')
* @constructor
* @ignore
*/
constructor: function(opts) {
opts = formatOptions(opts);
my.setConfig(opts.name);
var cfg = my.config,
self = this;
_.forEach(cfg, function(value, key) {
self[key] = value;
});
if (opts.path) {
opts.path = opts.path.urlSanify();
}
this.request = opts.request;
this.path = opts.path;
this.method = opts.method || 'GET';
this.plain = opts.plain || false;
},
/**
* Create a new request
*
* @param {Header} header The header instance
* @param {String} path The required route
* @param {String} [method = 'GET'] The http verb
* @param {String} [transport = 'http'] The transport protocol ('http' or 'ws')
* @private
*/
composeRequest: function(header, path, method, transport) {
transport = transport || 'http';
if (transport !== 'http' && transport !== 'ws') {
throw new CError('unknown transport').log();
}
var plain = true,
self = this;
method = method || 'GET';
// prepare request
if (!this.request || this.path !== path || this.method !== method) {
var url = util.format('%s%s', this.urlBase, path);
if (transport === 'http') {
this.request = agent(method, url);
// set timeout for request
if (this.requestTimeout && this.requestTimeout > 0) {
this.request.timeout(this.requestTimeout);
}
// set content type
this.request.type('json');
} else {
var ws = getWebsocketPlugin();
if (!ws) {
throw new CError('websocket plugin not loaded');
}
this.request = ws.wsclient.primusagent(method, url, {
timeout: this.requestTimeout
});
}
}
if (!this.request) {
throw new CError('no request available').log();
}
// set headers
if (header) {
this.headers[authHeaderKey] = header.encode();
_.each(this.headers, function(value, key) {
self.request.set(key, value);
});
plain = (header.getMode() === 'auth-plain');
}
// compose query if required
if (this._pendingQuery) {
_.forEach(this._pendingQuery, function(value) {
if (plain || self._noAuthHeader) {
if (!_.isObject(value)) {
throw new CError('wrong query format').log();
}
self.request.query(value);
} else {
// encrypt query in Base64url
if (!_.isObject(value)) {
throw new CError('wrong query format').log();
}
_.forEach(value, function(item, property) {
value[property] = header.encoder.encryptAes(item.toString(), header.getSecret(), 'url');
});
self.request.query(value);
}
});
}
// compose body if required
if (this._pendingContent) {
var content = {},
_content = {};
if (plain || this._noAuthHeader) {
_.forEach(this._pendingContent, function(value) {
if (!_.isObject(value)) {
throw new CError('wrong body format').log();
}
self.request.send(value);
});
log.info('%s on request -> write plaintext body', app.name);
} else {
if (!header) {
throw new CError('missing header').log();
}
_.forEach(this._pendingContent, function(value) {
if (!_.isObject(value)) {
throw new CError('wrong body format').log();
}
_.extend(_content, value);
});
content[my.config.encryptedBodyName] = header.cryptContent(_content);
// sign the body
if (my.config.signBody) {
content[my.config.encryptedSignatureName] = header.generateSignature(_content);
}
log.info('%s on request -> write encrypted body', app.name);
self.request.send(content);
}
}
},
/**
* @inheritdoc #handshake
*/
handshake: function(callback) {
var handshake = new Handshake({
key: my.config.key,
cert: my.config.cert,
encoderCert: my.config.cert
});
handshake.setToken(handshake.generateToken());
this.composeRequest(handshake, this.handshakePath, 'GET', 'http');
// perform request and process response
this.request.end(function(err, res) {
logResponse(err, res);
if (err) {
callback(err, res);
} else if (!res.ok) {
callback(null, res);
} else {
my.processHeader(res, function(err, result) {
// save the received certificate
if (!err) {
var session = {
sid: result.parsedHeader.payload.sid,
cert: result.parsedHeader.payload.cert
};
sessionStore.save(my.config.urlBase, session, function(err) {
if (!err) {
log.debug('%s saved session', app.name);
log.debug(session);
callback(null, result);
} else {
log.error('%s %s', app.name, err);
callback(err, result);
}
});
} else {
log.error('%s %s', app.name, err);
callback(err, result);
}
});
}
});
return this;
},
/**
* @inheritdoc #authenticate
*/
authenticate: function(username, password, callback) {
var self = this,
err;
if (!this.id || !this.app || !username || !password) {
err = 'missing required fields for authentication';
log.error('%s %s', app.name, err);
callback(err);
} else {
sessionStore.load(my.config.urlBase, function(loadErr, session) {
if (loadErr) {
log.error('%s %s', app.name, loadErr);
callback(loadErr);
} else if (session){
var authentication = new Authentication({
key: my.config.key,
cert: my.config.cert,
encoderCert: session.cert,
sid: session.sid,
secret: my.generateSecret(),
id: self.id,
app: self.app,
username: username,
password: password
});
authentication.setToken(authentication.generateToken());
self.composeRequest(authentication, self.authPath, 'GET', 'http');
// perform request and process response
self.request.end(function(err, res) {
logResponse(err, res);
if (err) {
callback(err, res);
} else if (!res.ok) {
callback(null, res);
} else {
my.processHeader(res, function(procErr, result) {
if (!procErr) {
// save token into the session
// NOTE: the sid is changed
session.sid = result.parsedHeader.payload.sid;
session.token = result.parsedHeader.content.token;
sessionStore.save(my.config.urlBase, session, function(saveErr) {
if (!saveErr) {
log.debug('%s saved session', app.name);
log.debug(session);
callback(err, result);
} else {
err = 'save session error';
log.error('%s %s', app.name, err);
callback(err);
}
});
} else {
callback(procErr);
}
});
}
});
} else {
err = 'session not found';
log.error('%s %s', app.name, err);
callback(err);
}
});
}
return this;
},
/**
* Perform a request
*
* @param {String} transport = 'http' The transport protocol ('http' or 'ws')
* @param {String} method = 'GET' The http verb
* @param {String} path The required route
* @param {Function} callback The callback: next(err, res)
* @param {Error/String} callback.err The error if occurred
* @param {Object} callback.res The response received from the server
* @private
*/
doConnect: function (transport, method, path, callback) {
if (transport === 'http' || transport === 'ws') {
log.debug('%s perform a %s request', app.name, (transport === 'ws' ? 'websocket' : 'http'));
this.composeRequest(null, path, method, transport);
// perform request and process response
this.request.end(function (err, res) {
logResponse(err, res);
callback(err, res);
// if (err) {
// callback(err, res);
// } else if (!res.ok) {
// callback(res.error, res);
// } else {
// callback(null, res);
// }
});
} else {
var err = 'unknown transport';
log.error('%s %s %s', app.name, err, transport);
callback(err);
}
},
/**
* A request without Authorization header
*
* @inheritdoc #authorize
* @private
*/
connect: function(opts) {
opts = opts || {};
var path = opts.path || this.path,
callback = opts.callback,
method = opts.method || this.method,
ws = getWebsocketPlugin(),
transports = app.config.transports,
self = this,
i = 0,
error,
response;
if (ws) {
if (transports && transports.length > 0) {
async.whilst(
function () {
return (i < transports.length);
},
function (done) {
self.doConnect(transports[i], method, path, function (err, res) {
error = err;
response = res;
if (!err && res) {
i = transports.length;
} else {
i++;
if (i < transports.length) {
delete self.request;
}
}
done();
});
},
function (err) {
callback(err || error, response);
}
);
} else {
error = 'no transport available';
log.error('%s %s', app.name, error);
callback(error);
}
} else {
this.doConnect('http', method, path, callback);
}
return this;
},
/**
* Perform a request
*
* @param {String} transport = 'http' The transport protocol ('http' or 'ws')
* @param {String} method The required method
* @param {String} path The required path
* @param {Boolean} plain = false The required plain
* @param {Object} header The header for the request
* @param {Object} session The session
* @param {Function} callback The callback: next(err, res)
* @param {Error/String} callback.err The error if occurred
* @param {Object} callback.res The response received from the server
* @private
*/
doRequest:function (transport, method, path, plain, header, session, callback){
var self = this,
err;
if (transport === 'http' || transport === 'ws') {
log.debug('%s perform a %s request', app.name, (transport === 'ws' ? 'websocket' : 'http'));
// compose request
this.composeRequest(header, path, method, transport);
// perform request and process response
this.request.end(function(err, res) {
logResponse(err, res);
if (err) {
callback(err, res);
// } else if (!res.ok) {
// callback(null, res);
} else if (path === my.config.logoutPath) {
// destroy local session
sessionStore.destroy.call(sessionStore, my.config.urlBase);
// the logout response does not have header
callback(null, res);
} else {
my.processHeader(res, function(procErr, result) {
// save the token
if (!procErr) {
// save token into the session
session.token = result.parsedHeader.content.token;
sessionStore.save(my.config.urlBase, session, function(saveErr) {
if (!saveErr) {
log.debug('%s saved session', app.name);
log.debug(session);
if (!(plain || self._noAuthHeader || path === my.config.logoutPath)) {
// decrypt body if present
res.body = my.decryptBody(result, header);
}
callback(err, result);
} else {
err = 'save session error';
log.error('%s %s', app.name, err);
callback(err);
}
});
} else {
callback(procErr);
}
});
}
});
} else {
err = 'unknown transport';
log.error('%s %s %s', app.name, err, transport);
callback(err);
}
},
/**
* Perform a request
*
* @param {String} method The required method
* @param {String} path The required path
* @param {Boolean} plain = false The required plain
* @param {Object} header The header for the request
* @param {Object} session The session
* @param {Function} callback The callback: next(err, res)
* @param {Error/String} callback.err The error if occurred
* @param {Object} callback.res The response received from the server
* @private
*/
doAuthorize: function (method, path, plain, header, session, callback) {
var ws = getWebsocketPlugin(),
self = this,
transports = app.config.transports,
i = 0,
error,
response;
if (ws) {
if (transports && transports.length > 0) {
async.whilst(
function() {
return (i < transports.length);
},
function (done) {
self.doRequest(transports[i], method, path, plain, header, session, function (err, res) {
error = err;
response = res;
if (!err && res && res.ok) {
i = transports.length;
} else {
i++;
if (i < transports.length) {
delete self.request;
}
}
done();
});
},
function (err) {
callback(err || error, response);
}
);
} else {
error = 'no transport available';
log.error('%s %s', app.name, error);
callback(error);
}
} else {
this.doRequest('http', method, path, plain, header, session, callback);
}
},
/**
* @inheritdoc #authorize
*/
authorize: function(opts) {
if (this._noAuthHeader) {
this.connect(opts);
} else {
opts = opts || {};
var path = opts.path || this.path,
callback = opts.callback,
plain = (opts.plain || this.plain),
mode = (plain ? 'auth-plain' : 'auth'),
method = opts.method || this.method,
self = this;
sessionStore.load(my.config.urlBase, function(err, session) {
if (err) {
log.error('%s %s', app.name, err);
callback(err);
} else if (session){
// if the client handle a wrong session the request is made without header because
// the server was unable to destroy the relative session
if (path === my.config.logoutPath && (!session.token || !session.sid)) {
// destroy local session
sessionStore.destroy.call(sessionStore, my.config.urlBase);
// make a new request without authorization header
self._noAuthHeader = true;
self.connect(opts);
} else if (!session.token) {
err = 'missing authentication token';
log.error('%s %s', app.name, err);
callback(err);
} else if (!session.sid) {
err = 'missing sid';
log.error('%s %s', app.name, err);
callback(err);
} else {
var authorization = new Authorization({
mode: mode,
key: my.config.key,
cert: my.config.cert,
encoderCert: session.cert,
sid: session.sid,
secret: my.generateSecret(),
token: session.token
});
self.doAuthorize(method, path, plain, authorization, session, callback);
}
} else {
err = 'session not found';
log.error('%s %s', app.name, err);
callback(err);
}
});
}
return this;
},
/**
* Complete a request and get the response from the server
*
* @chainable
* @param {Function} callback The function executed after the server reply: callback(err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
end: function(callback) {
this.authorize({ callback: callback });
return this;
},
/**
* Compose a query-string
*
* ## Example
*
* To compose a query-string like "?format=json&data=here" in a GET request:
*
* var client = require('authorify-client')({
* // set your options
* });
* client
* .get('/someroute')
* .query({ format: 'json' })
* .query({ data: 'here' })
* .end(function(err, res){
* // your logic
* });
*
* @chainable
* @param {Object} value The object to compose the query
* @return {Client} The client instance
*/
query: function(value) {
if (value) {
if (config.encryptQuery) {
this._pendingQuery = this._pendingQuery || [];
this._pendingQuery.push(value);
} else {
this.request.query(value);
}
}
return this;
},
/**
* Add a body in a POST/PUT request
*
* ## Example
*
* var client = require('authorify-client')({
* // set your options
* });
* client
* .post('/user')
* .send({ name: 'alex', surname: 'smith' })
* .end(function(err, res){
* // your logic
* });
*
* @param {Object} value The object to compose the body
* @return {Client} The client instance
*/
send: function(value) {
if (value) {
this._pendingContent = this._pendingContent || [];
this._pendingContent.push(value);
}
return this;
},
/**
* Abort the current request
*
* @chainable
* @return {Client} The client instance
*/
abort: function() {
if (this.request) {
this.request.abort();
}
return this;
},
/**
* Do not add the Authorization header (for free routes)
*
* @chainable
* @return {Client} The client instance
*/
pass: function() {
this._noAuthHeader = true;
return this;
}
});
/**
* @inheritdoc node_modules.authorify_client.mixin.WithCrypto#generateSecret
* @private
*/
my.generateSecret = function() {
return Crypter.generateSecret();
};
/**
* Decrypt the body
*
* @param {ServerResponse} res The server response
* @param {Header} header The header
* @returns {Object} The decrypted body
* @private
*/
my.decryptBody = function(res, header) {
var _body;
if (res && res.body) {
if (res.parsedHeader && header) {
switch (res.parsedHeader.payload.mode) {
case 'auth':
if (_.isObject(res.body)) {
if (res.body[my.config.encryptedBodyName]) {
var _secretBackup = header.getSecret();
var secret = header.keychain.decryptRsa(res.parsedHeader.payload.secret);
// set secret of the server
header.setSecret(secret);
// decrypt the body
_body = header.decryptContent(res.body[my.config.encryptedBodyName]);
// verify the signature
if (my.config.signBody) {
var signature = res.body[my.config.encryptedSignatureName];
if (!signature) {
throw new CError('missing signature').log();
}
var signVerifier = new Crypter({
cert: res.session.cert
});
if (!signVerifier.verifySignature(JSON.stringify(_body), signature)) {
throw new CError('forgery message').log();
}
}
header.setSecret(_secretBackup);
} else {
_body = res.body;
}
} else {
throw new CError('wrong body format').log();
}
break;
default:
_body = res.body;
break;
}
} else {
throw new CError('missing response header').log();
}
}
return _body;
};
/**
* Parse the Authorization header and verify it
*
* @param {ServerResponse} res The server response
* @param {Function} next The callback: next(err, res)
* @param {String} next.err The error if occurred
* @param {ServerResponse} next.res The server response
* @private
*/
my.processHeader = function(res, next) {
var authHeader = res.headers[authHeaderKey.toLowerCase()] || '',
senderCert,
reqSid,
reqToken;
async.series([
// parse header
function(callback) {
if (authHeader.length === 0) {
callback('missing header');
} else {
res.parsedHeader = Header.parse(authHeader, my.config.key, my.config.cert);
senderCert = res.parsedHeader.payload.cert; // get the certificate in handshake
reqSid = res.parsedHeader.payload.sid;
reqToken = res.parsedHeader.content.token;
callback(null);
}
},
// get sender certificate using payload cert (in handshake phase) or payload sid
function(callback) {
if (res.parsedHeader.payload.mode === 'handshake') {
res.session = {
sid: reqSid,
cert: senderCert,
token: reqToken
};
callback(null);
} else {
var querySid;
if (sessionStore instanceof Store || 'undefined' !== typeof window) {
querySid = my.config.urlBase;
} else {
querySid = reqSid;
}
// load the session if exists
sessionStore.load(querySid, function(err, session){
if (err) {
callback(err);
} else if (session) {
log.debug('%s loaded session', app.name);
log.debug(session);
res.session = session;
callback(null);
} else {
callback('session expired');
}
});
}
},
// verify certificate authenticity
function(callback) {
if (res.session && res.session.cert) {
if (res.parsedHeader.payload.mode === 'handshake') {
if (my.config.crypter.verifyCertificate(res.session.cert)) {
callback(null);
} else {
callback('unknown certificate');
}
} else {
callback(null);
}
} else {
callback('wrong session');
}
},
// verify signature using sender certificate
function(callback) {
if (!res.parsedHeader.signature) {
callback('unsigned');
} else {
var signVerifier = new Crypter({
cert: res.session.cert
});
if (signVerifier.verifySignature(JSON.stringify(res.parsedHeader.content), res.parsedHeader.signature)) {
callback(null);
} else {
callback('forgery');
}
}
},
// verify the date
function(callback) {
if (parseInt(config.clockSkew, 10) > 0) {
var now = new Date().toSerialNumber(),
sent = res.parsedHeader.content.date;
if ((now - sent) > config.clockSkew * 1000) {
callback('date too old');
} else {
callback(null);
}
} else {
callback(null);
}
}
], function(error) {
if (error) {
log.error('%s %s', app.name, error);
}
next(error, res);
});
};
/**
* Get the config by name
*
* @param {String} name The name of the config
* @return {Config} The configuration
*/
my.getConfig = function(name) {
var cfg;
if (name && _.isString(name)) {
cfg = my.configs[name];
}
return cfg;
};
/**
* Delete a non active configuration
*
* @chainable
* @param {String} name The name of the configuration
* @return {Client} The client instance
*/
my.deleteConfig = function(name) {
var err;
if (name) {
if (name === my.config.name) {
throw new CError('unable to remove active configuration').log();
}
delete my.configs[name];
}
return this;
};
/**
* Create a local config based on default config.
*
* ## Example
*
* var client = require('authorify-client')({
* port: 3000
* // set other options
* });
* client
* .set({
* name: 'newconfig',
* port: 4000,
* headers: {
* 'custom-header-1': 'value',
* 'custom-header-2': 'value'
* }
* })
* .get('/someroute')
* .end(function(err, res){
* // your logic here
* });
*
* console.log(client.configs.newconfig.port); // it is 4000
* console.log(client.configs.config.port); // it is 3000
* console.log(client.config.port); // it is 4000 because newconfig is the active configuration
*
* // switch to 'config' configuration
* client.set('config');
*
* @chainable
* @param {Object/String} [opts] The init configuration options or the active config name using default values
* @param {String} opts.name = 'config' The name of the configuration to activate, create or update
* @param {String} opts.protocol The protocol for requests
* @param {String} opts.host The host of the server
* @param {Integer} opts.port The port of the server
* @param {String} opts.key The client private RSA key
* @param {String} opts.cert The client public X.509 cert
* @param {String} opts.ca The Certification Authority certificate
* @param {String} opts.handshakePath The route exposed by the server for the handshake phase
* @param {String} opts.authPath The route exposed by the server for the authentication/authorization phases
* @param {String} opts.logoutPath The route exposed by the server for the logout
* @param {String} opts.id The id (uuid) assigned to the client
* @param {String} opts.app The app (uuid) assigned to the application that the client want to use
* @param {Object} opts.headers Additional headers
* @param {Object} opts.encryptedBodyName The property name for the encrypted body value
* @param {Integer} opts.requestTimeout Timeout in milliseconds for requests
* @param {String} opts.SECRET The secret key used in hash operations
* @param {String} opts.SECRET_CLIENT The key used in conjunction with SECRET to verify handshake token
*/
my.setConfig = function(opts) {
opts = formatOptions(opts);
var name = opts.name,
cfg = my.getConfig(name);
if (cfg) {
delete opts.name;
if (_.isEmpty(opts)) {
my.config = cfg;
} else {
// delete older configuration
delete my.configs[name];
// create new configuration
opts.name = name;
my.createNewConfig(opts);
log.debug("%s updated configuration '%s'", app.name, name);
}
} else {
my.createNewConfig(opts);
log.debug("%s created new configuration '%s'", app.name, name);
}
return this;
};
/**
* Create a new configuration
*
* @param {Object} opts Config options
* @private
* @ignore
*/
my.createNewConfig = function(opts) {
opts = getConfigOptions(opts);
var name = opts.name;
if (!(my.configs[name])) {
my.configs[name] = new Config(opts);
}
my.config = my.configs[name];
};
/**
* Perform a handshake request
*
* @chainable
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.handshake = function(callback) {
var _client = new Client();
_client.handshake(callback);
return _client;
};
/**
* Perform an authentication request
*
* @chainable
* @param {String} username The username for interactive login
* @param {String} password The password for interactive login
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.authenticate = function(username, password, callback) {
var _client = new Client();
_client.authenticate(username, password, callback);
return _client;
};
/**
* Perform an authorize request
*
* @chainable
* @param {Object} opts The options for authorization
* @param {String} opts.path The required route
* @param {String} [opts.method = 'GET'] The http verb
* @param {Boolean} [opts.plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} opts.callback.err The error if occurred
* @param {ServerResponse} opts.callback.res The server response
* @return {Client} The client instance
*/
my.authorize = function(opts) {
var _client = new Client();
_client.authorize(opts);
return _client;
};
/**
* Create a new client agent to request a route
*
* @chainable
* @param {String} [method = 'GET'] The http verb
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
* @private
*/
my.createClient = function(method, path, plain, callback) {
var opts = fixRequestOptions(method, path, plain, callback);
_.extend(opts, my.config);
var _client = new Client(opts);
if (opts.callback) {
_client.end(opts.callback);
}
return _client;
};
/**
* Perform a login action (handshake + authentication). Note that the required 'id' and 'app' are
* defined into the active configuration.
*
* @chainable
* @param {String} username The username for interactive login
* @param {String} password The password for interactive login
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.login = function( username, password, callback) {
var _client = my.handshake(function(err, res) {
if (!err) {
_client.authenticate(username, password, callback);
} else {
callback(err, res);
}
});
return _client;
};
/**
* Perform a logout request
*
* @chainable
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.logout = function(callback) {
if (!callback) {
callback = function() {};
}
return my.get(my.config.logoutPath, callback);
};
/**
* Destroy the session for the required or active configuration.
*
* @param {String} name The config name
* @return {Client} The client instance
* @chainable
*/
my.destroySession = function(name) {
name = name || defaultConfigName;
sessionStore.destroy(my.configs[name].urlBase);
return this;
};
/**
* Perform a GET request
*
* @method get
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.get = function(path, plain, callback) {
return my.createClient('GET', path, plain, callback);
};
/**
* Perform a POST request
*
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.post = function(path, plain, callback) {
return my.createClient('POST', path, plain, callback);
};
/**
* Perform a PUT request
*
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.put = function(path, plain, callback) {
return my.createClient('PUT', path, plain, callback);
};
/**
* Perform a DELETE request
*
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.del = function(path, plain, callback) {
return my.createClient('DELETE', path, plain, callback);
};
/**
* Perform a HEAD request
*
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.head = function(path, plain, callback) {
return my.createClient('HEAD', path, plain, callback);
};
/**
* Perform a PATCH request
*
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.patch = function(path, plain, callback) {
return my.createClient('PATCH', path, plain, callback);
};
/**
* Perform a OPTIONS request
*
* @method options
* @chainable
* @param {String} path The required route
* @param {Boolean} [plain = false] True if the request body is in plaintext
* @param {Function} callback The function executed after the server reply: callback (err, res)
* @param {String} callback.err The error if occurred
* @param {ServerResponse} callback.res The server response
* @return {Client} The client instance
*/
my.options = function(path, plain, callback) {
return my.createClient('OPTIONS', path, plain, callback);
};
/**
* @method opts
* @chainable
* @inheritdoc #options
*/
my.opts = my.options;
/**
* Load a plugin module to add some functionality.
*
* ## Example
*
* var authorify = require('authorify-client')({
* // add your options
* });
* authorify.load('pluginname', 'shortname', opts); // opts is an optional object to configure the plugin
* var loadedPlugin = authorify.plugin['shortname'];
* // below you can use all methods/properties exported by the plugin (loadedPlugin)
*
* @param {String} name The name of the plugin. THe plugin must be installed into the
* application folder that uses the authorify module.
* @param {String} [shortname] An optional short name for the plugin loaded as property in the root application
* (authorify.plugin['shortname']).
* @param {Object} [opts] The options required by the plugin.
*/
my.load = function(name, shortname, opts) {
if (_.isObject(shortname)) {
opts = shortname;
shortname = name;
} else if (!shortname) {
shortname = name;
}
opts = opts || {};
var plugin;
if ('undefined' === typeof window) {
plugin = require(name)(app, opts);
} else {
name = getModuleName(name);
plugin = window[name](app, opts);
}
if (plugin) {
my.plugin[shortname] = plugin;
log.info('%s plugin %s loaded with name %s', app.name, name, shortname);
} else {
log.error('%s plugin %s not loaded', app.name, name);
}
};
/**
* A crypter class
*
* @private
*/
my.Crypter = Crypter;
// init config
my.setConfig();
return my;
};