├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── README.md
├── example.js
├── index.js
├── index.md
├── package-lock.json
├── package.json
├── src
├── credentials.js
├── error_credentials.js
├── oauth-shim.js
├── oauth1.js
├── oauth2.js
├── proxy.js
├── sign.js
└── utils
│ ├── filter.js
│ ├── merge.js
│ ├── originRegExp.js
│ ├── param.js
│ ├── qs.js
│ └── request.js
└── test
├── .eslintrc.json
├── e2e
├── oauth-shim.js
└── proxy.js
├── mocha.opts
├── setup.js
└── unit
├── credentials.js
└── originRegExp.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | indent_size = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [.travis.yml]
13 | indent_style = space
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": "mr",
4 | "env": {
5 | "node": true
6 | },
7 | "rules": {
8 | "no-console": 0,
9 | "no-throw-literal": 0,
10 | "no-var": 0,
11 | "object-shorthand": 0,
12 | "prefer-arrow-callback": 0,
13 | "prefer-template": 0,
14 | "prefer-spread": 0
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | credentials.json
3 | npm-debug.log
4 | .env
5 | .coveralls.yml
6 | .nyc_output/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | sudo: false
3 | node_js:
4 | - 'node'
5 | - 'lts/*'
6 | deploy:
7 | provider: npm
8 | email: andrewjdodson@gmail.com
9 | api_key:
10 | secure: e2eN3zC7A7xBbKnu1y9K+bVMtnZROO0LBiLn3QLsup4joVYGrllLITUbFYJFF+fxRPrRB6RMCzkM3aFP3lYauxrLr1P1xSID1BsqR9tAzi6uesdUMAMceyHo7/YL3nHbw830j7ZH37Ii6ziX8igbrnbf+0klmIYvBPcrke4srhM=
11 | 'on':
12 | tags: true
13 | repo: MrSwitch/node-oauth-shim
14 | all_branches: true
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OAuth-shim
2 |
3 | Middleware offering OAuth1/OAuth2 authorization handshake for web applications using the [HelloJS](http://adodson.com/hello.js) clientside authentication library.
4 |
5 | [](https://greenkeeper.io/)
6 | [](https://snyk.io/test/github/mrswitch/node-oauth-shim)
7 | [](https://travis-ci.org/MrSwitch/node-oauth-shim)
8 | [](https://npmjs.org/package/oauth-shim)
9 | [](https://coveralls.io/github/MrSwitch/node-oauth-shim?branch=master)
10 |
11 |
12 | ## tl;dr;
13 |
14 | [https://auth-server.herokuapp.com](https://auth-server.herokuapp.com) is a service which utilizes this package. If you dont want to implement your own you can simply and freely register thirdparty application Key's and Secret's there.
15 |
16 |
17 | ## Implement
18 |
19 |
20 | ```bash
21 | npm install oauth-shim
22 | ```
23 |
24 | Middleware for Express/Connect
25 |
26 |
27 | ```javascript
28 | var oauthshim = require('oauth-shim'),
29 | express = require('express'),
30 | bodyParser = require('body-parser');
31 |
32 | var app = express();
33 |
34 | app.use(bodyParser.urlencoded({extended: true}));
35 | app.use(bodyParser.json());
36 |
37 | app.all('/oauthproxy', oauthshim);
38 |
39 | // Initiate the shim with Client ID's and secret, e.g.
40 | oauthshim.init([{
41 | // id : secret
42 | client_id: '12345',
43 | client_secret: 'secret678910',
44 | // Define the grant_url where to exchange Authorisation codes for tokens
45 | grant_url: 'https://linkedIn.com',
46 | // Restrict the callback URL to a delimited list of callback paths
47 | domain: 'test.com, example.com/redirect'
48 | }
49 | , ...
50 | ]);
51 | app.listen(3000);
52 | ```
53 |
54 | The above code will put your shimming service to the pathname `http://localhost:3000/oauthproxy`.
55 |
56 |
57 | ## Example
58 |
59 | An example of the above script can be found at [example.js](./example.js).
60 |
61 | To run `node example.js` locally:
62 |
63 | * Install developer dependencies `npm install -l`.
64 | * Create a `credentials.json` file. e.g.
65 |
66 | ```json
67 | [
68 | {
69 | "name": "twitter",
70 | "domain": "http://myapp.com",
71 | "client_id": "app1234",
72 | "client_secret": "secret1234",
73 | "grant_url": "https://api.twitter.com/oauth/access_token"
74 | },
75 | {
76 | "name": "yahoo",
77 | "domain": "http://myapp.com",
78 | "client_id": "app1234",
79 | "client_secret": "secret1234",
80 | },
81 | ...
82 | ]
83 | ```
84 |
85 | * Start up the server...
86 |
87 | ```bash
88 | PORT=5500 node example.js
89 | ```
90 |
91 | Configure your [HelloJS](https://github.com/MrSwitch/hello.js) to use this service.
92 |
93 | ```javascript
94 | hello.init({
95 | twitter: 'app1234',
96 | yahoo: 'app1234,'
97 | }, {
98 | oauth_proxy: `http://localhost:5500/proxy`
99 | });
100 | ```
101 |
102 | Then use helloJS as normal.
103 |
104 | ## Customised Middleware
105 |
106 | ### Capture Access Tokens
107 |
108 | Use the middleware to capture the access_token registered with your app at any point in the series of operations that this module steps through. In the example below they are disseminated with a `customHandler` in the middleware chain to capture the access_token...
109 |
110 |
111 | ```javascript
112 |
113 | app.all('/oauthproxy',
114 | oauthshim.interpret,
115 | customHandler,
116 | oauthshim.proxy,
117 | oauthshim.redirect,
118 | oauthshim.unhandled);
119 |
120 |
121 | function customHandler(req, res, next){
122 |
123 | // Check that this is a login redirect with an access_token (not a RESTful API call via proxy)
124 | if( req.oauthshim &&
125 | req.oauthshim.redirect &&
126 | req.oauthshim.data &&
127 | req.oauthshim.data.access_token &&
128 | req.oauthshim.options &&
129 | !req.oauthshim.options.path ){
130 |
131 | // do something with the token (req.oauthshim.data.access_token)
132 | }
133 |
134 | // Call next to complete the operation
135 | next()
136 | }
137 |
138 | ```
139 |
140 |
141 | ### Asynchronsly retrieve the secret
142 |
143 | Rewrite the function `getCredentials` to change the way the client secret is stored/retrieved. This method is asyncronous, to access the secret from a database etc..
144 | e.g...
145 |
146 | ```javascript
147 | // Overwrite the credentials `get` method
148 | oauthshim.credentials.get = function(query, callback){
149 | // Return
150 | if(query.client_id === '12345'){
151 | callback({
152 | client_secret: 'secret678910'
153 | });
154 | }
155 | if(query.client_id === 'abcde'){
156 | callback({
157 | client_secret: 'secret123456'
158 | });
159 | }
160 | }
161 | ```
162 |
163 | ## Authentication API
164 |
165 | The API adopts similar URL format as the standard OAuth2. Additional metadata about how to handle the request is communicated through the `state` parameter as a JSON string.
166 |
167 | ### Authentication OAuth 2.0
168 |
169 | [STATE] includes:
170 |
171 | | key | value
172 | |------------------|---------------------
173 | | oauth.version | 2
174 | | oauth.grant | [PROVIDERS_OAUTH2_GRANT_URL]
175 |
176 |
177 | The OAuth2 flow for the shim starts after a web application sends a client out to a providers site to grant permissions. The response is an authorization code "[AUTH_CODE]" which is returned to your site, this needs to be exchanged for an Access Token. Your page then needs to send this code to an //auth-server to be exhchanged for an access token, e.g.
178 |
179 |
180 | ?redirect_uri=[REDIRECT_PATH]
181 | &code=[AUTH_CODE]
182 | &client_id=[APP_KEY]
183 | &state=[STATE]
184 |
185 | The //auth-server exchanges the Authorization code for an access_token and redirects the client back to the location of [REDIRECT_PATH], with the contents of the server response as well as whatever was defined in the [STATE] in the hash. e.g...
186 |
187 |
188 | [REDIRECT_PATH]#state=[STATE]&access_token=ABCD1233234&expires=123123123
189 |
190 |
191 |
192 | ### Authentication OAuth 1.0 & 1.0a
193 |
194 | [STATE] includes:
195 |
196 | | key | value
197 | |------------------|---------------------
198 | |oauth.version | 1.0a
199 | |oauth.request | [OAUTH_REQUEST_TOKEN_URL]
200 | |oauth.auth | [OAUTH_AUTHORIZATION_URL]
201 | |oauth.token | [OAUTH_TOKEN_URL]
202 | |oauth_proxy | //auth-server
203 |
204 | OAuth 1.0 has a number of steps so forgive the verbosity here. An app is required to make an initial request to the //auth-server, which in-turn initiates the authentication flow.
205 |
206 |
207 | //auth-server?redirect_uri=[REDIRECT_PATH]
208 | &client_id=[APP_KEY]
209 | &state=[STATE]
210 |
211 |
212 | The //auth-server signs the client request and redirects the user to the providers login page defined by `[OAUTH_AUTHRIZATION_URL]`.
213 |
214 | Once the user has signed in they are redirected back to a page on the developers app defined by `[REDIRECT_PATH]`.
215 |
216 | The provider should have included an oauth_callback parameter which was defined by //auth-server, this includes part of the path where the token can be returned for an access token. The total path response shall look something like this.
217 |
218 |
219 | [REDIRECT_PATH]
220 | ?state=[STATE]
221 | &client_id=[APP_KEY]
222 | &oauth_token=abc12465
223 |
224 |
225 | The page you defined locally as the `[REDIRECT_PATH]`, must then construct a call to //auth-server to exchange the unauthorized oauth_token for an access token. This would look like this...
226 |
227 |
228 | //auth-server?oauth_token=abc12465
229 | &redirect_uri=[REDIRECT_PATH]
230 | &client_id=[APP_KEY]
231 | &state=[STATE]
232 |
233 |
234 | Finally the //auth-server returns the access_token to your redirect path and its the responsibility of your script to store this in the client in order to make subsequent API calls.
235 |
236 | [REDIRECT_PATH]#state=[STATE]&access_token=ABCD1233234&expires=123123123
237 |
238 |
239 | This access token still needs to be signed via //auth-server every time an API request is made - read on...
240 |
241 |
242 |
243 |
244 |
245 | ## API: Signing API Requests
246 |
247 | The OAuth 1.0 API requires that each request is uniquely signed with the application secret. This restriction was removed in OAuth 2.0, so only applied to OAuth1 endpoints.
248 |
249 | ### A simple GET Redirect
250 |
251 | To sign a request to `[API_PATH]`, use the `[ACCESS_TOKEN]` returned in OAuth 1.0 above and send to the auth-server.
252 |
253 | ?access_token=[ACCESS_TOKEN]
254 | &path=[API_PATH]
255 |
256 | The oauth shim signs and redirects the requests to the `[API_PATH]` e.g.
257 |
258 | [API_PATH]?oauth_token=asdf&oauth_consumer_key=asdf&...&oauth_signature=1234
259 |
260 | If the initial request was other than a GET request, it will be proxied through the oauthshim by default. CORS headers would be added to the response from the end server.
261 |
262 | ### Signing a Request and returning the Signed Request URL
263 |
264 | If the end server supports CORS and a lot of data is expected to be either sent or returned. The burded on the oauthshim can be lessened by merely returning the signed request url and handling the action elsewhere.
265 |
266 | ?access_token=[ACCESS_TOKEN]
267 | &path=[API_PATH]
268 | &then=return
269 |
270 | ### Proxying the Request
271 | Conversely forcing the request to proxy through the oauthshim is achieved by applying the flag then=proxy. CORS headers are added to the response. This naturally is the slow route for data and is best avoided.
272 |
273 | ?access_token=[ACCESS_TOKEN]
274 | &path=[API_PATH]
275 | &then=proxy
276 |
277 |
278 | ### Change the method and add callback for JSONP
279 | Add a JSONP callback function and override the method. E.g.
280 |
281 | ?access_token=[ACCESS_TOKEN]
282 | &path=[API_PATH]
283 | &then=return
284 | &method=post
285 | &callback=myJSONP
286 |
287 |
288 | ## Specs
289 |
290 | ```bash
291 | # Install the test dependencies.
292 | npm install -l
293 |
294 | # Run tests
295 | npm test
296 | ```
297 |
--------------------------------------------------------------------------------
/example.js:
--------------------------------------------------------------------------------
1 | // Demonstation of integration
2 | var oauthshim = require('./index.js');
3 | var express = require('express');
4 | var bodyParser = require('body-parser');
5 |
6 | var app = express();
7 |
8 | // use bodyParser to enable form POST and JSON POST requests
9 | app.use(bodyParser.urlencoded({extended: true}));
10 | app.use(bodyParser.json());
11 |
12 | // Define a path where to put this OAuth Shim
13 | app.all('/proxy', oauthshim);
14 |
15 | // Create a new file called "credentials.json", an array of objects containing {domain, client_id, client_secret, grant_url}
16 | var creds = require('./credentials.json');
17 |
18 | // Initiate the shim with credentials
19 | oauthshim.init(creds);
20 |
21 | // Set application to listen on PORT
22 | app.listen(process.env.PORT);
23 |
24 | console.log('OAuth Shim listening on ' + process.env.PORT);
25 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | //
2 | // Implement oauth-shim with a webservice
3 | //
4 |
5 | var oauthshim = require('./src/oauth-shim');
6 | var url = require('url');
7 |
8 | oauthshim.listen = function(server, requestPathname) {
9 |
10 | // Store old Listeners
11 | var oldListeners = server.listeners('request');
12 | server.removeAllListeners('request');
13 |
14 | server.on('request', function(req, res) {
15 |
16 | // Lets let something else handle this.
17 | // Trigger all oldListeners
18 | function passthru() {
19 | oldListeners.forEach(function(handler) {
20 | handler.call(server, req, res);
21 | });
22 | }
23 |
24 | // If the request is limited to a given path, here it is.
25 | if (requestPathname && requestPathname !== url.parse(req.url).pathname) {
26 | passthru();
27 | return;
28 | }
29 |
30 | oauthshim.request(req, res);
31 | });
32 | };
33 |
34 | module.exports = oauthshim;
35 |
--------------------------------------------------------------------------------
/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: node-oauth-shim
3 | ---
4 |
5 |
6 |
7 | {% include_relative README.md %}
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oauth-shim",
3 | "version": "1.1.5",
4 | "description": "OAuth2 shim for OAuth1 services, works with the clientside library HelloJS",
5 | "homepage": "https://github.com/MrSwitch/node-oauth-shim",
6 | "main": "index.js",
7 | "scripts": {
8 | "lint": "eslint ./",
9 | "spec": "nyc mocha test/**/*.js",
10 | "server": "PORT=5500 nodemon example.js",
11 | "test": "npm run lint && npm run spec && (nyc report --reporter=text-lcov | coveralls)"
12 | },
13 | "files": [
14 | "src/",
15 | "index.js"
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git@github.com/MrSwitch/node-oauth-shim"
20 | },
21 | "keywords": [
22 | "oauth",
23 | "oauth-proxy",
24 | "oauth-shim",
25 | "rest"
26 | ],
27 | "author": "Andrew Dodson ",
28 | "license": "BSD",
29 | "bugs": {
30 | "url": "https://github.com/MrSwitch/node-oauth-shim/issues"
31 | },
32 | "devDependencies": {
33 | "coveralls": "^3.0.2",
34 | "eslint": "^5.14.1",
35 | "eslint-config-mr": "^1.1.0",
36 | "expect.js": "^0.3.1",
37 | "express": "^4.16.4",
38 | "mocha": "^6.0.0",
39 | "nyc": "^13.3.0",
40 | "sinon": "^7.2.4",
41 | "supertest": "^4.0.0"
42 | },
43 | "dependencies": {}
44 | }
45 |
--------------------------------------------------------------------------------
/src/credentials.js:
--------------------------------------------------------------------------------
1 | //
2 | // Credentials..
3 | // Given an object containing {client_id, ...},
4 | // Append the property client_secret to the original request object
5 | // This must be called in the scope of an object containing an array of credentials.
6 | //
7 |
8 | var originRegExp = require('./utils/originRegExp');
9 |
10 | module.exports = {
11 |
12 | // Store the credentials in an array
13 | credentials: [],
14 |
15 | // Set the credentials too the array
16 | // The input needs to be an array of objects {client_id, client_secret, ...}
17 | set: function(credentials) {
18 | this.credentials.push.apply(this.credentials, credentials);
19 | },
20 |
21 | // Retrieve the credentials
22 | get: function(query, callback) {
23 |
24 | // Loop through the services
25 | for (var i = 0, len = this.credentials.length; i < len; i++) {
26 |
27 | // Item
28 | var item = this.credentials[i];
29 |
30 | // Does matches the client_id
31 | if (item.client_id === query.client_id) {
32 | callback(item);
33 | return;
34 | }
35 | }
36 |
37 | // Return
38 | callback(false);
39 | },
40 |
41 | check: function(query, match) {
42 |
43 | // Is the client_id defined
44 | if (!query.client_id) {
45 | // No client id
46 | return error('required_credentials', 'The client_id "' + query.client_id + '" is missing from the request');
47 | }
48 | else if (!match) {
49 | // No matching details found
50 | return error('invalid_credentials', 'The client_id "' + query.client_id + '" is unknown');
51 | }
52 |
53 | // Define the grant_url base upon the query
54 | if (!query.grant_url && query.oauth && query.oauth.grant) {
55 | query.grant_url = query.oauth.grant;
56 | }
57 |
58 | // Verify this request is for the correct grant_url/token_url
59 | // If a grant is defined, throw an error if it is wrong.
60 | if (match.grant_url && query.grant_url && query.grant_url !== match.grant_url) {
61 |
62 | // Execute callback
63 | return error('invalid_credentials', 'Grant URL "' + query.grant_url + '" must match "' + match.grant_url + '"');
64 | }
65 |
66 | else if (match.domain && query.redirect_uri && !query.redirect_uri.match(originRegExp(match.domain))) {
67 |
68 | // Execute callback
69 | return error('invalid_credentials', 'Redirect URL "' + query.redirect_uri + '" must match "' + match.domain + '"');
70 | }
71 | // Return
72 | return {success: true};
73 | }
74 | };
75 |
76 | function error(code, message) {
77 | return {
78 | error: {
79 | code: code,
80 | message: message
81 | }
82 | };
83 | }
84 |
--------------------------------------------------------------------------------
/src/error_credentials.js:
--------------------------------------------------------------------------------
1 | // error_credentials
2 |
3 | module.exports = function(p) {
4 | return {
5 | error: ((p.client_id || p.id) ? 'invalid' : 'required') + '_credentials',
6 | error_message: 'Could not find the credentials that match the provided client_id: ' + (p.client_id || p.id),
7 | state: p.state || ''
8 | };
9 | };
10 |
--------------------------------------------------------------------------------
/src/oauth-shim.js:
--------------------------------------------------------------------------------
1 | //
2 | // Node-OAuth-Shim
3 | // A RESTful API for interacting with OAuth1 and 2 services.
4 | //
5 | // @author Andrew Dodson
6 | // @since July 2013
7 |
8 | var url = require('url');
9 |
10 | var qs = require('./utils/qs');
11 | var merge = require('./utils/merge');
12 | var param = require('./utils/param');
13 |
14 |
15 | var sign = require('./sign.js');
16 | var proxy = require('./proxy.js');
17 |
18 | var oauth2 = require('./oauth2');
19 | var oauth1 = require('./oauth1');
20 |
21 | // Export a new instance of the API
22 | module.exports = oauth_shim;
23 |
24 | // Map default options
25 | function oauth_shim(req, res, next) {
26 | return oauth_shim.request(req, res, next);
27 | }
28 |
29 | // Get the credentials object for managing the getting and setting of credentials.
30 | var credentials = require('./credentials');
31 |
32 | // Assign the credentials object for remote access to overwrite its functions
33 | oauth_shim.credentials = credentials;
34 |
35 | // Set pretermined client-id's and client-secret
36 | oauth_shim.init = function(arr) {
37 |
38 | // Apply the credentials
39 | credentials.set(arr);
40 | };
41 |
42 | // Request
43 | // Compose all the default operations of this component
44 | oauth_shim.request = function(req, res, next) {
45 |
46 | var self = oauth_shim;
47 |
48 | return self.interpret(req, res,
49 | self.proxy.bind(self, req, res,
50 | self.redirect.bind(self, req, res,
51 | self.unhandled.bind(self, req, res, next))));
52 | };
53 |
54 | // Interpret the oauth login
55 | // Append data to the request object to hand over to the 'redirect' handler
56 | oauth_shim.interpret = function(req, res, next) {
57 |
58 | // if the querystring includes
59 | // An authentication 'code',
60 | // client_id e.g. '1231232123',
61 | // response_uri, '1231232123',
62 | var p = req.query || param(url.parse(req.url).search);
63 | var state = p.state;
64 |
65 | // Has the parameters been stored in the state attribute?
66 | try {
67 | // decompose the p.state, redefine p
68 | p = merge(p, JSON.parse(p.state));
69 | p.state = state; // set this back to the string
70 | }
71 | catch (e) {
72 | // Continue
73 | }
74 |
75 | // Convert p.id into p.client_id
76 | if (p.id && !p.client_id) {
77 | p.client_id = p.id;
78 | }
79 |
80 | // Define the options
81 | req.oauthshim = {
82 | options: p
83 | };
84 |
85 | // Generic formatting `redirect_uri` is of the correct format
86 | if (typeof p.redirect_uri === 'string' && !p.redirect_uri.match(/^[a-z]+:\/\//i)) {
87 | p.redirect_uri = '';
88 | }
89 |
90 | // OAUTH2
91 | if ((p.code || p.refresh_token) && p.redirect_uri) {
92 |
93 | // Get
94 | login(p, function() {
95 |
96 | // OAuth2
97 | oauth2(p, function(session) {
98 |
99 | // Redirect page
100 | // With the Auth response, we need to return it to the parent
101 | session.state = p.state || '';
102 |
103 | // OAuth Login
104 | redirect(req, p.redirect_uri, session, next);
105 | });
106 |
107 | }, function(error) {
108 | redirect(req, p.redirect_uri, error, next);
109 | });
110 |
111 |
112 | }
113 |
114 | // OAUTH1
115 | else if (p.redirect_uri && ((p.oauth && parseInt(p.oauth.version, 10) === 1) || p.oauth_token)) {
116 |
117 | // Credentials...
118 | login(p, function() {
119 | // Add environment info.
120 | p.location = url.parse('http' + (req.connection.encrypted ? 's' : '') + '://' + req.headers.host + req.url);
121 |
122 | // OAuth1
123 | oauth1(p, function(session) {
124 |
125 | var loc = p.redirect_uri;
126 |
127 | if (typeof session === 'string') {
128 | loc = session;
129 | session = {};
130 | }
131 | else {
132 | // Add the state
133 | session.state = p.state || '';
134 | }
135 |
136 | redirect(req, loc, session, next);
137 | });
138 |
139 | }, function(error) {
140 | redirect(req, p.redirect_uri, error, next);
141 | });
142 |
143 |
144 | }
145 |
146 | // Move on
147 | else if (next) {
148 | next();
149 | }
150 |
151 | };
152 |
153 | // Proxy
154 | // Signs/Relays requests
155 | oauth_shim.proxy = function(req, res, next) {
156 |
157 | var p = param(url.parse(req.url).search);
158 |
159 | // SUBSEQUENT SIGNING OF REQUESTS
160 | // Previously we've been preoccupoed with handling OAuth authentication/
161 | // However OAUTH1 also needs every request to be signed.
162 | if (p.access_token && p.path) {
163 |
164 | // errr
165 | var buffer = proxy.buffer(req);
166 |
167 | signRequest((p.method || req.method), p.path, p.data, p.access_token, proxyHandler.bind(null, req, res, next, p, buffer));
168 |
169 |
170 | }
171 | else if (p.path) {
172 |
173 | proxyHandler(req, res, next, p, undefined, p.path);
174 |
175 |
176 | }
177 |
178 | else if (next) {
179 | next();
180 | }
181 | };
182 |
183 |
184 | //
185 | // Redirect Request
186 | // Is this request marked for redirect?
187 | //
188 | oauth_shim.redirect = function(req, res, next) {
189 |
190 | if (req.oauthshim && req.oauthshim.redirect) {
191 |
192 | var hash = req.oauthshim.data;
193 | var path = req.oauthshim.redirect;
194 |
195 | path += (hash ? '#' + param(hash) : '');
196 |
197 | res.writeHead(302, {
198 | 'Access-Control-Allow-Origin': '*',
199 | Location: path
200 | });
201 |
202 | res.end();
203 | }
204 | else if (next) {
205 | next();
206 | }
207 | };
208 |
209 |
210 | //
211 | // unhandled
212 | // What to return if the request was previously unhandled
213 | //
214 | oauth_shim.unhandled = function(req, res) {
215 |
216 | var p = param(url.parse(req.url).search);
217 |
218 | serveUp(res, errorObj('invalid_request', 'The request is unrecognised'), p.callback);
219 |
220 | };
221 |
222 |
223 | //
224 | //
225 | //
226 | //
227 | // UTILITIES
228 | //
229 | //
230 | //
231 | //
232 |
233 | function login(p, successHandler, errorHandler) {
234 |
235 | credentials.get(p, function(match) {
236 |
237 | // Handle error
238 | var check = credentials.check(p, match);
239 |
240 | // Handle errors
241 | if (check.error) {
242 |
243 | var e = check.error;
244 |
245 | errorHandler({
246 | error: e.code,
247 | error_message: e.message,
248 | state: p.state || ''
249 | });
250 | }
251 | else {
252 |
253 | // Add the secret
254 | p.client_secret = match.client_secret;
255 |
256 | // Success
257 | successHandler(match);
258 | }
259 | });
260 | }
261 |
262 | //
263 | // Sign
264 | //
265 |
266 | function signRequest(method, path, data, access_token, callback) {
267 |
268 | var token = access_token.match(/^([^:]+):([^@]+)@(.+)$/);
269 |
270 | if (!token) {
271 |
272 | // If the access_token exists, append it too the path
273 | if (access_token) {
274 | path = qs(path, {
275 | access_token: access_token
276 | });
277 | }
278 |
279 | callback(path);
280 | return;
281 | }
282 |
283 | // Create a credentials object to append the secret too..
284 | var query = {
285 | client_id: token[3]
286 | };
287 |
288 | // Update the credentials object with the client_secret
289 | credentials.get(query, function(match) {
290 |
291 | if (match && match.client_secret) {
292 | path = sign(path, {
293 | oauth_token: token[1],
294 | oauth_consumer_key: query.client_id
295 | }, match.client_secret, token[2], null, method.toUpperCase(), data ? JSON.parse(data) : null);
296 | }
297 |
298 | callback(path);
299 |
300 | });
301 | }
302 |
303 |
304 | //
305 | // Process, pass the request the to be processed,
306 | // The returning function contains the data to be sent
307 | function redirect(req, path, hash, next) {
308 |
309 | req.oauthshim = req.oauthshim || {};
310 | req.oauthshim.data = hash;
311 | req.oauthshim.redirect = path;
312 |
313 | if (next) {
314 | next();
315 | }
316 | }
317 |
318 |
319 | //
320 | // Serve Up
321 | //
322 |
323 | function serveUp(res, body, jsonp_callback) {
324 |
325 | if (typeof(body) === 'object') {
326 | body = JSON.stringify(body, null, 2);
327 | }
328 | else if (typeof(body) === 'string' && jsonp_callback) {
329 | body = '"' + body + '"';
330 | }
331 |
332 | if (jsonp_callback) {
333 | body = jsonp_callback + '(' + body + ')';
334 | }
335 |
336 | res.writeHead(200, {'Access-Control-Allow-Origin': '*'});
337 | res.end(body, 'utf8');
338 | }
339 |
340 |
341 | function proxyHandler(req, res, next, p, buffer, path) {
342 |
343 | // Define Default Handler
344 | // Has the user specified the handler
345 | // determine the default`
346 | if (!p.then) {
347 | if (req.method === 'GET') {
348 | if (!p.method || p.method.toUpperCase() === 'GET') {
349 | // Change the location
350 | p.then = 'redirect';
351 | }
352 | else {
353 | // return the signed path
354 | p.then = 'return';
355 | }
356 | }
357 | else {
358 | // proxy the request through this server
359 | p.then = 'proxy';
360 | }
361 | }
362 |
363 |
364 | //
365 | if (p.then === 'redirect') {
366 | // redirect the users browser to the new path
367 | redirect(req, path, null, next);
368 | }
369 | else if (p.then === 'return') {
370 | // redirect the users browser to the new path
371 | serveUp(res, path, p.callback);
372 | }
373 | else {
374 | var options = url.parse(path);
375 | options.method = p.method ? p.method.toUpperCase() : req.method;
376 |
377 | //
378 | // Proxy
379 | proxy.proxy(req, res, options, buffer);
380 | }
381 | }
382 |
383 | function errorObj(code, message) {
384 | return {
385 | error: {
386 | code: code,
387 | message: message
388 | }
389 | };
390 | }
391 |
--------------------------------------------------------------------------------
/src/oauth1.js:
--------------------------------------------------------------------------------
1 | // ----------------------
2 | // OAuth1 authentication
3 | // ----------------------
4 |
5 | var param = require('./utils/param');
6 | var sign = require('./sign');
7 | var url = require('url');
8 | var request = require('./utils/request');
9 |
10 | // token=>secret lookup
11 | var _token_secrets = {};
12 |
13 | module.exports = function(p, callback) {
14 |
15 | var path;
16 | var token_secret;
17 | var client_secret = p.client_secret;
18 | var version = (p.oauth ? p.oauth.version : 1);
19 |
20 | var opts = {
21 | oauth_consumer_key: p.client_id
22 | };
23 |
24 | // Refresh token?
25 | // Does this include an access token?
26 |
27 | if (p.access_token) {
28 | // Disect access_token
29 | var token = p.access_token.match(/^([^:]+):([^@]+)@(.+)$/);
30 | if (token) {
31 |
32 | // Assign the token
33 | p.oauth_token = token[0];
34 | token_secret = token[1];
35 |
36 | // Grap the refresh token and add it to the opts if it exists.
37 | if (p.refresh_token) {
38 | opts.oauth_session_handle = p.refresh_token;
39 | }
40 | }
41 | }
42 |
43 | // OAUTH 1: FIRST STEP
44 | // The oauth_token has not been provisioned.
45 |
46 | if (!p.oauth_token) {
47 |
48 | // Change the path to be that of the intitial handshake
49 | path = p.oauth ? p.oauth.request : null;
50 |
51 | if (!path) {
52 | return callback({
53 | error: 'required_request_url',
54 | error_message: 'A state.oauth.request is required',
55 | });
56 | }
57 |
58 | // Create the URL of this service
59 | // We are building up a callback URL which we want the client to easily be able to use.
60 |
61 | // Callback
62 | var oauth_callback = p.redirect_uri + (p.redirect_uri.indexOf('?') > -1 ? '&' : '?') + param({
63 | // proxy_url: Deprecated as of HelloJS @ v1.7.1 - property included in `state`, accessed from `state` hence.
64 | proxy_url: p.location.protocol + '//' + p.location.host + p.location.pathname,
65 | state: p.state || '',
66 | client_id: p.client_id
67 | }, function(r) {
68 | // Encode all the parameters
69 | return encodeURIComponent(r);
70 | });
71 |
72 | // Version 1.0a requires the oauth_callback parameter for signing the request
73 | if (version === '1.0a') {
74 | // Define the OAUTH CALLBACK Parameters
75 | opts.oauth_callback = oauth_callback;
76 |
77 | // TWITTER HACK
78 | // See issue https://twittercommunity.com/t/oauth-callback-ignored/33447
79 | if (path.match('api.twitter.com')) {
80 | opts.oauth_callback = encodeURIComponent(oauth_callback);
81 | }
82 | }
83 | }
84 |
85 | // SECOND STEP
86 | // The provider has provisioned a temporary token
87 |
88 | else {
89 |
90 | // Change the path to be that of the Providers token exchange
91 | path = p.oauth ? p.oauth.token : null;
92 |
93 | if (!path) {
94 | return callback({
95 | error: 'required_token_url',
96 | error_message: 'A state.oauth.token url is required to authenticate the oauth_token',
97 | });
98 | }
99 |
100 | // Check that there is a token
101 | opts.oauth_token = p.oauth_token;
102 | if (p.oauth_verifier) {
103 | opts.oauth_verifier = p.oauth_verifier;
104 | }
105 |
106 | // If token secret has not been supplied by an access_token in case of a refresh
107 | // Get secret from temp storage
108 | if (!token_secret && p.oauth_token in _token_secrets) {
109 | token_secret = _token_secrets[p.oauth_token];
110 | }
111 |
112 | // If no secret is given, panic
113 | if (!token_secret) {
114 | return callback({
115 | error: (!p.oauth_token ? 'required' : 'invalid') + '_oauth_token',
116 | error_message: 'The oauth_token ' + (!p.oauth_token ? ' is required' : ' was not recognised'),
117 | });
118 | }
119 | }
120 |
121 |
122 | // Sign the request using the application credentials
123 | var signed_url = sign(path, opts, client_secret, token_secret || null);
124 |
125 | // Requst
126 | var r = url.parse(signed_url);
127 |
128 | // Make the call
129 | request(r, null, function(err, res, data, json) {
130 |
131 | if (err) {
132 | /////////////////////////////
133 | // The server failed to respond
134 | /////////////////////////////
135 | return callback({
136 | error: 'server_error',
137 | error_message: 'Unable to connect to ' + signed_url
138 | });
139 | }
140 |
141 | if (json.error || res.statusCode >= 400) {
142 |
143 | if (!json.error) {
144 | json = {
145 | error: json.oauth_problem || 'auth_failed',
146 | error_message: data.toString() || (res.statusCode + ' could not authenticate')
147 | };
148 | }
149 | callback(json);
150 | }
151 |
152 | // Was this a preflight request
153 | else if (!opts.oauth_token) {
154 | // Step 1
155 |
156 | // Store the oauth_token_secret
157 | if (json.oauth_token_secret) {
158 | _token_secrets[json.oauth_token] = json.oauth_token_secret;
159 | }
160 |
161 | var params = {
162 | oauth_token: json.oauth_token
163 | };
164 |
165 | // Version 1.0a should return oauth_callback_confirmed=true,
166 | // otherwise apply oauth_callback
167 | if (json.oauth_callback_confirmed !== 'true') {
168 | // Define the OAUTH CALLBACK Parameters
169 | params.oauth_callback = oauth_callback;
170 | }
171 |
172 | // Great redirect the user to authenticate
173 | var url = p.oauth.auth;
174 | callback(url + (url.indexOf('?') > -1 ? '&' : '?') + param(params));
175 | }
176 |
177 | else {
178 | // Step 2
179 | // Construct the access token to send back to the client
180 | json.access_token = json.oauth_token + ':' + json.oauth_token_secret + '@' + p.client_id;
181 |
182 | // Optionally return the refresh_token and expires_in if given
183 | if (json.oauth_expires_in) {
184 | json.expires_in = json.oauth_expires_in;
185 | delete json.oauth_expires_in;
186 | }
187 |
188 | // Optionally standarize any refresh token
189 | if (json.oauth_session_handle) {
190 | json.refresh_token = json.oauth_session_handle;
191 | delete json.oauth_session_handle;
192 |
193 | if (json.oauth_authorization_expires_in) {
194 | json.refresh_expires_in = json.oauth_authorization_expires_in;
195 | delete json.oauth_authorization_expires_in;
196 | }
197 | }
198 |
199 | // Return the entire response object to the client
200 | // Often included is ID's, name etc which can save additional requests
201 | callback(json);
202 | }
203 |
204 |
205 | });
206 | };
207 |
--------------------------------------------------------------------------------
/src/oauth2.js:
--------------------------------------------------------------------------------
1 | //
2 | // OAuth2
3 | // Process OAuth2 exchange
4 | //
5 |
6 | var request = require('./utils/request');
7 | var param = require('./utils/param');
8 | var url = require('url');
9 |
10 | module.exports = function(p, callback) {
11 |
12 | // Make the OAuth2 request
13 | var post = null;
14 | if (p.code) {
15 | post = {
16 | code: p.code,
17 | client_id: p.client_id || p.id,
18 | client_secret: p.client_secret,
19 | grant_type: 'authorization_code',
20 | redirect_uri: encodeURIComponent(p.redirect_uri)
21 | };
22 | }
23 | else if (p.refresh_token) {
24 | post = {
25 | refresh_token: p.refresh_token,
26 | client_id: p.client_id || p.id,
27 | client_secret: p.client_secret,
28 | grant_type: 'refresh_token',
29 | };
30 | }
31 |
32 | // Get the grant_url
33 | var grant_url = p.oauth ? p.oauth.grant : false;
34 |
35 | if (!grant_url) {
36 | return callback({
37 | error: 'required_grant',
38 | error_message: 'Missing parameter state.oauth.grant url',
39 | });
40 | }
41 |
42 | // Convert the post object literal to a string
43 | post = param(post, function(r) {
44 | return r;
45 | });
46 |
47 | // Create the request
48 | var r = url.parse(grant_url);
49 | r.method = 'POST';
50 | r.headers = {
51 | 'Content-length': post.length,
52 | 'Content-type': 'application/x-www-form-urlencoded'
53 | };
54 |
55 | // Workaround for Vimeo, which requires an extra Authorization header
56 | if (p.authorisation === 'header') {
57 | r.headers.Authorization = 'basic ' + new Buffer(p.client_id + ':' + p.client_secret).toString('base64');
58 | }
59 |
60 | //opts.body = post;
61 | request(r, post, function(err, res, body, data) {
62 |
63 | // Check responses
64 | if (err || !body || (!('access_token' in data) && !('error' in data))) {
65 | if (!data || typeof(data) !== 'object') {
66 | data = {};
67 | }
68 | data.error = 'invalid_grant';
69 | data.error_message = (err
70 | ? 'Could not find the authenticating server, '
71 | : 'Could not get a sensible response from the authenticating server, '
72 | ) + grant_url;
73 | }
74 | else if ('access_token' in data && !('expires_in' in data)) {
75 | data.expires_in = 3600;
76 | }
77 |
78 | // If the refresh token was on the original request lets return it.
79 | if (p.refresh_token && !data.refresh_token) {
80 | data.refresh_token = p.refresh_token;
81 | }
82 |
83 | // Return to the handler
84 | callback(data);
85 | });
86 | };
87 |
--------------------------------------------------------------------------------
/src/proxy.js:
--------------------------------------------------------------------------------
1 | //
2 | // Proxy Server
3 | // -------------
4 | // Proxies requests with the Access-Control-Allow-Origin Header
5 | //
6 | // @author Andrew Dodson
7 | // Heavily takes code design from ConnectJS
8 |
9 | var url = require('url');
10 | var http = require('http');
11 | var https = require('https');
12 | var EventEmitter = require('events').EventEmitter;
13 |
14 | function request(opts, callback) {
15 |
16 | /*
17 | // Use fiddler?
18 | opts.path = (opts.protocol === 'https:'? 'https' : 'http') + '://' + opts.host + (opts.port?':' + opts.port:'') + opts.path;
19 | if (!opts.headers) {
20 | opts.headers = {};
21 | }
22 | opts.headers.host = opts.host;
23 | opts.host = '127.0.0.1';
24 | // opts.host = 'localhost';
25 | opts.port = 8888;
26 | // opts.protocol = null;
27 |
28 | /**/
29 | var req;
30 | try {
31 | req = (opts.protocol === 'https:' ? https : http).request(opts, callback);
32 | }
33 | catch (e) {
34 | console.error(e);
35 | console.error(JSON.stringify(opts, null, 2));
36 | }
37 | return req;
38 | }
39 |
40 | //
41 | // @param req - Request Object
42 | // @param options || url - Map request to this
43 | // @param res - Response, bind response to this
44 | exports.proxy = function(req, res, options, buffer) {
45 |
46 | //////////////////////////
47 | // Inherit from events
48 | //////////////////////////
49 |
50 | // TODO:
51 | // make this extend the instance
52 | var self = new EventEmitter();
53 |
54 |
55 | ///////////////////////////
56 | // Define where this request is going
57 | ///////////////////////////
58 |
59 | if (typeof(options) === 'string') {
60 | options = url.parse(options);
61 | options.method = req.method;
62 | }
63 | else {
64 | if (!options.method) {
65 | options.method = req.method;
66 | }
67 | }
68 |
69 | if (!options.headers) {
70 | options.headers = {};
71 | }
72 |
73 | if (options.method === 'DELETE') {
74 | options.headers['content-length'] = req.headers['content-length'] || '0';
75 | }
76 |
77 |
78 | // Loop through all req.headers
79 | for (var header in req.headers) {
80 | // Is this a custom header?
81 | if (header.match(/^(x-|content-type|authorization|accept)/i)) {
82 | options.headers[header] = req.headers[header];
83 | }
84 | }
85 |
86 |
87 | options.agent = false;
88 |
89 |
90 | ///////////////////////////////////
91 | // Preflight request
92 | ///////////////////////////////////
93 |
94 | if (req.method.toUpperCase() === 'OPTIONS') {
95 |
96 | // Response headers
97 | var obj = {
98 | 'access-control-allow-origin': '*',
99 | 'access-control-allow-methods': 'OPTIONS, TRACE, GET, HEAD, POST, PUT, DELETE',
100 | 'content-length': 0
101 | // 'Access-Control-Max-Age': 3600, // seconds
102 | };
103 |
104 | // Return any headers the client has specified
105 | if (req.headers['access-control-request-headers']) {
106 | obj['access-control-allow-headers'] = req.headers['access-control-request-headers'];
107 | }
108 |
109 | res.writeHead(204, 'no content', obj);
110 |
111 | return res.end();
112 | }
113 |
114 |
115 | ///////////////////////////////////
116 | // Define error handler
117 | ///////////////////////////////////
118 | function proxyError(err) {
119 |
120 | errState = true;
121 |
122 | //
123 | // Emit an `error` event, allowing the application to use custom
124 | // error handling. The error handler should end the response.
125 | //
126 | if (self.emit('proxyError', err, req, res)) {
127 | return;
128 | }
129 |
130 | res.writeHead(502, {
131 | 'Content-Type': 'text/plain',
132 | 'Access-Control-Allow-Origin': '*',
133 | 'Access-Control-Allow-Methods': 'OPTIONS, TRACE, GET, HEAD, POST, PUT'
134 | });
135 |
136 | if (req.method !== 'HEAD') {
137 |
138 | //
139 | // This NODE_ENV=production behavior is mimics Express and
140 | // Connect.
141 | //if (process.env.NODE_ENV === 'production') {
142 | // res.write('Internal Server Error');
143 | //}
144 | res.write(JSON.stringify({error: err}));
145 | }
146 |
147 | try {
148 | res.end();
149 | }
150 | catch (ex) {
151 | console.error('res.end error: %s', ex.message);
152 | }
153 | }
154 |
155 |
156 | ///////////////////////////////////
157 | // Make outbound call
158 | ///////////////////////////////////
159 | var _req = request(options, function(_res) {
160 |
161 | // Process the `reverseProxy` `response` when it's received.
162 | //
163 | if (req.httpVersion === '1.0') {
164 | if (req.headers.connection) {
165 | _res.headers.connection = req.headers.connection;
166 | }
167 | else {
168 | _res.headers.connection = 'close';
169 | }
170 | }
171 | else if (!_res.headers.connection) {
172 | if (req.headers.connection) {
173 | _res.headers.connection = req.headers.connection;
174 | }
175 | else {
176 | _res.headers.connection = 'keep-alive';
177 | }
178 | }
179 |
180 | // Remove `Transfer-Encoding` header if client's protocol is HTTP/1.0
181 | // or if this is a DELETE request with no content-length header.
182 | // See: https://github.com/nodejitsu/node-http-proxy/pull/373
183 | if (req.httpVersion === '1.0' || (req.method === 'DELETE' && !req.headers['content-length'])) {
184 | delete _res.headers['transfer-encoding'];
185 | }
186 |
187 |
188 | //
189 | // When the `reverseProxy` `response` ends, end the
190 | // corresponding outgoing `res` unless we have entered
191 | // an error state. In which case, assume `res.end()` has
192 | // already been called and the 'error' event listener
193 | // removed.
194 | //
195 | var ended = false;
196 | _res.on('close', function() {
197 | if (!ended) {
198 | _res.emit('end');
199 | }
200 | });
201 |
202 |
203 | //
204 | // After reading a chunked response, the underlying socket
205 | // will hit EOF and emit a 'end' event, which will abort
206 | // the request. If the socket was paused at that time,
207 | // pending data gets discarded, truncating the response.
208 | // This code makes sure that we flush pending data.
209 | //
210 | _res.connection.on('end', function() {
211 | if (_res.readable && _res.resume) {
212 | _res.resume();
213 | }
214 | });
215 |
216 | _res.on('end', function() {
217 | ended = true;
218 | if (!errState) {
219 | try {
220 | res.end();
221 | }
222 | catch (ex) {
223 | console.error('res.end error: %s', ex.message);
224 | }
225 |
226 | // Emit the `end` event now that we have completed proxying
227 | self.emit('end', req, res, _res);
228 | }
229 | });
230 | // Allow observer to modify headers or abort response
231 | try {
232 | self.emit('proxyResponse', req, res, _res);
233 | }
234 | catch (ex) {
235 | errState = true;
236 | return;
237 | }
238 |
239 | // Set the headers of the client response
240 | Object.keys(_res.headers).forEach(function(key) {
241 | res.setHeader(key, _res.headers[key]);
242 | });
243 | res.setHeader('access-control-allow-methods', 'OPTIONS, TRACE, GET, HEAD, POST, PUT');
244 | res.setHeader('access-control-allow-origin', '*');
245 |
246 | //
247 | // StatusCode
248 | // Should we supress error codes
249 | //
250 | var suppress_response_codes = url.parse(req.url, true).query.suppress_response_codes;
251 |
252 | // Overwrite the nasty ones
253 | res.writeHead(suppress_response_codes ? 200 : _res.statusCode);
254 |
255 |
256 | //
257 | // Data
258 | //
259 | _res.on('data', function(chunk, encoding) {
260 | if (res.writable) {
261 | // Only pause if the underlying buffers are full,
262 | // *and* the connection is not in 'closing' state.
263 | // Otherwise, the pause will cause pending data to
264 | // be discarded and silently lost.
265 | if (res.write(chunk, encoding) === false && _res.pause && _res.connection.readable) {
266 | _res.pause();
267 | }
268 | }
269 | });
270 |
271 | res.on('drain', function() {
272 | if (_res.readable && _res.resume) {
273 | _res.resume();
274 | }
275 | });
276 | });
277 |
278 | if (!req) {
279 | console.error('proxyError');
280 | proxyError();
281 | return;
282 | }
283 |
284 | var errState = false;
285 |
286 | ///////////////////////////
287 | // Set Listeners to handle errors
288 | ///////////////////////////
289 |
290 | req.on('error', proxyError);
291 | _req.on('error', proxyError);
292 |
293 | req.on('aborted', function() {
294 | _req.abort();
295 | });
296 |
297 | _req.on('aborted', function() {
298 | _req.abort();
299 | });
300 |
301 |
302 | ///////////////////////////
303 | // Set Listeners to write data
304 | ///////////////////////////
305 |
306 | req.on('data', function(chunk) {
307 |
308 | if (errState) {
309 | return;
310 | }
311 |
312 | // Writing chunk data doesn not require an encoding parameter
313 | var flushed = _req.write(chunk);
314 |
315 | if (flushed) {
316 | return;
317 | }
318 |
319 | req.pause();
320 | _req.once('drain', function() {
321 | try {
322 | req.resume();
323 | }
324 | catch (er) {
325 | console.error('req.resume error: %s', er.message);
326 | }
327 | });
328 |
329 | //
330 | // Force the `drain` event in 100ms if it hasn't
331 | // happened on its own.
332 | //
333 | setTimeout(function() {
334 | _req.emit('drain');
335 | }, 100);
336 | });
337 |
338 | //
339 | // When the incoming `req` ends, end the corresponding `reverseProxy`
340 | // request unless we have entered an error state.
341 | //
342 | req.on('end', function() {
343 | if (!errState) {
344 | _req.end();
345 | }
346 | });
347 |
348 | //
349 | // Buffer
350 | if (buffer) {
351 | return !errState ? buffer.resume() : buffer.destroy();
352 | }
353 |
354 | return this;
355 | };
356 |
357 |
358 | // __Attribution:__ This approach is based heavily on
359 | // [Connect](https://github.com/senchalabs/connect/blob/master/lib/utils.js#L157).
360 | // However, this is not a big leap from the implementation in node-http-proxy < 0.4.0.
361 | // This simply chooses to manage the scope of the events on a new Object literal as opposed to
362 | // [on the HttpProxy instance](https://github.com/nodejitsu/node-http-proxy/blob/v0.3.1/lib/node-http-proxy.js#L154).
363 | //
364 | exports.buffer = function(obj) {
365 | var events = [];
366 | var onData;
367 | var onEnd;
368 |
369 | obj.on('data', onData = function(data, encoding) {
370 | events.push(['data', data, encoding]);
371 | });
372 |
373 | obj.on('end', onEnd = function(data, encoding) {
374 | events.push(['end', data, encoding]);
375 | });
376 |
377 | return {
378 | end: function() {
379 | obj.removeListener('data', onData);
380 | obj.removeListener('end', onEnd);
381 | },
382 | destroy: function() {
383 | this.end();
384 | this.resume = function() {
385 | console.error('Cannot resume buffer after destroying it.');
386 | };
387 |
388 | onData = onEnd = events = obj = null;
389 | },
390 | resume: function() {
391 | this.end();
392 | for (var i = 0, len = events.length; i < len; i++) {
393 | obj.emit.apply(obj, events[i]);
394 | }
395 | }
396 | };
397 | };
398 |
--------------------------------------------------------------------------------
/src/sign.js:
--------------------------------------------------------------------------------
1 | //
2 | // Sign
3 | // -------------------------
4 | // Sign OAuth requests
5 | //
6 | // @author Andrew Dodson
7 |
8 | var crypto = require('crypto');
9 | var url = require('url');
10 | var querystring = require('querystring');
11 |
12 | var merge = require('./utils/merge');
13 |
14 | function hashString(key, str, encoding) {
15 | var hmac = crypto.createHmac('sha1', key);
16 | hmac.update(str);
17 | return hmac.digest(encoding);
18 | }
19 |
20 | function encode(s) {
21 | return encodeURIComponent(s).replace(/!/g, '%21')
22 | .replace(/'/g, '%27')
23 | .replace(/\(/g, '%28')
24 | .replace(/\)/g, '%29')
25 | .replace(/\*/g, '%2A');
26 | }
27 |
28 | module.exports = function(uri, opts, consumer_secret, token_secret, nonce, method, data) {
29 |
30 | // Damage control
31 | if (!opts.oauth_consumer_key) {
32 | console.error('OAuth requires opts.oauth_consumer_key');
33 | }
34 |
35 | // Seperate querystring from path
36 | var path = uri.replace(/[?#].*/, '');
37 | var qs = querystring.parse(url.parse(uri).query);
38 |
39 | // Create OAuth Properties
40 | var query = {
41 | oauth_nonce: nonce || parseInt(Math.random() * 1e20, 10).toString(16),
42 | oauth_timestamp: nonce || parseInt((new Date()).getTime() / 1000, 10),
43 | oauth_signature_method: 'HMAC-SHA1',
44 | oauth_version: '1.0'
45 | };
46 |
47 | // Merge opts and querystring
48 | query = merge(query, opts || {});
49 | query = merge(query, qs || {});
50 | query = merge(query, data || {});
51 |
52 | // Sort in order of properties
53 | var keys = Object.keys(query);
54 | keys.sort();
55 | var params = [];
56 | var _queryString = [];
57 |
58 | keys.forEach(function(k) {
59 | if (query[k]) {
60 | params.push(k + '=' + encode(query[k]));
61 | if (!data || !(k in data)) {
62 | _queryString.push(k + '=' + encode(query[k]));
63 | }
64 | }
65 | });
66 |
67 | params = params.join('&');
68 | _queryString = _queryString.join('&');
69 |
70 | var http = [method || 'GET', encode(path).replace(/\+/g, ' ').replace(/%7E/g, '~'), encode(params).replace(/\+/g, ' ').replace(/%7E/g, '~')];
71 |
72 | // Create oauth_signature
73 | query.oauth_signature = hashString(consumer_secret + '&' + (token_secret || ''),
74 | http.join('&'),
75 | 'base64');
76 |
77 | return path + '?' + _queryString + '&oauth_signature=' + encode(query.oauth_signature);
78 | };
79 |
--------------------------------------------------------------------------------
/src/utils/filter.js:
--------------------------------------------------------------------------------
1 |
2 | //
3 | // filter
4 | // @param sorts the returning resultset
5 | //
6 | module.exports = function filter(o) {
7 | if (['string', 'number'].indexOf(typeof(o)) !== -1) {
8 | return o;
9 | }
10 |
11 | var r = (Array.isArray(o) ? [] : {});
12 |
13 | for (var x in o) {
14 | if (o.hasOwnProperty(x)) {
15 | if (o[x] !== null) {
16 | if (typeof(x) === 'number') {
17 | r.push(this.filter(o[x]));
18 | }
19 | else {
20 | r[x] = this.filter(o[x]);
21 | }
22 | }
23 | }
24 | }
25 | return r;
26 | };
27 |
--------------------------------------------------------------------------------
/src/utils/merge.js:
--------------------------------------------------------------------------------
1 | //
2 | // merge
3 | // recursive merge two objects into one, second parameter overides the first
4 | // @param a array
5 | //
6 |
7 | module.exports = function merge(a, b) {
8 |
9 | var x;
10 | var r = {};
11 |
12 | if (typeof(a) === 'object' && typeof(b) === 'object') {
13 | for (x in a) {
14 | if (Object.prototype.hasOwnProperty.call(a, x)) {
15 | r[x] = a[x];
16 | if (x in b) {
17 | r[x] = merge(a[x], b[x]);
18 | }
19 | }
20 | }
21 | for (x in b) {
22 | if (Object.prototype.hasOwnProperty.call(b, x)) {
23 | if (!(x in a)) {
24 | r[x] = b[x];
25 | }
26 | }
27 | }
28 | }
29 | else {
30 | r = b;
31 | }
32 | return r;
33 | };
34 |
--------------------------------------------------------------------------------
/src/utils/originRegExp.js:
--------------------------------------------------------------------------------
1 | // Given a string representing various domain options.
2 | // Create a regular expression which can match those domains.
3 | module.exports = function(str) {
4 |
5 | // Split the string up into parts
6 | str = '^(' + str.split(/[,\s]+/).map(function(pattern) {
7 |
8 | // Escape weird characters
9 | pattern = pattern.replace(/[^a-z0-9/:*]/g, '\\$&');
10 |
11 | // Prefix
12 | if (!pattern.match(/^https?:\/\//)) {
13 | pattern = 'https?://' + pattern.replace(/^:?\/+/, '');
14 | }
15 |
16 | // Format wildcards
17 | return pattern.replace('*', '.*');
18 | }).join('|') + ')';
19 |
20 | return new RegExp(str);
21 | };
22 |
--------------------------------------------------------------------------------
/src/utils/param.js:
--------------------------------------------------------------------------------
1 |
2 | //
3 | // Param
4 | // Explode/Encode the parameters of an URL string/object
5 | // @param string s, String to decode
6 | //
7 | module.exports = function(s, encode) {
8 |
9 | var a = {};
10 | var m;
11 |
12 | if (typeof(s) === 'string') {
13 |
14 | var decode = encode || decodeURIComponent;
15 |
16 | m = s.replace(/^[#?]/, '').match(/([^=/&]+)=([^&]+)/g);
17 |
18 | if (m) {
19 | m.forEach(function(match) {
20 | var b = match.split('=');
21 | a[b[0]] = decode(b[1]);
22 | });
23 | }
24 | return a;
25 | }
26 | else {
27 | var o = s;
28 | encode = encode || encodeURIComponent;
29 |
30 | a = [];
31 |
32 | for (var x in o) {
33 | if (o.hasOwnProperty(x) && o[x] !== null) {
34 | a.push([x, o[x] === '?' ? '?' : encode(o[x])].join('='));
35 | }
36 | }
37 |
38 | return a.join('&');
39 | }
40 | };
41 |
--------------------------------------------------------------------------------
/src/utils/qs.js:
--------------------------------------------------------------------------------
1 | var param = require('./param');
2 |
3 |
4 | // Append the querystring to a url
5 | // @param string url
6 | // @param object parameters
7 |
8 | module.exports = function(url, params) {
9 | if (params) {
10 | var reg;
11 | for (var x in params) {
12 | if (url.indexOf(x) > -1) {
13 | var str = '[\\?\\&]' + x + '=[^\\&]*';
14 | reg = new RegExp(str);
15 | url = url.replace(reg, '');
16 | }
17 | }
18 | }
19 | return url + (!empty(params) ? (url.indexOf('?') > -1 ? '&' : '?') + param(params) : '');
20 | };
21 |
22 | // empty
23 | // Checks whether an Array has length 0, an object has no properties etc
24 | function empty(o) {
25 | if (isObject(o)) {
26 | return Object.keys(o).length === 0;
27 | }
28 | if (Array.isArray(o)) {
29 | return o.length === 0;
30 | }
31 | else {
32 | return !!o;
33 | }
34 | }
35 |
36 | function isObject(obj) {
37 | return Object.prototype.toString.call(obj) === '[object Object]';
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/request.js:
--------------------------------------------------------------------------------
1 | var https = require('https');
2 | var http = require('http');
3 |
4 | var param = require('./param');
5 |
6 | // Wrap HTTP/HTTPS calls
7 | module.exports = function(req, data, callback) {
8 |
9 | var r = (req.protocol === 'https:' ? https : http).request(req, function(res) {
10 | var buffer = '';
11 | res.on('data', function(data) {
12 | buffer += data;
13 | });
14 | res.on('end', function() {
15 |
16 | var data = buffer.toString();
17 |
18 | // Extract the response into data
19 | var json = {};
20 | try {
21 | json = JSON.parse(data);
22 | }
23 | catch (e) {
24 | try {
25 | json = param(data);
26 | }
27 | catch (ee) {
28 | console.error('ERROR', 'REQUEST: ' + req.url, 'RESPONSE: ' + data);
29 | }
30 | }
31 |
32 | callback(null, res, buffer, json);
33 | });
34 | });
35 |
36 | r.on('error', function(err) {
37 | callback(err);
38 | });
39 |
40 | if (data) {
41 | r.write(data);
42 | }
43 |
44 | r.end();
45 |
46 | return r;
47 | };
48 |
--------------------------------------------------------------------------------
/test/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jasmine": true,
4 | "mocha": true
5 | },
6 | "globals": {
7 | "sinon": true,
8 | "mochaPhantomJS": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/test/e2e/oauth-shim.js:
--------------------------------------------------------------------------------
1 | //
2 | // OAuth Shim Tests
3 | // Run from root with using command 'npm test'
4 | //
5 | // @author Andrew Dodson
6 | // @since July 2013
7 | //
8 | //
9 |
10 | ////////////////////////////////
11 | // Dependiencies
12 | ////////////////////////////////
13 |
14 | var sign = require('../../src/sign');
15 | var oauthshim = require('../../index');
16 | var querystring = require('querystring');
17 |
18 | // Setup a test server
19 | var request = require('supertest');
20 | var expect = require('expect.js');
21 | var express = require('express');
22 | var app = express();
23 |
24 | ////////////////////////////////
25 | // SETUP SHIM LISTENING
26 | ////////////////////////////////
27 |
28 | oauthshim.init([{
29 | // OAuth 1
30 | client_id: 'oauth_consumer_key',
31 | client_secret: 'oauth_consumer_secret'
32 | }, {
33 | // OAuth 2
34 | client_id: 'client_id',
35 | client_secret: 'client_secret'
36 | }]);
37 |
38 | // Start listening
39 | app.all('/proxy', oauthshim);
40 |
41 | ////////////////////////////////
42 | // SETUP REMOTE SERVER
43 | // This reproduces a third party OAuth and API Server
44 | ////////////////////////////////
45 |
46 | var remoteServer = express();
47 | var srv;
48 | var test_port = 3333;
49 |
50 | beforeEach(function() {
51 | oauthshim.onauthorization = null;
52 | srv = remoteServer.listen(test_port);
53 | });
54 |
55 | // tests here
56 | afterEach(function() {
57 | srv.close();
58 | });
59 |
60 | ////////////////////////////////
61 | // Helper functions
62 | ////////////////////////////////
63 |
64 | function param(o) {
65 | var r = {};
66 | for (var x in o) {
67 | if (o.hasOwnProperty(x)) {
68 | if (typeof(o[x]) === 'object') {
69 | r[x] = JSON.stringify(o[x]);
70 | }
71 | else {
72 | r[x] = o[x];
73 | }
74 | }
75 | }
76 |
77 | return querystring.stringify(r);
78 | }
79 |
80 | ////////////////////////////////
81 | // TEST OAUTH2 SIGNING
82 | ////////////////////////////////
83 |
84 | var oauth2codeExchange = '';
85 |
86 | remoteServer.use('/oauth/grant', function(req, res) {
87 |
88 | res.writeHead(200);
89 | res.write(oauth2codeExchange);
90 | res.end();
91 | });
92 |
93 | var error_unrecognised = {
94 | error: {
95 | code: 'invalid_request',
96 | message: 'The request is unrecognised'
97 | }
98 | };
99 |
100 | describe('OAuth2 exchanging code for token, ', function() {
101 |
102 | var query = {};
103 |
104 | beforeEach(function() {
105 | query = {
106 | code: '123456',
107 | client_id: 'client_id',
108 | redirect_uri: 'http://localhost:' + test_port + '/response',
109 | state: JSON.stringify({
110 | oauth: {
111 | grant: 'http://localhost:' + test_port + '/oauth/grant'
112 | }
113 | })
114 | };
115 |
116 | oauth2codeExchange = querystring.stringify({
117 | expires_in: 'expires_in',
118 | access_token: 'access_token',
119 | state: query.state
120 | });
121 |
122 | });
123 |
124 | function redirect_uri(o) {
125 | var hash = [];
126 | for (var x in o) {
127 | hash.push(x + '=' + o[x]);
128 | }
129 | return new RegExp(query.redirect_uri.replace(/\//g, '\\/') + '#' + hash.join('&'));
130 | }
131 |
132 | it('should return an access_token, and redirect back to redirect_uri', function(done) {
133 |
134 | request(app)
135 | .get('/proxy?' + querystring.stringify(query))
136 | .expect('Location', query.redirect_uri + '#' + oauth2codeExchange)
137 | .expect(302)
138 | .end(function(err) {
139 | if (err) throw err;
140 | done();
141 | });
142 | });
143 |
144 | xit('should trigger the listener on authorization', function(done) {
145 |
146 | oauthshim.onauthorization = function(session) {
147 | expect(session).to.have.property('access_token');
148 | done();
149 | };
150 |
151 | request(app)
152 | .get('/proxy?' + querystring.stringify(query))
153 | .end(function(err) {
154 | if (err) throw err;
155 | });
156 | });
157 |
158 | it('should fail if the state.oauth.grant is missing, and redirect back to redirect_uri', function(done) {
159 |
160 | query.state = JSON.stringify({});
161 |
162 | request(app)
163 | .get('/proxy?' + querystring.stringify(query))
164 | .expect('Location', redirect_uri({
165 | error: 'required_grant',
166 | error_message: '([^&]+)',
167 | state: encodeURIComponent(query.state)
168 | }))
169 | .expect(302)
170 | .end(function(err) {
171 | if (err) throw err;
172 | done();
173 | });
174 | });
175 |
176 | it('should fail if the state.oauth.grant is invalid, and redirect back to redirect_uri', function(done) {
177 |
178 | query.state = JSON.stringify({
179 | oauth: {
180 | grant: 'http://localhost:5555'
181 | }
182 | });
183 |
184 | request(app)
185 | .get('/proxy?' + querystring.stringify(query))
186 | .expect('Location', redirect_uri({
187 | error: 'invalid_grant',
188 | error_message: '([^&]+)',
189 | state: encodeURIComponent(query.state)
190 | }))
191 | .expect(302)
192 | .end(function(err) {
193 | if (err) throw err;
194 | done();
195 | });
196 | });
197 |
198 |
199 | it('should error with required_credentials if the client_id was not provided', function(done) {
200 |
201 | delete query.client_id;
202 |
203 | request(app)
204 | .get('/proxy?' + querystring.stringify(query))
205 | .expect('Location', redirect_uri({
206 | error: 'required_credentials',
207 | error_message: '([^&]+)',
208 | state: encodeURIComponent(query.state)
209 | }))
210 | .expect(302)
211 | .end(function(err) {
212 | if (err) throw err;
213 | done();
214 | });
215 | });
216 |
217 | it('should error with invalid_credentials if the supplied client_id had no associated client_secret', function(done) {
218 |
219 | query.client_id = 'unrecognised';
220 |
221 | request(app)
222 | .get('/proxy?' + querystring.stringify(query))
223 | .expect('Location', redirect_uri({
224 | error: 'invalid_credentials',
225 | error_message: '([^&]+)',
226 | state: encodeURIComponent(query.state)
227 | }))
228 | .expect(302)
229 | .end(function(err) {
230 | if (err) throw err;
231 | done();
232 | });
233 | });
234 |
235 | });
236 |
237 |
238 | // /////////////////////////////
239 | // OAuth2 Excahange refresh_token for access_token
240 | // /////////////////////////////
241 |
242 | describe('OAuth2 exchange refresh_token for access token', function() {
243 |
244 | var query = {};
245 |
246 | beforeEach(function() {
247 | query = {
248 | refresh_token: '123456',
249 | client_id: 'client_id',
250 | redirect_uri: 'http://localhost:' + test_port + '/response',
251 | state: JSON.stringify({
252 | oauth: {
253 | grant: 'http://localhost:' + test_port + '/oauth/grant'
254 | }
255 | })
256 | };
257 | oauth2codeExchange = querystring.stringify({
258 | expires_in: 'expires_in',
259 | access_token: 'access_token',
260 | state: query.state
261 | });
262 | });
263 |
264 | it('should redirect back to redirect_uri with an access_token and refresh_token', function(done) {
265 |
266 | request(app)
267 | .get('/proxy?' + querystring.stringify(query))
268 | .expect('Location', query.redirect_uri + '#' + oauth2codeExchange + '&refresh_token=123456')
269 | .expect(302)
270 | .end(function(err) {
271 | if (err) throw err;
272 | done();
273 | });
274 | });
275 |
276 |
277 | context('should permit a variety of redirect_uri\'s', function() {
278 |
279 | ['http://99problems.com', 'https://problems', 'file:///problems'].forEach(function(s) {
280 |
281 | it('should regard ' + s + ' as valid', function(done) {
282 |
283 | query.redirect_uri = s;
284 | request(app)
285 | .get('/proxy?' + querystring.stringify(query))
286 | .expect(302)
287 | .end(function(err) {
288 | if (err) throw err;
289 | done();
290 | });
291 | });
292 |
293 | });
294 | });
295 |
296 |
297 | xit('should trigger on authorization handler', function(done) {
298 |
299 | oauthshim.onauthorization = function(session) {
300 | expect(session).to.have.property('access_token');
301 | done();
302 | };
303 |
304 | request(app)
305 | .get('/proxy?' + querystring.stringify(query))
306 | .end(function(err) {
307 | if (err) throw err;
308 | });
309 | });
310 | });
311 |
312 |
313 | ////////////////////////////////
314 | // REMOTE SERVER AUTHENTICATION
315 | ////////////////////////////////
316 |
317 | // Step 1: Return oauth_token & oauth_token_secret
318 | remoteServer.use('/oauth/request', function(req, res) {
319 |
320 | res.writeHead(200);
321 | var body = querystring.stringify({
322 | oauth_token: 'oauth_token',
323 | oauth_token_secret: 'oauth_token_secret'
324 | });
325 | res.write(body);
326 | res.end();
327 | });
328 |
329 | // Step 3: Return verified token and secret
330 | remoteServer.use('/oauth/token', function(req, res) {
331 |
332 | res.writeHead(200);
333 | var body = querystring.stringify({
334 | oauth_token: 'oauth_token',
335 | oauth_token_secret: 'oauth_token_secret'
336 | });
337 | res.write(body);
338 | res.end();
339 | });
340 |
341 |
342 | ////////////////////////////////
343 | // TEST OAUTH SIGNING
344 | ////////////////////////////////
345 |
346 | describe('OAuth authenticate', function() {
347 |
348 | var query = {};
349 |
350 | beforeEach(function() {
351 | query = {
352 | state: {
353 | oauth: {
354 | version: '1.0a',
355 | request: 'http://localhost:' + test_port + '/oauth/request',
356 | token: 'http://localhost:' + test_port + '/oauth/token',
357 | auth: 'http://localhost:' + test_port + '/oauth/auth'
358 | }
359 | },
360 | client_id: 'oauth_consumer_key',
361 | redirect_uri: 'http://localhost:' + test_port + '/'
362 | };
363 | });
364 |
365 | function redirect_uri(o) {
366 | var hash = [];
367 | for (var x in o) {
368 | hash.push(x + '=' + o[x]);
369 | }
370 | return new RegExp((query.redirect_uri || '').replace(/\//g, '\\/') + '#' + hash.join('&'));
371 | }
372 |
373 |
374 | it('should correctly sign a request', function() {
375 | var callback = 'http://location.com/?wicked=knarly&redirect_uri=' +
376 | encodeURIComponent('http://local.knarly.com/hello.js/redirect.html' +
377 | '?state=' + encodeURIComponent(JSON.stringify({proxy: 'http://localhost'})));
378 | var signed = sign('https://api.dropbox.com/1/oauth/request_token', {oauth_consumer_key: 't5s644xtv7n4oth', oauth_callback: callback}, 'h9b3uri43axnaid', '', '1354345524');
379 | expect(signed).to.equal('https://api.dropbox.com/1/oauth/request_token?oauth_callback=http%3A%2F%2Flocation.com%2F%3Fwicked%3Dknarly%26redirect_uri%3Dhttp%253A%252F%252Flocal.knarly.com%252Fhello.js%252Fredirect.html%253Fstate%253D%25257B%252522proxy%252522%25253A%252522http%25253A%25252F%25252Flocalhost%252522%25257D&oauth_consumer_key=t5s644xtv7n4oth&oauth_nonce=1354345524&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1354345524&oauth_version=1.0&oauth_signature=7hCq53%2Bcl5PBpKbCa%2FdfMtlGkS8%3D');
380 | });
381 |
382 | it('should redirect users to the path defined as `state.oauth.auth` with the oauth_token in 1.0a', function(done) {
383 |
384 | request(app)
385 | .get('/proxy?' + param(query))
386 | .expect('Location', new RegExp(query.state.oauth.auth.replace(/\//g, '\\/') + '\\?oauth_token\\=oauth_token'))
387 | .expect(302)
388 | .end(function(err) {
389 | if (err) throw err;
390 | done();
391 | });
392 | });
393 |
394 | it('should redirect users to the path defined as `state.oauth.auth` with the oauth_token and oauth_callback in 1.0', function(done) {
395 |
396 | query.state.oauth.version = 1;
397 |
398 | request(app)
399 | .get('/proxy?' + param(query))
400 | .expect('Location', new RegExp(query.state.oauth.auth.replace(/\//g, '\\/') + '\\?oauth_token\\=oauth_token\\&oauth_callback\\=' + encodeURIComponent(query.redirect_uri).replace(/\//g, '\\/')))
401 | .expect(302)
402 | .end(function(err) {
403 | if (err) throw err;
404 | done();
405 | });
406 | });
407 |
408 |
409 | it('should return an #error if given a wrong `state.oauth.request`', function(done) {
410 |
411 | query.state.oauth.request = 'http://localhost:' + test_port + '/oauth/brokenrequest';
412 |
413 | request(app)
414 | .get('/proxy?' + param(query))
415 | .expect('Location', redirect_uri({
416 | error: 'auth_failed',
417 | error_message: '([^&]+)',
418 | state: encodeURIComponent(JSON.stringify(query.state))
419 | }))
420 | .expect(302)
421 | .end(function(err) {
422 | if (err) throw err;
423 | done();
424 | });
425 | });
426 |
427 | it('should return an Error `server_error` if given a wrong domain', function(done) {
428 |
429 | query.state.oauth.request = 'http://localhost:' + (test_port + 1) + '/wrongdomain';
430 |
431 | request(app)
432 | .get('/proxy?' + param(query))
433 | .expect('Location', redirect_uri({
434 | error: 'server_error',
435 | error_message: '([^&]+)',
436 | state: encodeURIComponent(JSON.stringify(query.state))
437 | }))
438 | .expect(302)
439 | .end(function(err) {
440 | if (err) throw err;
441 | done();
442 | });
443 | });
444 |
445 | it('should return Error `required_request_url` if `state.oauth.request` url is missing', function(done) {
446 |
447 | delete query.state.oauth.request;
448 |
449 | request(app)
450 | .get('/proxy?' + param(query))
451 | .expect('Location', redirect_uri({
452 | error: 'required_request_url',
453 | error_message: '([^&]+)',
454 | state: encodeURIComponent(JSON.stringify(query.state))
455 | }))
456 | .expect(302)
457 | .end(function(err) {
458 | if (err) throw err;
459 | done();
460 | });
461 | });
462 |
463 | it('should return error `invalid_request` if redirect_uri is missing', function(done) {
464 |
465 | delete query.redirect_uri;
466 |
467 | request(app)
468 | .get('/proxy?' + param(query))
469 | .expect(200, JSON.stringify(error_unrecognised, null, 2))
470 | .end(function(err) {
471 | if (err) throw err;
472 | done();
473 | });
474 | });
475 |
476 | it('should return error `invalid_request` if redirect_uri is not a URL', function(done) {
477 |
478 | query.redirect_uri = 'should be a url';
479 |
480 | request(app)
481 | .get('/proxy?' + param(query))
482 | .expect(200, JSON.stringify(error_unrecognised, null, 2))
483 | .end(function(err) {
484 | if (err) throw err;
485 | done();
486 | });
487 | });
488 |
489 |
490 | it('should error with `required_credentials` if the client_id was not provided', function(done) {
491 |
492 | delete query.client_id;
493 |
494 | request(app)
495 | .get('/proxy?' + param(query))
496 | .expect('Location', redirect_uri({
497 | error: 'required_credentials',
498 | error_message: '([^&]+)',
499 | state: encodeURIComponent(JSON.stringify(query.state))
500 | }))
501 | .expect(302)
502 | .end(function(err) {
503 | if (err) throw err;
504 | done();
505 | });
506 | });
507 |
508 | it('should error with `invalid_credentials` if the supplied client_id had no associated client_secret', function(done) {
509 |
510 | query.client_id = 'unrecognised';
511 |
512 | request(app)
513 | .get('/proxy?' + param(query))
514 | .expect('Location', redirect_uri({
515 | error: 'invalid_credentials',
516 | error_message: '([^&]+)',
517 | state: encodeURIComponent(JSON.stringify(query.state))
518 | }))
519 | .expect(302)
520 | .end(function(err) {
521 | if (err) throw err;
522 | done();
523 | });
524 | });
525 |
526 |
527 | });
528 |
529 |
530 | ////////////////////////////////
531 | // TEST OAUTH EXCHANGE TOKEN
532 | ////////////////////////////////
533 |
534 | describe('OAuth exchange token', function() {
535 |
536 | var query = {};
537 |
538 | beforeEach(function() {
539 | query = {
540 | oauth_token: 'oauth_token',
541 | redirect_uri: 'http://localhost:' + test_port + '/',
542 | client_id: 'oauth_consumer_key',
543 | state: {
544 | oauth: {
545 | token: 'http://localhost:' + test_port + '/oauth/token',
546 | }
547 | }
548 | };
549 | });
550 |
551 | function redirect_uri(o) {
552 | var hash = [];
553 | for (var x in o) {
554 | hash.push(x + '=' + o[x]);
555 | }
556 | return new RegExp(query.redirect_uri.replace(/\//g, '\\/') + '#' + hash.join('&'));
557 | }
558 |
559 |
560 | it('should exchange an oauth_token, and return an access_token', function(done) {
561 |
562 | request(app)
563 | .get('/proxy?' + param(query))
564 | .expect('Location', redirect_uri({
565 | oauth_token: encodeURIComponent('oauth_token'),
566 | oauth_token_secret: encodeURIComponent('oauth_token_secret'),
567 | access_token: encodeURIComponent('oauth_token:oauth_token_secret@' + query.client_id)
568 | }))
569 | .expect(302)
570 | .end(function(err) {
571 | if (err) throw err;
572 | done();
573 | });
574 | });
575 |
576 |
577 | xit('should trigger on authorization handler', function(done) {
578 |
579 | oauthshim.onauthorization = function(session) {
580 | expect(session).to.have.property('access_token');
581 | done();
582 | };
583 |
584 | request(app)
585 | .get('/proxy?' + param(query))
586 | .expect(302)
587 | .end(function(err) {
588 | if (err) throw err;
589 | });
590 | });
591 |
592 |
593 | it('should return an #error if given an erroneous token_url', function(done) {
594 |
595 | query.state.oauth.token = 'http://localhost:' + test_port + '/oauth/brokentoken';
596 |
597 | request(app)
598 | .get('/proxy?' + param(query))
599 | .expect('Location', redirect_uri({
600 | error: 'auth_failed',
601 | error_message: '([^&]+)',
602 | state: encodeURIComponent(JSON.stringify(query.state))
603 | }))
604 | .expect(302)
605 | .end(function(err) {
606 | if (err) throw err;
607 | done();
608 | });
609 | });
610 |
611 | it('should return an #error if token_url is missing', function(done) {
612 |
613 | delete query.state.oauth.token;
614 |
615 | request(app)
616 | .get('/proxy?' + param(query))
617 | .expect('Location', redirect_uri({
618 | error: 'required_token_url',
619 | error_message: '([^&]+)',
620 | state: encodeURIComponent(JSON.stringify(query.state))
621 | }))
622 | .expect(302)
623 | .end(function(err) {
624 | if (err) throw err;
625 | done();
626 | });
627 | });
628 |
629 | it('should return an #error if the oauth_token is wrong', function(done) {
630 |
631 | query.oauth_token = 'boom';
632 |
633 | request(app)
634 | .get('/proxy?' + param(query))
635 | .expect('Location', redirect_uri({
636 | error: 'invalid_oauth_token',
637 | error_message: '([^&]+)',
638 | state: encodeURIComponent(JSON.stringify(query.state))
639 | }))
640 | .expect(302)
641 | .end(function(err) {
642 | if (err) throw err;
643 | done();
644 | });
645 | });
646 |
647 | });
648 |
649 |
650 | ////////////////////////////////
651 | // REMOTE SERVER API
652 | ////////////////////////////////
653 |
654 | remoteServer.use('/api/', function(req, res) {
655 |
656 | // If an Number is passed on the URL then return that number as the StatusCode
657 | if (req.url.replace(/^\//, '') > 200) {
658 | res.writeHead(req.url.replace(/^\//, '') * 1);
659 | res.end();
660 | return;
661 | }
662 |
663 | res.setHeader('x-test-url', req.url);
664 | res.setHeader('x-test-method', req.method);
665 | res.writeHead(200);
666 |
667 | // console.log(req.headers);
668 |
669 | var buf = '';
670 | req.on('data', function(data) {
671 | buf += data;
672 | });
673 |
674 | req.on('end', function() {
675 | ////////////////////
676 | // TAILOR THE RESPONSE TO MATCH THE REQUEST
677 | ////////////////////
678 | res.write([req.method, req.headers.header, buf].filter(function(a) {
679 | return !!a;
680 | }).join('&'));
681 | res.end();
682 | });
683 |
684 | });
685 |
686 |
687 | // Test path
688 | var api_url = 'http://localhost:' + test_port + '/api/';
689 | var access_token = 'token_key:token_secret@oauth_consumer_key';
690 |
691 |
692 | ////////////////////////////////
693 | // TEST PROXY
694 | ////////////////////////////////
695 |
696 | describe('Proxying requests with a shimed access_token', function() {
697 |
698 |
699 | ///////////////////////////////
700 | // REDIRECT THE AGENT
701 | ///////////////////////////////
702 |
703 | it('should correctly sign and return a 302 redirection, implicitly', function() {
704 |
705 | request(app)
706 | .get('/proxy?access_token=' + access_token + '&path=' + api_url)
707 | .expect('Location', new RegExp(api_url + '\\?oauth_consumer_key\\=oauth_consumer_key\\&oauth_nonce\\=.+&oauth_signature_method=HMAC-SHA1\\&oauth_timestamp=[0-9]+\\&oauth_token\\=token_key\\&oauth_version\\=1\\.0\\&oauth_signature\\=.+\\%3D'))
708 | .expect(302)
709 | .end(function(err) {
710 | if (err) throw err;
711 | });
712 | });
713 |
714 | it('should correctly sign and return a 302 redirection, explicitly', function() {
715 |
716 | request(app)
717 | .get('/proxy?access_token=' + access_token + '&then=redirect&path=' + api_url)
718 | .expect('Location', new RegExp(api_url + '\\?oauth_consumer_key\\=oauth_consumer_key\\&oauth_nonce\\=.+&oauth_signature_method=HMAC-SHA1\\&oauth_timestamp=[0-9]+\\&oauth_token\\=token_key\\&oauth_version\\=1\\.0\\&oauth_signature\\=.+\\%3D'))
719 | .expect(302)
720 | .end(function(err) {
721 | if (err) throw err;
722 | });
723 | });
724 |
725 |
726 | ///////////////////////////////
727 | // RETURN THE SIGNED REQUEST
728 | ///////////////////////////////
729 |
730 | it('should correctly return a signed uri', function() {
731 |
732 | request(app)
733 | .get('/proxy?then=return&access_token=' + access_token + '&path=' + api_url)
734 | .expect(200, new RegExp(api_url + '\\?oauth_consumer_key\\=oauth_consumer_key\\&oauth_nonce\\=.+&oauth_signature_method=HMAC-SHA1\\&oauth_timestamp=[0-9]+\\&oauth_token\\=token_key\\&oauth_version\\=1\\.0\\&oauth_signature\\=.+\\%3D'))
735 | .end(function(err) {
736 | if (err) throw err;
737 | });
738 | });
739 |
740 | it('should correctly return signed uri in a JSONP callback', function() {
741 |
742 | request(app)
743 | .get('/proxy?then=return&access_token=' + access_token + '&path=' + api_url + '&callback=myJSON')
744 | .expect(200, new RegExp('myJSON\\(([\'"])' + api_url + '\\?oauth_consumer_key\\=oauth_consumer_key\\&oauth_nonce\\=.+&oauth_signature_method=HMAC-SHA1\\&oauth_timestamp=[0-9]+\\&oauth_token\\=token_key\\&oauth_version\\=1\\.0\\&oauth_signature\\=.+\\%3D(\\1)\\)'))
745 | .end(function(err) {
746 | if (err) throw err;
747 | });
748 | });
749 |
750 | it('should accept the method and correctly return a signed uri accordingly', function() {
751 |
752 | request(app)
753 | .get('/proxy?then=return&method=POST&access_token=' + access_token + '&path=' + api_url)
754 | .expect(200, new RegExp(api_url + '\\?oauth_consumer_key\\=oauth_consumer_key\\&oauth_nonce\\=.+&oauth_signature_method=HMAC-SHA1\\&oauth_timestamp=[0-9]+\\&oauth_token\\=token_key\\&oauth_version\\=1\\.0\\&oauth_signature\\=.+\\%3D'))
755 | .end(function(err) {
756 | if (err) throw err;
757 | });
758 | });
759 |
760 |
761 | ///////////////////////////////
762 | // PROXY REQUESTS - SIGNED
763 | ///////////////////////////////
764 |
765 | it('should correctly sign the path and proxy GET requests', function(done) {
766 | request(app)
767 | .get('/proxy?then=proxy&access_token=' + access_token + '&path=' + api_url)
768 | .expect('GET')
769 | .end(function(err) {
770 | if (err) throw err;
771 | done();
772 | });
773 | });
774 |
775 | it('should correctly sign the path and proxy POST body', function(done) {
776 |
777 | request(app)
778 | .post('/proxy?then=proxy&access_token=' + access_token + '&path=' + api_url)
779 | .send('POST_DATA')
780 | .expect('Access-Control-Allow-Origin', '*')
781 | .expect('POST&POST_DATA')
782 | .end(function(err) {
783 | if (err) throw err;
784 | done();
785 | });
786 | });
787 |
788 | it('should correctly sign the path and proxy POST asynchronously', function(done) {
789 |
790 | oauthshim.getCredentials = function(id, callback) {
791 | setTimeout(function() {
792 | callback('oauth_consumer_secret');
793 | }, 1000);
794 | };
795 |
796 | request(app)
797 | .post('/proxy?then=proxy&access_token=' + access_token + '&path=' + api_url)
798 | .attach('file', './package.json')
799 | .expect('Access-Control-Allow-Origin', '*')
800 | .expect(/^POST&(--.*?)[\s\S]*(\1)--(\r\n)?$/)
801 | .end(function(err) {
802 | if (err) throw err;
803 | done();
804 | });
805 | });
806 | });
807 |
808 |
809 | describe('Proxying unsigned requests', function() {
810 |
811 | var access_token = 'token';
812 |
813 | ///////////////////////////////
814 | // PROXY REQUESTS - UNSIGNED
815 | ///////////////////////////////
816 |
817 | it('should append the access_token to the path - if it does not conform to an OAuth1 token, and needs not be signed', function(done) {
818 | request(app)
819 | .get('/proxy?then=proxy&access_token=' + access_token + '&path=' + api_url)
820 | .expect('GET')
821 | .expect('x-test-url', /access_token=token/)
822 | .end(function(err) {
823 | if (err) throw err;
824 | done();
825 | });
826 | });
827 |
828 | /*
829 | xit('should not sign the request if the OAuth1 access_token does not match any on record', function(done) {
830 |
831 | var get = credentials.get;
832 | credentials.get = function(query, callback) {
833 | callback(null);
834 | };
835 |
836 | var unknown_oauth1_token = 'user_token_key:user_token_secret@app_token_key';
837 |
838 | request(app)
839 | .get('/proxy?then=proxy&access_token=' + unknown_oauth1_token + '&path=' + api_url)
840 | .expect('GET')
841 | // .expect('x-test-url', /access_token\=token/)
842 | .end(function(err) {
843 | if (err) throw err;
844 | done();
845 | });
846 | });
847 | */
848 |
849 | it('should correctly return a 302 redirection', function() {
850 |
851 | request(app)
852 | .get('/proxy?path=' + api_url)
853 | .expect('Location', api_url)
854 | .expect(302)
855 | .end(function(err) {
856 | if (err) throw err;
857 | });
858 | });
859 |
860 | it('should correctly proxy GET requests', function(done) {
861 | request(app)
862 | .get('/proxy?then=proxy&path=' + api_url)
863 | .expect('GET')
864 | .end(function(err) {
865 | if (err) throw err;
866 | done();
867 | });
868 | });
869 |
870 | it('should correctly proxy POST requests', function(done) {
871 | request(app)
872 | .post('/proxy?then=proxy&path=' + api_url)
873 | .send('POST_DATA')
874 | .expect('Access-Control-Allow-Origin', '*')
875 | .expect('POST&POST_DATA')
876 | .end(function(err) {
877 | if (err) throw err;
878 | done();
879 | });
880 | });
881 |
882 | it('should correctly proxy multipart POST requests', function(done) {
883 | request(app)
884 | .post('/proxy?then=proxy&path=' + api_url)
885 | .attach('file', './package.json')
886 | .expect('Access-Control-Allow-Origin', '*')
887 | .expect(/^POST&(--.*?)[\s\S]*(\1)--(\r\n)?$/)
888 | .end(function(err) {
889 | if (err) throw err;
890 | done();
891 | });
892 | });
893 |
894 | /*
895 | it('should correctly pass through headers', function(done) {
896 | request(app)
897 | .post('/proxy?then=proxy&path=' + api_url)
898 | .set('header', 'header')
899 | .expect('Access-Control-Allow-Origin', '*')
900 | .expect('POST&header')
901 | .end(function(err) {
902 | if (err) throw err;
903 | done();
904 | });
905 | }); */
906 |
907 | it('should correctly proxy DELETE requests', function(done) {
908 | request(app)
909 | .del('/proxy?then=proxy&path=' + api_url)
910 | .expect('Access-Control-Allow-Origin', '*')
911 | .expect('DELETE')
912 | .end(function(err) {
913 | if (err) throw err;
914 | done();
915 | });
916 | });
917 |
918 | it('should handle invalid paths', function(done) {
919 | var fake_url = 'http://localhost:45673/';
920 | request(app)
921 | .post('/proxy?then=proxy&path=' + fake_url)
922 | .send('POST_DATA')
923 | .expect('Access-Control-Allow-Origin', '*')
924 | .expect(502)
925 | .end(function(err) {
926 | if (err) throw err;
927 | done();
928 | });
929 | });
930 |
931 | it('should return server errors', function(done) {
932 |
933 | request(app)
934 | .post('/proxy?then=proxy&path=' + api_url + '401')
935 | .send('POST_DATA')
936 | .expect('Access-Control-Allow-Origin', '*')
937 | .expect(401)
938 | .end(function(err) {
939 | if (err) throw err;
940 | done();
941 | });
942 | });
943 |
944 |
945 | it('should return a JSON error object if absent path parameter', function(done) {
946 |
947 | request(app)
948 | .post('/proxy')
949 | .expect('Access-Control-Allow-Origin', '*')
950 | .expect(200)
951 | .end(function(err, res) {
952 | var obj = JSON.parse(res.text);
953 | if (obj.error.code !== 'invalid_request') throw new Error('Not failing gracefully');
954 | done();
955 | });
956 | });
957 |
958 | });
959 |
--------------------------------------------------------------------------------
/test/e2e/proxy.js:
--------------------------------------------------------------------------------
1 | var proxy = require('../../src/proxy');
2 | var url = require('url');
3 |
4 | // Setup a test server
5 | var request = require('supertest');
6 | var express = require('express');
7 | var app = express();
8 |
9 | /////////////////////////////////
10 | // PROXY SERVER
11 | /////////////////////////////////
12 |
13 | app.all('/proxy', function(req, res) {
14 | var path = req.query.path;
15 | var method = req.query.method || req.method;
16 |
17 | var options = url.parse(path);
18 | options.method = method;
19 |
20 | // Proxy request
21 | proxy.proxy(req, res, options);
22 | });
23 |
24 |
25 | /////////////////////////////////
26 | // FAKE REMOTE SERVER
27 | /////////////////////////////////
28 |
29 | var remoteServer = express();
30 | var srv;
31 | var test_port = 1337;
32 | var api_url = 'http://localhost:' + test_port;
33 |
34 |
35 | ////////////////////////////////
36 | // REMOTE SERVER API
37 | ////////////////////////////////
38 |
39 | remoteServer.use('/', function(req, res) {
40 |
41 | // If an Number is passed on the URL then return that number as the StatusCode
42 | if (req.url.replace(/^\//, '') > 200) {
43 | res.writeHead(req.url.replace(/^\//, '') * 1);
44 | res.end();
45 | return;
46 | }
47 |
48 | res.writeHead(200);
49 |
50 | res.write([req.method, req.headers.header].filter(function(a) {
51 | return !!a;
52 | }).join('&') + '&');
53 |
54 | // console.log(req.headers);
55 |
56 | req.on('data', function(data, encoding) {
57 | res.write(data, encoding);
58 | });
59 |
60 | req.on('end', function() {
61 | ////////////////////
62 | // TAILOR THE RESPONSE TO MATCH THE REQUEST
63 | ////////////////////
64 | res.end();
65 | });
66 |
67 | });
68 |
69 |
70 | beforeEach(function() {
71 | srv = remoteServer.listen(test_port);
72 | });
73 | // tests here
74 | afterEach(function() {
75 | srv.close();
76 | });
77 |
78 |
79 | describe('Proxying unsigned requests', function() {
80 |
81 | ///////////////////////////////
82 | // PROXY REQUESTS - UNSIGNED
83 | ///////////////////////////////
84 |
85 | it('with a GET request', function(done) {
86 | request(app)
87 | .get('/proxy?path=' + api_url)
88 | .expect('GET&')
89 | .end(function(err) {
90 | if (err) throw err;
91 | done();
92 | });
93 | });
94 |
95 | it('with a GET request and x-headers', function(done) {
96 | request(app)
97 | .get('/proxy?path=' + api_url)
98 | .set('x-custom-header', 'custom-header')
99 | .expect('GET&')
100 | .end(function(err) {
101 | if (err) throw err;
102 | done();
103 | });
104 | });
105 |
106 | it('with a POST request', function(done) {
107 | request(app)
108 | .post('/proxy?path=' + api_url)
109 | .send('POST_DATA')
110 | .expect('POST&POST_DATA')
111 | .end(function(err) {
112 | if (err) throw err;
113 | done();
114 | });
115 | });
116 |
117 | it('with a multipart POST request', function(done) {
118 | request(app)
119 | .post('/proxy?path=' + api_url)
120 | .attach('package.json', __dirname + '/../../package.json')
121 | .expect(/^POST&(--.*?)[\s\S]*(\1)--(\r\n)?$/)
122 | .end(function(err) {
123 | if (err) throw err;
124 | done();
125 | });
126 | });
127 |
128 | it('with a multipart DELETE request', function(done) {
129 | request(app)
130 | .del('/proxy?path=' + api_url)
131 | .expect('DELETE&')
132 | .end(function(err) {
133 | if (err) throw err;
134 | done();
135 | });
136 | });
137 |
138 | });
139 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require ./test/setup.js
2 | --reporter spec
3 | --ui bdd
4 |
--------------------------------------------------------------------------------
/test/setup.js:
--------------------------------------------------------------------------------
1 | // Setup
2 |
3 | // Global parameters
4 | global.sinon = require('sinon');
5 | global.expect = require('expect.js');
6 |
--------------------------------------------------------------------------------
/test/unit/credentials.js:
--------------------------------------------------------------------------------
1 | var credentials = require('../../src/credentials');
2 |
3 | describe('credentials', function() {
4 |
5 | beforeEach(function() {
6 | // reset internal values
7 | credentials.credentials = [];
8 | });
9 |
10 | describe('set', function() {
11 |
12 | it('should set credentials to an internal array', function() {
13 |
14 | // Set conf
15 | var conf = {
16 | client_id: 'token',
17 | client_secret: 'secret'
18 | };
19 |
20 | credentials.set([conf]);
21 | expect(credentials.credentials).to.be.an('array');
22 | expect(credentials.credentials[0]).to.be.equal(conf);
23 | });
24 | });
25 |
26 |
27 | describe('get', function() {
28 | var match;
29 |
30 | beforeEach(function() {
31 | match = {
32 | client_id: 'token',
33 | client_secret: 'secret',
34 | };
35 |
36 | // Set the default credentials
37 | credentials.credentials = [match];
38 | });
39 |
40 | it('should find and return credentials matching an object containing a client_id', function() {
41 |
42 | // Spy
43 | var spy = sinon.spy(function(val) {
44 | expect(val).to.eql(match);
45 | });
46 |
47 | var requestObject = {
48 | client_id: 'token'
49 | };
50 |
51 | // Execute the credentials.get
52 | credentials.get(requestObject, spy);
53 |
54 | expect(spy.called).to.be.ok();
55 | });
56 |
57 | it('should return false when no match is found', function() {
58 |
59 | // Spy
60 | var spy = sinon.spy(function(val) {
61 | expect(val).to.eql(false);
62 | });
63 |
64 | var requestObject = {
65 | client_id: 'unregistered_token'
66 | };
67 |
68 | // Execute the credentials.get
69 | credentials.get(requestObject, spy);
70 | expect(spy.called).to.be.ok();
71 | });
72 | });
73 |
74 | describe('check', function() {
75 |
76 | var match;
77 |
78 | beforeEach(function() {
79 |
80 | match = {
81 | client_id: 'token',
82 | client_secret: 'secret',
83 | grant_url: 'https://grant/',
84 | domain: 'test.com'
85 | };
86 |
87 | // Set the default credentials
88 | credentials.credentials = [match];
89 | });
90 |
91 | it('should error invalid_credentials when match is empty', function() {
92 |
93 | var query = {
94 | client_id: 'unregistered_token'
95 | };
96 |
97 | var a = [false, null, 0];
98 |
99 | a.forEach(function(match) {
100 | var output = credentials.check(query, match);
101 | expect(output).to.have.property('error');
102 | expect(output.error).to.have.property('code', 'invalid_credentials');
103 | });
104 |
105 | });
106 |
107 | it('should error required_credentials when client_id is missing from the query', function() {
108 |
109 | var output = credentials.check({}, match);
110 |
111 | expect(output).to.have.property('error');
112 | expect(output.error).to.have.property('code', 'required_credentials');
113 |
114 | });
115 |
116 | it('should error invalid_credentials when grant_url in query and match differ', function() {
117 |
118 | // Valid
119 | var query = {
120 | client_id: 'token',
121 | grant_url: 'https://grant/'
122 | };
123 |
124 | var output = credentials.check(query, match);
125 | expect(output).to.not.have.property('error');
126 | expect(output).to.have.property('success');
127 |
128 |
129 | // InValid
130 | query = {
131 | client_id: 'token',
132 | grant_url: 'https://grantmalicious/'
133 | };
134 | output = credentials.check(query, match);
135 | expect(output).to.have.property('error');
136 | expect(output.error).to.have.property('code', 'invalid_credentials');
137 |
138 | });
139 |
140 | it('should validate the redirect_uri againt the domain and error with invalid_credentials if does not match', function() {
141 |
142 | var unmatch = Object.create(match);
143 | unmatch.domain = 'other.com';
144 |
145 | // Valid
146 | ['https://test.com/path', 'http://test.com/path'].forEach(function(redirect_uri) {
147 | var query = {
148 | client_id: 'token',
149 | redirect_uri: redirect_uri
150 | };
151 | var output = credentials.check(query, match);
152 | expect(output).to.not.have.property('error');
153 |
154 | output = credentials.check(query, unmatch);
155 | expect(output).to.have.property('error');
156 | });
157 |
158 | });
159 | });
160 | });
161 |
--------------------------------------------------------------------------------
/test/unit/originRegExp.js:
--------------------------------------------------------------------------------
1 | var originRegExp = require('../../src/utils/originRegExp');
2 |
3 | describe('originRegExp', function() {
4 |
5 | it('should set return a regular expression', function() {
6 |
7 | var valid_url = 'https://test.com:8080/awesome';
8 | var reg = originRegExp('');
9 | expect(reg).to.be.a('regexp');
10 | expect(valid_url.match(reg)).to.be.ok();
11 |
12 | });
13 |
14 | it('should interpret the following patterns', function() {
15 |
16 | // Valid test url
17 | var valid_url = 'https://test.com:8080/awesome';
18 | var invalid_url = 'https://test.org/awesome';
19 | var mal_url = 'https://t.st.com/awesome/https://test.com';
20 |
21 | // Valid
22 | ['test.com'
23 | , '//test.com'
24 | , '://test.com'
25 | , 'https://test.com'
26 | , 'http://test.com, https://test.com'
27 | , 'https://test.com:8080/awesome'
28 | , 'test.com:8080/*'
29 | ].forEach(function(pattern) {
30 | var reg = originRegExp(pattern);
31 |
32 | expect(valid_url.match(reg)).to.be.ok();
33 |
34 | expect(invalid_url.match(reg)).to.not.be.ok();
35 |
36 | expect(mal_url.match(reg)).to.not.be.ok();
37 |
38 | });
39 |
40 | });
41 |
42 | it('should not break', function() {
43 |
44 | // Valid test url
45 | var valid_url = 'https://test.com:8080/awesome';
46 |
47 | // Invalid syntax
48 | ['?&*)SDASD'
49 | , '////&(ASDT$%£!"£$%^&*()'
50 | ].forEach(function(pattern) {
51 | var reg = originRegExp(pattern);
52 |
53 | expect(valid_url.match(reg)).to.not.be.ok();
54 |
55 | });
56 |
57 | });
58 |
59 | });
60 |
--------------------------------------------------------------------------------