diff --git a/README.md b/README.md index 879f84a..a6be183 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Run proxy server : $ npm start ``` -When you use JSforce in your JavaScript app, set `proxyUrl` when creating `Connection` instance. +When you use JSforce in your JavaScript app, set `proxyUrl` when creating `Connection` instance. ```javascript var conn = new jsforce.Connection({ @@ -37,6 +37,10 @@ conn.query('SELECT Id, Name FROM Account', function(err, res) { }); ``` +### Proxy Authentication + +Authentication is also supported through the use of the X-Proxy-Authorization header. User name and password are specified through the env variables USER_NAME and PASSWORD. Proxy Authentication is disabled by default. Set ENABLE_AUTH=true in order to enable it. + ## Using as Middleware Ajax proxy is not only provided in standalone server but also works as connect middleware. @@ -69,4 +73,3 @@ app.all('/proxy/?*', jsforceAjaxProxy({ enableCORS: true })); You don't have to use this app when you are building a JSforce app in Visualforce, because it works in the same domain as Salesforce API. - diff --git a/lib/express-proxy-auth.js b/lib/express-proxy-auth.js new file mode 100644 index 0000000..07d2407 --- /dev/null +++ b/lib/express-proxy-auth.js @@ -0,0 +1,85 @@ +const auth = require('./proxy-auth') +const assert = require('assert') + +function ensureFunction(option, defaultValue) { + if(option == undefined) + return function() { return defaultValue } + + if(typeof option != 'function') + return function() { return option } + + return option +} + +function buildMiddleware(options) { + var challenge = options.challenge != undefined ? !!options.challenge : false + var users = options.users || {} + var authorizer = options.authorizer || staticUsersAuthorizer + var isAsync = options.authorizeAsync != undefined ? !!options.authorizeAsync : false + var getResponseBody = ensureFunction(options.unauthorizedResponse, '') + var realm = ensureFunction(options.realm) + + assert(typeof users == 'object', 'Expected an object for the basic auth users, found ' + typeof users + ' instead') + assert(typeof authorizer == 'function', 'Expected a function for the basic auth authorizer, found ' + typeof authorizer + ' instead') + + function staticUsersAuthorizer(username, password) { + for(var i in users) + if(username == i && password == users[i]) + return true + + return false + } + + return function authMiddleware(req, res, next) { + if (req.method === 'OPTIONS') { + next(); + return; + } + var authentication = auth(req) + + if(!authentication) + return unauthorized() + + req.auth = { + user: authentication.name, + password: authentication.pass + } + + if(isAsync) + return authorizer(authentication.name, authentication.pass, authorizerCallback) + else if(!authorizer(authentication.name, authentication.pass)) + return unauthorized() + + return next() + + function unauthorized() { + if(challenge) { + var challengeString = 'Basic' + var realmName = realm(req) + + if(realmName) + challengeString += ' realm="' + realmName + '"' + + res.set('WWW-Authenticate', challengeString) + } + + const response = getResponseBody(req) + + if(typeof response == 'string') + return res.status(401).send(response) + + return res.status(401).json(response) + } + + function authorizerCallback(err, approved) { + assert.ifError(err) + + if(approved) + return next() + + return unauthorized() + } + } +} + +module.exports = buildMiddleware diff --git a/lib/proxy-auth.js b/lib/proxy-auth.js new file mode 100644 index 0000000..34e2ad5 --- /dev/null +++ b/lib/proxy-auth.js @@ -0,0 +1,114 @@ +'use strict' + +module.exports = auth +module.exports.parse = parse + +/** + * RegExp for basic auth credentials + * + * credentials = auth-scheme 1*SP token68 + * auth-scheme = "Basic" ; case insensitive + * token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"=" + * @private + */ + +var CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/ + +/** + * RegExp for basic auth user/pass + * + * user-pass = userid ":" password + * userid = * + * password = *TEXT + * @private + */ + +var USER_PASS_REGEXP = /^([^:]*):(.*)$/ + +/** + * Parse the Proxy Authorization header field of a request. + * + * @param {object} req + * @return {object} with .name and .pass + * @public + */ + +function auth (req) { + if (!req) { + throw new TypeError('argument req is required') + } + + if (typeof req !== 'object') { + throw new TypeError('argument req is required to be an object') + } + + // get header + var header = getProxyAuthorization(req.req || req) + + // parse header + return parse(header) +} + +/** + * Decode base64 string. + * @private + */ + +function decodeBase64 (str) { + var escapedCredentials = decodeURIComponent(escape(str)) + return new Buffer(escapedCredentials, 'base64').toString() +} + +/** + * Get the Proxy Authorization header from request object. + * @private + */ + +function getProxyAuthorization (req) { + if (!req.headers || typeof req.headers !== 'object') { + throw new TypeError('argument req is required to have headers property') + } + + return req.headers['x-proxy-authorization'] +} + +/** + * Parse basic auth to object. + * + * @param {string} string + * @return {object} + * @public + */ + +function parse (string) { + if (typeof string !== 'string') { + return undefined + } + + // parse header + var match = CREDENTIALS_REGEXP.exec(string) + + if (!match) { + return undefined + } + + // decode user pass + var userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1])) + + if (!userPass) { + return undefined + } + + // return credentials object + return new Credentials(userPass[1], userPass[2]) +} + +/** + * Object to represent user credentials. + * @private + */ + +function Credentials (name, pass) { + this.name = name + this.pass = pass +} diff --git a/lib/proxy.js b/lib/proxy.js index cb9533b..b96c30d 100644 --- a/lib/proxy.js +++ b/lib/proxy.js @@ -2,7 +2,7 @@ var request = require('request'); var debug = require('debug')('jsforce-ajax-proxy'); /** - * Allowed request headers + * Allowed request headers */ var ALLOWED_HEADERS = [ 'Authorization', @@ -13,7 +13,8 @@ var ALLOWED_HEADERS = [ 'SOAPAction', 'SForce-Auto-Assign', 'If-Modified-Since', - 'X-User-Agent' + 'X-User-Agent', + 'X-Proxy-Authorization' ]; /** @@ -55,6 +56,9 @@ module.exports = function(options) { headers[name] = req.headers[header]; } }); + if (headers['x-proxy-authorization']) { + delete headers['x-proxy-authorization']; + } var params = { url: sfEndpoint || "https://login.salesforce.com//services/oauth2/token", method: req.method, diff --git a/lib/server.js b/lib/server.js index c7aa881..2390902 100644 --- a/lib/server.js +++ b/lib/server.js @@ -2,9 +2,24 @@ var http = require('http'); var express = require('express'); var jsforceAjaxProxy = require('./proxy'); +var proxyAuth = require('./express-proxy-auth'); var app = express(); +if (process.env.ENABLE_AUTH === 'true') { + var userName = process.env.USER_NAME; + var password = process.env.PASSWORD; + + if (!userName || !password) { + throw new Error("User name or password for basic authentication is not set."); + } + + var users = {}; + users[userName] = password; + + app.use(proxyAuth({users})); +} + app.configure(function () { app.set('port', process.env.PORT || 3123); }); @@ -13,7 +28,10 @@ app.configure('development', function () { app.use(express.errorHandler()); }); -app.all('/proxy/?*', jsforceAjaxProxy({ enableCORS: true })); +app.all('/proxy/?*', jsforceAjaxProxy({ + enableCORS: !process.env.DISABLE_CORS || process.env.DISABLE_CORS === 'false', + allowedOrigin: process.env.ALLOWED_ORIGIN +})); app.get('/', function(req, res) { res.send('JSforce AJAX Proxy'); diff --git a/package.json b/package.json index 042572e..857cd8a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jsforce-ajax-proxy", "description": "Ajax proxy server to access Salesforce APIs from browser JavaScript resides in outer domain.", - "version": "1.0.0", + "version": "1.0.1", "main": "lib/proxy.js", "scripts": { "start": "node lib/server.js"