Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for proxy authentication using the X-Proxy-Authorizatio… #11

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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.
Expand Down Expand Up @@ -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.

85 changes: 85 additions & 0 deletions lib/express-proxy-auth.js
Original file line number Diff line number Diff line change
@@ -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
114 changes: 114 additions & 0 deletions lib/proxy-auth.js
Original file line number Diff line number Diff line change
@@ -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 = *<TEXT excluding ":">
* 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
}
8 changes: 6 additions & 2 deletions lib/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -13,7 +13,8 @@ var ALLOWED_HEADERS = [
'SOAPAction',
'SForce-Auto-Assign',
'If-Modified-Since',
'X-User-Agent'
'X-User-Agent',
'X-Proxy-Authorization'
];

/**
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 19 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand All @@ -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');
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down