Skip to content

Commit 844ac78

Browse files
committed
Added support for proxy authentication using the X-Proxy-Authorization header.
1 parent eef34c9 commit 844ac78

File tree

6 files changed

+230
-6
lines changed

6 files changed

+230
-6
lines changed

README.md

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Run proxy server :
2323
$ npm start
2424
```
2525

26-
When you use JSforce in your JavaScript app, set `proxyUrl` when creating `Connection` instance.
26+
When you use JSforce in your JavaScript app, set `proxyUrl` when creating `Connection` instance.
2727

2828
```javascript
2929
var conn = new jsforce.Connection({
@@ -37,6 +37,10 @@ conn.query('SELECT Id, Name FROM Account', function(err, res) {
3737
});
3838
```
3939

40+
### Proxy Authentication
41+
42+
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.
43+
4044
## Using as Middleware
4145

4246
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 }));
6973

7074
You don't have to use this app when you are building a JSforce app in Visualforce,
7175
because it works in the same domain as Salesforce API.
72-

lib/express-proxy-auth.js

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const auth = require('./proxy-auth')
2+
const assert = require('assert')
3+
4+
function ensureFunction(option, defaultValue) {
5+
if(option == undefined)
6+
return function() { return defaultValue }
7+
8+
if(typeof option != 'function')
9+
return function() { return option }
10+
11+
return option
12+
}
13+
14+
function buildMiddleware(options) {
15+
var challenge = options.challenge != undefined ? !!options.challenge : false
16+
var users = options.users || {}
17+
var authorizer = options.authorizer || staticUsersAuthorizer
18+
var isAsync = options.authorizeAsync != undefined ? !!options.authorizeAsync : false
19+
var getResponseBody = ensureFunction(options.unauthorizedResponse, '')
20+
var realm = ensureFunction(options.realm)
21+
22+
assert(typeof users == 'object', 'Expected an object for the basic auth users, found ' + typeof users + ' instead')
23+
assert(typeof authorizer == 'function', 'Expected a function for the basic auth authorizer, found ' + typeof authorizer + ' instead')
24+
25+
function staticUsersAuthorizer(username, password) {
26+
for(var i in users)
27+
if(username == i && password == users[i])
28+
return true
29+
30+
return false
31+
}
32+
33+
return function authMiddleware(req, res, next) {
34+
if (req.method === 'OPTIONS') {
35+
next();
36+
return;
37+
}
38+
var authentication = auth(req)
39+
40+
if(!authentication)
41+
return unauthorized()
42+
43+
req.auth = {
44+
user: authentication.name,
45+
password: authentication.pass
46+
}
47+
48+
if(isAsync)
49+
return authorizer(authentication.name, authentication.pass, authorizerCallback)
50+
else if(!authorizer(authentication.name, authentication.pass))
51+
return unauthorized()
52+
53+
return next()
54+
55+
function unauthorized() {
56+
if(challenge) {
57+
var challengeString = 'Basic'
58+
var realmName = realm(req)
59+
60+
if(realmName)
61+
challengeString += ' realm="' + realmName + '"'
62+
63+
res.set('WWW-Authenticate', challengeString)
64+
}
65+
66+
const response = getResponseBody(req)
67+
68+
if(typeof response == 'string')
69+
return res.status(401).send(response)
70+
71+
return res.status(401).json(response)
72+
}
73+
74+
function authorizerCallback(err, approved) {
75+
assert.ifError(err)
76+
77+
if(approved)
78+
return next()
79+
80+
return unauthorized()
81+
}
82+
}
83+
}
84+
85+
module.exports = buildMiddleware

lib/proxy-auth.js

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
'use strict'
2+
3+
module.exports = auth
4+
module.exports.parse = parse
5+
6+
/**
7+
* RegExp for basic auth credentials
8+
*
9+
* credentials = auth-scheme 1*SP token68
10+
* auth-scheme = "Basic" ; case insensitive
11+
* token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
12+
* @private
13+
*/
14+
15+
var CREDENTIALS_REGEXP = /^ *(?:[Bb][Aa][Ss][Ii][Cc]) +([A-Za-z0-9._~+/-]+=*) *$/
16+
17+
/**
18+
* RegExp for basic auth user/pass
19+
*
20+
* user-pass = userid ":" password
21+
* userid = *<TEXT excluding ":">
22+
* password = *TEXT
23+
* @private
24+
*/
25+
26+
var USER_PASS_REGEXP = /^([^:]*):(.*)$/
27+
28+
/**
29+
* Parse the Proxy Authorization header field of a request.
30+
*
31+
* @param {object} req
32+
* @return {object} with .name and .pass
33+
* @public
34+
*/
35+
36+
function auth (req) {
37+
if (!req) {
38+
throw new TypeError('argument req is required')
39+
}
40+
41+
if (typeof req !== 'object') {
42+
throw new TypeError('argument req is required to be an object')
43+
}
44+
45+
// get header
46+
var header = getProxyAuthorization(req.req || req)
47+
48+
// parse header
49+
return parse(header)
50+
}
51+
52+
/**
53+
* Decode base64 string.
54+
* @private
55+
*/
56+
57+
function decodeBase64 (str) {
58+
var escapedCredentials = decodeURIComponent(escape(str))
59+
return new Buffer(escapedCredentials, 'base64').toString()
60+
}
61+
62+
/**
63+
* Get the Proxy Authorization header from request object.
64+
* @private
65+
*/
66+
67+
function getProxyAuthorization (req) {
68+
if (!req.headers || typeof req.headers !== 'object') {
69+
throw new TypeError('argument req is required to have headers property')
70+
}
71+
72+
return req.headers['x-proxy-authorization']
73+
}
74+
75+
/**
76+
* Parse basic auth to object.
77+
*
78+
* @param {string} string
79+
* @return {object}
80+
* @public
81+
*/
82+
83+
function parse (string) {
84+
if (typeof string !== 'string') {
85+
return undefined
86+
}
87+
88+
// parse header
89+
var match = CREDENTIALS_REGEXP.exec(string)
90+
91+
if (!match) {
92+
return undefined
93+
}
94+
95+
// decode user pass
96+
var userPass = USER_PASS_REGEXP.exec(decodeBase64(match[1]))
97+
98+
if (!userPass) {
99+
return undefined
100+
}
101+
102+
// return credentials object
103+
return new Credentials(userPass[1], userPass[2])
104+
}
105+
106+
/**
107+
* Object to represent user credentials.
108+
* @private
109+
*/
110+
111+
function Credentials (name, pass) {
112+
this.name = name
113+
this.pass = pass
114+
}

lib/proxy.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var request = require('request');
22
var debug = require('debug')('jsforce-ajax-proxy');
33

44
/**
5-
* Allowed request headers
5+
* Allowed request headers
66
*/
77
var ALLOWED_HEADERS = [
88
'Authorization',
@@ -13,7 +13,8 @@ var ALLOWED_HEADERS = [
1313
'SOAPAction',
1414
'SForce-Auto-Assign',
1515
'If-Modified-Since',
16-
'X-User-Agent'
16+
'X-User-Agent',
17+
'X-Proxy-Authorization'
1718
];
1819

1920
/**
@@ -55,6 +56,9 @@ module.exports = function(options) {
5556
headers[name] = req.headers[header];
5657
}
5758
});
59+
if (headers['x-proxy-authorization']) {
60+
delete headers['x-proxy-authorization'];
61+
}
5862
var params = {
5963
url: sfEndpoint || "https://login.salesforce.com//services/oauth2/token",
6064
method: req.method,

lib/server.js

+19-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,24 @@
22
var http = require('http');
33
var express = require('express');
44
var jsforceAjaxProxy = require('./proxy');
5+
var proxyAuth = require('./express-proxy-auth');
56

67
var app = express();
78

9+
if (process.env.ENABLE_AUTH === 'true') {
10+
var userName = process.env.USER_NAME;
11+
var password = process.env.PASSWORD;
12+
13+
if (!userName || !password) {
14+
throw new Error("User name or password for basic authentication is not set.");
15+
}
16+
17+
var users = {};
18+
users[userName] = password;
19+
20+
app.use(proxyAuth({users}));
21+
}
22+
823
app.configure(function () {
924
app.set('port', process.env.PORT || 3123);
1025
});
@@ -13,7 +28,10 @@ app.configure('development', function () {
1328
app.use(express.errorHandler());
1429
});
1530

16-
app.all('/proxy/?*', jsforceAjaxProxy({ enableCORS: true }));
31+
app.all('/proxy/?*', jsforceAjaxProxy({
32+
enableCORS: !process.env.DISABLE_CORS || process.env.DISABLE_CORS === 'false',
33+
allowedOrigin: process.env.ALLOWED_ORIGIN
34+
}));
1735

1836
app.get('/', function(req, res) {
1937
res.send('JSforce AJAX Proxy');

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "jsforce-ajax-proxy",
33
"description": "Ajax proxy server to access Salesforce APIs from browser JavaScript resides in outer domain.",
4-
"version": "1.0.0",
4+
"version": "1.0.1",
55
"main": "lib/proxy.js",
66
"scripts": {
77
"start": "node lib/server.js"

0 commit comments

Comments
 (0)