├── .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 | [![Greenkeeper badge](https://badges.greenkeeper.io/MrSwitch/node-oauth-shim.svg)](https://greenkeeper.io/) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/mrswitch/node-oauth-shim/badge.svg)](https://snyk.io/test/github/mrswitch/node-oauth-shim) 7 | [![Build Status](https://img.shields.io/travis/MrSwitch/node-oauth-shim.svg?style=flat)](https://travis-ci.org/MrSwitch/node-oauth-shim) 8 | [![NPM Version](https://img.shields.io/npm/v/oauth-shim.svg?style=flat&branch=master)](https://npmjs.org/package/oauth-shim) 9 | [![Coverage Status](https://coveralls.io/repos/github/MrSwitch/node-oauth-shim/badge.svg?branch=master)](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 | --------------------------------------------------------------------------------