├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── Makefile ├── README ├── double_servers │ ├── README │ ├── app.js │ └── server.js ├── facebook_oauth2_client │ ├── README │ ├── app.js │ └── server.js └── simple_oauth2_client │ ├── README │ ├── app.js │ └── server.js ├── lib └── oauth2_client.js ├── package.json └── tests ├── test_auth_process_login.js ├── test_redirects_for_login.js ├── test_transform_token_response.js └── test_valid_grant.js /.gitignore: -------------------------------------------------------------------------------- 1 | examples/dependencies/* 2 | 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | tests/* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, af83 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2 Client in Node 2 | 3 | ## Description 4 | 5 | oauth2_client_node is a node library providing the bases to implement an OAuth2 client. It features a [connect](https://github.com/senchalabs/connect) middleware to ease the integration with any other components. 6 | 7 | It implements the OAuth2 [web server schema](http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-1.4.1) as specified by the [draft 10 of the OAuth2 specification](http://tools.ietf.org/html/draft-ietf-oauth-v2-10). 8 | 9 | This project will follow the specification evolutions, so a branch for the [draft 11](http://tools.ietf.org/html/draft-ietf-oauth-v2-11) will soon be created. 10 | 11 | 12 | ## Similar projects 13 | 14 | oauth2_client_node is developed together with: 15 | 16 | - [oauth2_server_node](https://github.com/AF83/oauth2_server_node), a connect middleware featuring an OAuth2 server bases. 17 | - [auth_server](https://github.com/AF83/auth_server), an authentication and authorization server in node (using both oauth2_client_node and oauth2_server_node). 18 | 19 | 20 | ## Usage 21 | 22 | There are two examples of usage in the examples directory, one using Facebook as OAuth2 server, and one using auth_server as OAuth2 server. 23 | 24 | To create an OAuth2 client, you will need to to create an oauth2_client_node middleware using oauth2_client.connector. This method returns a connect middleware and takes as arguments: 25 | 26 | - config: hash containing: 27 | 28 | - client, hash containing: 29 | - base_url: The base URL of the OAuth2 client. 30 | Ex: http://domain.com:8080 31 | - process_login_url: the URL where to the OAuth2 server must redirect 32 | the user when authenticated. 33 | - login_url: the URL where the user must go to be redirected 34 | to OAuth2 server for authentication. 35 | - logout_url: the URL where the user must go so that his session is 36 | cleared, and he is unlogged from client. 37 | - default_redirection_url: default URL to redirect to after login / logout. 38 | Optional, default to '/'. 39 | - crypt_key: string, encryption key used to crypt information contained in states. This is a symmetric key and must be kept secret. 40 | - sign_key: string, signature key used to sign (HMAC) issued states. This is a symmetric key and must be kept secret. 41 | 42 | - default_server: which server to use for default login when user 43 | access login_url (ex: 'facebook.com'). 44 | - servers: hash associating OAuth2 server ids (ex: "facebook.com") 45 | with a hash containing (for each): 46 | - server_authorize_endpoint: full URL, OAuth2 server token endpoint 47 | (ex: "https://graph.facebook.com/oauth/authorize"). 48 | - server_token_endpoint: full url, where to check the token 49 | (ex: "https://graph.facebook.com/oauth/access_token"). 50 | - client_id: the client id as registered by this OAuth2 server. 51 | - client_secret: shared secret between client and this OAuth2 server. 52 | 53 | - options: optional, hash associating OAuth2 server ids 54 | (ex: "facebook.com") with hash containing some options specific to the server. 55 | Not all servers have to be listed here, neither all options. 56 | Possible options: 57 | - valid_grant: a function which will replace the default one 58 | to check the grant is ok. You might want to use this shortcut if you 59 | have a faster way of checking than requesting the OAuth2 server 60 | with an HTTP request. 61 | - treat_access_token: a function which will replace the 62 | default one to do something with the access token. You will tipically 63 | use that function to set some info in session. 64 | - transform_token_response: a function which will replace 65 | the default one to obtain a hash containing the access_token from 66 | the OAuth2 server reply. This method should be provided if the 67 | OAuth2 server we are requesting does not return JSON encoded data. 68 | 69 | 70 | Once set and plug, the oauth2_client middleware will catch and answer requests 71 | aimed at the oauth2 client (login, logout and process_login endpoints). 72 | 73 | 74 | ## Dependencies 75 | 76 | * connect 77 | * request 78 | * serializer 79 | 80 | Tested with node v0.4. 81 | 82 | ## Tests 83 | 84 | with nodetk. 85 | 86 | ## Projects using oauth2_client_node 87 | 88 | A [wiki page](https://github.com/AF83/oauth2_client_node/wiki) lists the projects using oauth2_client_node. Don't hesitate to edit it. 89 | 90 | 91 | ## License 92 | 93 | BSD. 94 | -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | 2 | dependencies: 3 | mkdir dependencies 4 | git clone git://github.com/senchalabs/connect.git dependencies/connect 5 | git clone git://github.com/caolan/cookie-sessions.git dependencies/cookie-sessions 6 | 7 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | Here are three examples of use for the OAuth2 client server connect middleware. 2 | 3 | - simple_oauth2_client: an OAuth2 client server connecting to auth_server 4 | OAuth2 server. 5 | - facebook_oauth2_client: an OAuth2 client server connecting to Facebook 6 | OAuth2 server. 7 | - double_servers: an OAuth2 client who lets the user choose which OAuth2 8 | server he wants to use. 9 | 10 | 11 | Before running the examples, you should run: 12 | 13 | make dependencies 14 | 15 | This will retrieve needed dependencies to run the examples (connect and cookie-sessions). 16 | 17 | -------------------------------------------------------------------------------- /examples/double_servers/README: -------------------------------------------------------------------------------- 1 | 2 | To run this example: 3 | 4 | - Same described ../simple_oauth2_client/README 5 | 6 | - Starts the server (Oauth2 client server): 7 | 8 | node server.js 9 | 10 | 11 | - Go to http://127.0.0.1:7070/ and try to log in with one of your FB account(s) 12 | or with auth_server. 13 | 14 | -------------------------------------------------------------------------------- /examples/double_servers/app.js: -------------------------------------------------------------------------------- 1 | var URL = require('url'); 2 | 3 | 4 | var app = function(req, res) { 5 | res.writeHead(200, {'Content-Type': 'text/html'}); 6 | var name = req.session.user_name; 7 | var loginout = null; 8 | if(!name) { 9 | name = 'anonymous'; 10 | loginout = '
Login with Facebook
'; 11 | loginout += '
Login with AuthServer
'; 12 | } 13 | else { 14 | loginout = 'Logout'; 15 | } 16 | res.end('Hello '+ name +'!
'+loginout); 17 | }; 18 | 19 | 20 | exports.connector = function() { 21 | return function(req, res, next) { 22 | var url = URL.parse(req.url); 23 | if(url.pathname == "/") app(req, res); 24 | else next(); 25 | }; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /examples/double_servers/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Add location of submodules to path: 4 | require.paths.unshift(__dirname + '/../dependencies/connect/lib'); 5 | require.paths.unshift(__dirname + '/../dependencies/cookie-sessions/lib'); 6 | require.paths.unshift(__dirname + '/../../vendors/nodetk/src'); 7 | require.paths.unshift(__dirname + '/../../vendors/node-base64'); 8 | require.paths.unshift(__dirname + '/../../src'); 9 | 10 | 11 | var querystring = require('querystring') 12 | , app = require('./app') 13 | , oauth2_client = require('oauth2_client') 14 | , connect = require('connect') 15 | , sessions = require('cookie-sessions') 16 | , web = require('nodetk/web') 17 | ; 18 | 19 | var base_url = 'http://127.0.0.1:7070'; 20 | var config = { 21 | oauth2_client: { 22 | client: { 23 | base_url: base_url, 24 | process_login_url: '/login/process/', 25 | redirect_uri: base_url + '/login/process/', 26 | login_url: '/login', 27 | logout_url: '/logout', 28 | default_redirection_url: '/' 29 | }, 30 | default_server: 'facebook.com', 31 | servers: { 32 | 'facebook.com': { 33 | server_authorize_endpoint: "https://graph.facebook.com/oauth/authorize", 34 | server_token_endpoint: 'https://graph.facebook.com/oauth/access_token', 35 | // These are the client id and secret of a FB application only registered 36 | // for testing purpose... don't use it in prod! 37 | client_id: "c5c0789871d6a65e485bc78235639d36", 38 | client_secret: 'ccd74db270c0b5f1dad0a603d36d6f1b', 39 | } 40 | , "auth_server": { 41 | server_authorize_endpoint: 'http://localhost:8080/oauth2/authorize', 42 | server_token_endpoint: 'http://localhost:8080/oauth2/token', 43 | client_id: null, // TODO: define this before running 44 | client_secret: 'some secret string', 45 | name: 'Test client' 46 | } 47 | } 48 | } 49 | }; 50 | 51 | var oauth2_client_options = { 52 | // To get info from access_token and set them in session 53 | 'facebook.com': { 54 | treat_access_token: function(access_token, req, res, callback) { 55 | var params = {access_token: access_token}; 56 | web.GET('https://graph.facebook.com/me', params, 57 | function(status_code, headers, data) { 58 | console.log('Info given by FB:', data); 59 | var info = JSON.parse(data); 60 | req.session.user_name = info.name; 61 | callback(); 62 | }); 63 | } 64 | , transform_token_response: function(body) { 65 | // It seems Facebook does not respect OAuth2 draft 10 here, so we 66 | // have to override the method. 67 | var data = querystring.parse(body); 68 | if(!data.access_token) return null; 69 | return data; 70 | } 71 | } 72 | , "auth_server": { 73 | treat_access_token: function(access_token, req, res, callback) { 74 | var params = {oauth_token: access_token}; 75 | web.GET('http://localhost:8080/auth', params, 76 | function(status_code, headers, data) { 77 | console.log(data); 78 | var info = JSON.parse(data); 79 | req.session.user_name = info.email; 80 | callback(); 81 | }); 82 | } 83 | } 84 | }; 85 | 86 | var server = connect.createServer( 87 | sessions({secret: '123abc', session_key: 'session'}) 88 | , oauth2_client.connector(config.oauth2_client, oauth2_client_options) 89 | , app.connector() 90 | ); 91 | 92 | var serve = function(port, callback) { 93 | server.listen(port, callback); 94 | } 95 | 96 | if(process.argv[1] == __filename) { 97 | if(!config.oauth2_client.servers['auth_server'].client_id) { 98 | console.log('You must set a oauth2 client id in config (cf. README).'); 99 | process.exit(1); 100 | } 101 | serve(7070, function() { 102 | console.log('OAuth2 client server listning on http://localhost:7070'); 103 | }); 104 | } 105 | 106 | -------------------------------------------------------------------------------- /examples/facebook_oauth2_client/README: -------------------------------------------------------------------------------- 1 | 2 | To run this example: 3 | 4 | Add the following line to your /etc/hosts file: 5 | 6 | 127.0.0.1 example.com 7 | 8 | 9 | - Starts the server (Oauth2 client server): 10 | 11 | node server.js 12 | 13 | 14 | - Go to http://example.com:7070/ and try to log in with one of your FB account(s) 15 | 16 | 17 | NOTE: dating from 22/12/2010, it seems we can nolonger use 127.0.0.1 as redirect_url (as Facebook then redirects to 127.facebook.com and not 127.0.0.1). 18 | A solution is to set some host in /etc/hosts file. 19 | 20 | -------------------------------------------------------------------------------- /examples/facebook_oauth2_client/app.js: -------------------------------------------------------------------------------- 1 | var URL = require('url'); 2 | 3 | 4 | var app = function(req, res) { 5 | res.writeHead(200, {'Content-Type': 'text/html'}); 6 | var name = req.session.user_name; 7 | var loginout = null; 8 | if(!name) { 9 | name = 'anonymous'; 10 | loginout = 'Login'; 11 | } 12 | else { 13 | loginout = 'Logout'; 14 | } 15 | res.end('Hello '+ name +'!
'+loginout); 16 | }; 17 | 18 | 19 | exports.connector = function() { 20 | return function(req, res, next) { 21 | var url = URL.parse(req.url); 22 | if(url.pathname == "/") app(req, res); 23 | else next(); 24 | }; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/facebook_oauth2_client/server.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Add location of submodules to path: 4 | require.paths.unshift(__dirname + '/../dependencies/connect/lib'); 5 | require.paths.unshift(__dirname + '/../dependencies/cookie-sessions/lib'); 6 | require.paths.unshift(__dirname + '/../../vendors/nodetk/src'); 7 | require.paths.unshift(__dirname + '/../../vendors/node-base64'); 8 | require.paths.unshift(__dirname + '/../../src'); 9 | 10 | 11 | var querystring = require('querystring') 12 | , app = require('./app') 13 | , oauth2_client = require('oauth2_client') 14 | , connect = require('connect') 15 | , sessions = require('cookie-sessions') 16 | , web = require('nodetk/web') 17 | ; 18 | 19 | var base_url = 'http://example.com:7070'; 20 | var config = { 21 | oauth2_client: { 22 | client: { 23 | base_url: base_url, 24 | process_login_url: '/login/process/', 25 | redirect_uri: base_url + '/login/process/', 26 | login_url: '/login', 27 | logout_url: '/logout', 28 | default_redirection_url: '/' 29 | }, 30 | default_server: 'facebook.com', 31 | servers: { 32 | 'facebook.com': { 33 | server_authorize_endpoint: "https://graph.facebook.com/oauth/authorize", 34 | server_token_endpoint: 'https://graph.facebook.com/oauth/access_token', 35 | // These are the client id and secret of a FB application only registered 36 | // for testing purpose... don't use it in prod! 37 | client_id: "c5c0789871d6a65e485bc78235639d36", 38 | client_secret: 'ccd74db270c0b5f1dad0a603d36d6f1b', 39 | } 40 | } 41 | } 42 | }; 43 | 44 | var oauth2_client_options = { 45 | "facebook.com": { 46 | // To get info from access_token and set them in session 47 | treat_access_token: function(access_token, req, res, callback) { 48 | var params = {access_token: access_token}; 49 | web.GET('https://graph.facebook.com/me', params, 50 | function(status_code, headers, data) { 51 | console.log('Info given by FB:', data); 52 | var info = JSON.parse(data); 53 | req.session.user_name = info.name; 54 | callback(); 55 | }); 56 | } 57 | , transform_token_response: function(body) { 58 | // It seems Facebook does not respect OAuth2 draft 10 here, so we 59 | // have to override the method. 60 | var data = querystring.parse(body); 61 | if(!data.access_token) return null; 62 | return data; 63 | } 64 | } 65 | }; 66 | 67 | var server = connect.createServer( 68 | sessions({secret: '123abc', session_key: 'session'}) 69 | , oauth2_client.connector(config.oauth2_client, oauth2_client_options) 70 | , app.connector() 71 | ); 72 | 73 | var serve = function(port, callback) { 74 | server.listen(port, callback); 75 | } 76 | 77 | if(process.argv[1] == __filename) { 78 | serve(7070, function() { 79 | console.log('OAuth2 client server listning on http://localhost:7070'); 80 | }); 81 | } 82 | 83 | -------------------------------------------------------------------------------- /examples/simple_oauth2_client/README: -------------------------------------------------------------------------------- 1 | 2 | To run this example: 3 | 4 | - Set-up auth_server running with basic config and test data: 5 | 6 | In auth_server: 7 | node src/scripts/load_data.js # This will display a client_id value 8 | node src/server.js 9 | 10 | 11 | - Set-up the client in example: 12 | 13 | Set the client_id value prviously displayed in server.js (search for 'TODO'). 14 | 15 | Then run it: 16 | node src/server.js 17 | 18 | 19 | - Go to http://127.0.0.1:7070/ and try to log in 20 | Credentials: pruyssen@af83.com / 1234 21 | 22 | To clear the sessions : just remove the cookies. 23 | 24 | -------------------------------------------------------------------------------- /examples/simple_oauth2_client/app.js: -------------------------------------------------------------------------------- 1 | var URL = require('url'); 2 | 3 | 4 | var app = function(req, res) { 5 | res.writeHead(200, {'Content-Type': 'text/html'}); 6 | var name = req.session.user_email; 7 | var loginout = null; 8 | if(!name) { 9 | name = 'anonymous'; 10 | loginout = 'Login'; 11 | } 12 | else { 13 | loginout = 'Logout'; 14 | } 15 | res.end('Hello '+ name +'!
'+loginout); 16 | }; 17 | 18 | 19 | exports.connector = function() { 20 | return function(req, res, next) { 21 | var url = URL.parse(req.url); 22 | if(url.pathname == "/") app(req, res); 23 | else next(); 24 | }; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /examples/simple_oauth2_client/server.js: -------------------------------------------------------------------------------- 1 | var app = require('./app') 2 | , oauth2_client = require('../../') 3 | , connect = require('connect') 4 | , sessions = require('cookie-sessions') 5 | , request = require('request') 6 | ; 7 | 8 | var base_url = 'http://127.0.0.1:7071'; 9 | var config = { 10 | oauth2_client: { 11 | client: { 12 | base_url: base_url, 13 | process_login_url: '/login/process/', 14 | redirect_uri: base_url + '/login/process/', 15 | login_url: '/login', 16 | logout_url: '/logout', 17 | default_redirection_url: '/', 18 | }, 19 | default_server: "auth_server", 20 | servers: { 21 | "auth_server": { 22 | server_authorize_endpoint: 'http://localhost:7070/oauth2/authorize', 23 | server_token_endpoint: 'http://localhost:7070/oauth2/token', 24 | 25 | client_id: "4d540f5d1277275252000005", // TODO: define this before running 26 | client_secret: 'some secret string', 27 | name: 'geeks' 28 | } 29 | } 30 | } 31 | }; 32 | 33 | var oauth2_client_options = { 34 | "auth_server": { 35 | // To get info from access_token and set them in session 36 | treat_access_token: function(access_token, req, res, callback) { 37 | request.get({uri: 'http://localhost:7070/portable_contacts/@me/@self', 38 | headers: {"Authorization" : "OAuth "+ access_token.token.access_token}}, 39 | function(status_code, headers, data) { 40 | console.log(data); 41 | var info = JSON.parse(data); 42 | req.session.user_email = info.entry[0].displayName; 43 | callback(); 44 | }); 45 | } 46 | } 47 | }; 48 | 49 | var client = oauth2_client.createClient(config.oauth2_client, oauth2_client_options); 50 | 51 | var server = connect.createServer( 52 | sessions({secret: '123abc', session_key: 'session'}) 53 | , client.connector() 54 | , app.connector() 55 | ); 56 | 57 | var serve = function(port, callback) { 58 | server.listen(port, callback); 59 | }; 60 | 61 | if(process.argv[1] == __filename) { 62 | if(!config.oauth2_client.servers['auth_server'].client_id) { 63 | console.log('You must set a oauth2 client id in config (cf. README).'); 64 | process.exit(1); 65 | } 66 | serve(7071, function() { 67 | console.log('OAuth2 client server listening on http://localhost:7071'); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /lib/oauth2_client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * OAuth2 client module, defining: 3 | * - a connect middleware, that allows your application to act as a OAuth2 4 | * client. 5 | * - the method redirects_for_login (connector method must have been called 6 | * before), that redirects the user to the OAuth2 server for authentication. 7 | * 8 | */ 9 | var URL = require('url') 10 | , querystring = require('querystring') 11 | , serializer = require('serializer') 12 | , router = require('connect').router 13 | , request = require('request') 14 | ; 15 | 16 | function redirect(res, url) { 17 | res.writeHead(303, {'Location': url}); 18 | res.end(); 19 | } 20 | 21 | function server_error(res, err) { 22 | res.writeHead(500, {'Content-Type': 'text/plain'}); 23 | if(typeof err == "string") res.end(err); 24 | else { 25 | res.write('An error has occured: ' + err.message); 26 | res.write('\n\n'); 27 | res.end(err.stack); 28 | } 29 | } 30 | 31 | function OAuth2Client(conf, options) { 32 | this.config = conf; 33 | this.createSerializer(); 34 | // Build hash associating serverid and custom|default methods. 35 | this.methods = {}; 36 | for(var serverid in conf.servers) { 37 | this.addServer(serverid, conf.servers[serverid], (options[serverid] || {})); 38 | } 39 | } 40 | OAuth2Client.prototype = { 41 | /** 42 | * Add oauth2 server 43 | */ 44 | addServer: function(serverName, config, options) { 45 | this.methods[serverName] = {}; 46 | this.config.servers[serverName] = config; 47 | var self = this; 48 | ['valid_grant', 'treat_access_token', 'transform_token_response'].forEach(function(fctName) { 49 | self.methods[serverName][fctName] = options[fctName] || self[fctName].bind(self); 50 | }); 51 | }, 52 | 53 | createSerializer: function() { 54 | var cconf = this.config.client; 55 | this.serializer = serializer.createSecureSerializer(cconf.crypt_key, cconf.sign_key); 56 | }, 57 | /** 58 | * Given body answer to the HTTP request to obtain the access_token, 59 | * returns a JSON hash containing: 60 | * - access_token 61 | * - expires_in (optional) 62 | * - refresh_token (optional) 63 | * 64 | * If no access_token in there, return null; 65 | * 66 | */ 67 | transform_token_response : function(body) { 68 | return JSON.parse(body); 69 | }, 70 | 71 | /** 72 | * Valid the grant given by user requesting the OAuth2 server 73 | * at OAuth2 token endpoint. 74 | * 75 | * Arguments: 76 | * - data: hash containing: 77 | * - oauth2_server_id: string, the OAuth2 server is (ex: "facebook.com"). 78 | * - next_url: string, the next_url associated with the OAuth2 request. 79 | * - state: hash, state associated with the OAuth2 request. 80 | * - code: the authorization code given by OAuth2 server to user. 81 | * - callback: function to be called once grant is validated/rejected. 82 | * Called with the access_token returned by OAuth2 server as first 83 | * parameter. If given token might be null, meaning it was rejected 84 | * by OAuth2 server. 85 | * 86 | */ 87 | valid_grant : function(data, code, callback) { 88 | var self = this; 89 | var cconfig = this.config.client; 90 | var sconfig = this.config.servers[data.oauth2_server_id]; 91 | request.post({uri: sconfig.server_token_endpoint, 92 | headers: {'content-type': 'application/x-www-form-urlencoded'}, 93 | body: querystring.stringify({ 94 | grant_type: "authorization_code", 95 | client_id: sconfig.client_id, 96 | code: code, 97 | client_secret: sconfig.client_secret, 98 | redirect_uri: cconfig.redirect_uri 99 | }) 100 | }, function(error, response, body) { 101 | console.log(body); 102 | if (!error && response.statusCode == 200) { 103 | try { 104 | var methods = self.methods[data.oauth2_server_id]; 105 | var token = methods.transform_token_response(body) 106 | callback(null, token); 107 | } catch(err) { 108 | callback(err); 109 | } 110 | } else { 111 | // TODO: check if error code indicates problem on the client, 112 | console.error(error, body); 113 | callback(error); 114 | } 115 | }); 116 | }, 117 | 118 | /** 119 | * Make something with the access_token. 120 | * 121 | * This is the default implementation provided by this client. 122 | * This implementation does nothing, and the exact way this access_token 123 | * should be used is not specified by the OAuth2 spec (only how it should 124 | * be passed to resource provider). 125 | * 126 | * Arguments: 127 | * - data: hash containing: 128 | * - token: the body returned by the server in json. 129 | * The oauth_token param value to send will be: data.token.access_token. 130 | * - next_url: the url to redirect to after the OAuth2 process is done. 131 | * - state: hash, state associated with the OAuth2 process. 132 | * - req 133 | * - res 134 | * - callback: to be called when action is done. The request will be blocked 135 | * while this callback has not been called (so that the session can be 136 | * updated...). 137 | * 138 | */ 139 | treat_access_token: function(data, req, res, callback) { 140 | callback(); 141 | }, 142 | 143 | /** 144 | * Check the grant given by user to login in authserver is a good one. 145 | * 146 | * Arguments: 147 | * - req 148 | * - res 149 | */ 150 | auth_process_login: function(req, res) { 151 | var params = URL.parse(req.url, true).query 152 | , code = params.code 153 | , state = params.state 154 | ; 155 | 156 | if(!code) { 157 | res.writeHead(400, {'Content-Type': 'text/plain'}); 158 | return res.end('The "code" parameter is missing.'); 159 | } 160 | if(!state) { 161 | res.writeHead(400, {'Content-Type': 'text/plain'}); 162 | return res.end('The "state" parameter is missing.'); 163 | } 164 | try { 165 | state = this.serializer.parse(state); 166 | } catch(err) { 167 | res.writeHead(400, {'Content-Type': 'text/plain'}); 168 | return res.end('The "state" parameter is invalid.'); 169 | } 170 | var data = { 171 | oauth2_server_id: state[0] 172 | , next_url: state[1] 173 | , state: state[2] 174 | } 175 | var methods = this.methods[data.oauth2_server_id]; 176 | methods.valid_grant(data, code, function(err, token) { 177 | if (err) return server_error(res, err); 178 | if(!token) { 179 | res.writeHead(400, {'Content-Type': 'text/plain'}); 180 | res.end('Invalid grant.'); 181 | return; 182 | } 183 | data.token = token; 184 | methods.treat_access_token(data, req, res, function() { 185 | redirect(res, data.next_url); 186 | }, function(err){server_error(res, err)}); 187 | }); 188 | }, 189 | 190 | /** 191 | * Redirects the user to OAuth2 server for authentication. 192 | * 193 | * Arguments: 194 | * - oauth2_server_id: OAuth2 server identification (ex: "facebook.com"). 195 | * - res 196 | * - next_url: an url to redirect to once the process is complete. 197 | * - state: optional, a hash containing info you want to retrieve at the end 198 | * of the process. 199 | */ 200 | redirects_for_login: function(oauth2_server_id, res, next_url, state) { 201 | var sconfig = this.config.servers[oauth2_server_id]; 202 | var cconfig = this.config.client; 203 | var data = { 204 | client_id: sconfig.client_id, 205 | redirect_uri: cconfig.redirect_uri, 206 | response_type: 'code', 207 | state: this.serializer.stringify([oauth2_server_id, next_url, state || null]) 208 | }; 209 | var url = sconfig.server_authorize_endpoint +'?'+ querystring.stringify(data); 210 | redirect(res, url); 211 | }, 212 | 213 | /** 214 | * Returns value of next url query parameter if present, default otherwise. 215 | * The next query parameter should not contain the domain, the result will. 216 | */ 217 | nexturl_query: function(req, params) { 218 | if(!params) { 219 | params = URL.parse(req.url, true).query; 220 | } 221 | var cconfig = this.config.client; 222 | var next = params.next || cconfig.default_redirection_url; 223 | var url = cconfig.base_url + next; 224 | return url; 225 | }, 226 | 227 | /** 228 | * Logout the eventual logged in user. 229 | */ 230 | logout: function(req, res) { 231 | req.session = {}; 232 | redirect(res, this.nexturl_query(req)); 233 | }, 234 | 235 | /** 236 | * Triggers redirects_for_login with next param if present in url query. 237 | */ 238 | login: function(req, res) { 239 | var params = URL.parse(req.url, true).query; 240 | var oauth2_server_id = params.provider || this.config.default_server; 241 | var next_url = this.nexturl_query(req, params); 242 | this.redirects_for_login(oauth2_server_id, res, next_url); 243 | }, 244 | 245 | /** 246 | * Returns OAuth2 client connect middleware. 247 | * 248 | * This middleware will intercep requests aiming at OAuth2 client 249 | * and treat them. 250 | */ 251 | connector: function() { 252 | var client = this; 253 | var cconf = this.config.client; 254 | 255 | return router(function(app) { 256 | app.get(cconf.process_login_url, client.auth_process_login.bind(client)); 257 | app.get(cconf.login_url, client.login.bind(client)); 258 | app.get(cconf.logout_url, client.logout.bind(client)); 259 | }); 260 | } 261 | }; 262 | 263 | /** 264 | * Returns OAuth2 client 265 | * 266 | * Arguments: 267 | * - config: hash containing: 268 | * 269 | * - client, hash containing: 270 | * - base_url: The base URL of the OAuth2 client. 271 | * Ex: http://domain.com:8080 272 | * - process_login_url: the URL where to the OAuth2 server must redirect 273 | * the user when authenticated. 274 | * - login_url: the URL where the user must go to be redirected 275 | * to OAuth2 server for authentication. 276 | * - logout_url: the URL where the user must go so that his session is 277 | * cleared, and he is unlogged from client. 278 | * - default_redirection_url: default URL to redirect to after login / logout. 279 | * Optional, default to '/'. 280 | * - crypt_key: string, encryption key used to crypt information 281 | * contained in the states. 282 | * This is a symmetric key and must be kept secret. 283 | * - sign_key: string, signature key used to sign (HMAC) issued states. 284 | * This is a symmetric key and must be kept secret. 285 | * 286 | * - default_server: which server to use for default login when user 287 | * access login_url (ex: 'facebook.com'). 288 | * - servers: hash associating OAuth2 server ids (ex: "facebook.com") 289 | * with a hash containing (for each): 290 | * - server_authorize_endpoint: full URL, OAuth2 server token endpoint 291 | * (ex: "https://graph.facebook.com/oauth/authorize"). 292 | * - server_token_endpoint: full url, where to check the token 293 | * (ex: "https://graph.facebook.com/oauth/access_token"). 294 | * - client_id: the client id as registered by this OAuth2 server. 295 | * - client_secret: shared secret between client and this OAuth2 server. 296 | * 297 | * - options: optional, hash associating OAuth2 server ids 298 | * (ex: "facebook.com") with hash containing some options specific to the server. 299 | * Not all servers have to be listed here, neither all options. 300 | * Possible options: 301 | * - valid_grant: a function which will replace the default one 302 | * to check the grant is ok. You might want to use this shortcut if you 303 | * have a faster way of checking than requesting the OAuth2 server 304 | * with an HTTP request. 305 | * - treat_access_token: a function which will replace the 306 | * default one to do something with the access token. You will tipically 307 | * use that function to set some info in session. 308 | * - transform_token_response: a function which will replace 309 | * the default one to obtain a hash containing the access_token from 310 | * the OAuth2 server reply. This method should be provided if the 311 | * OAuth2 server we are requesting does not return JSON encoded data. 312 | * 313 | */ 314 | function createClient(conf, options) { 315 | conf.default_redirection_url = conf.default_redirection_url || '/'; 316 | options = options || {}; 317 | return new OAuth2Client(conf, options); 318 | } 319 | 320 | exports.createClient = createClient; 321 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name" : "oauth2-client" 2 | , "description" : "A library providing the bases to implement an OAuth2 client (as connect middleware)." 3 | , "version": "0.0.4" 4 | , "main": "./lib/oauth2_client.js" 5 | , "repository" : 6 | { "type" : "git" 7 | , "url" : "https://github.com/AF83/oauth2_client_node.git" 8 | } 9 | , "dependencies": { 10 | "connect": "", 11 | "request": "", 12 | "serializer":"" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/test_auth_process_login.js: -------------------------------------------------------------------------------- 1 | var assert = require('nodetk/testing/custom_assert') 2 | , tools = require('nodetk/testing/tools') 3 | , oauth2_client = require('../lib/oauth2_client') 4 | , serializer = require('serializer') 5 | ; 6 | 7 | var client; 8 | 9 | exports.setup = function(callback) { 10 | client = oauth2_client.createClient({ 11 | client: {}, 12 | servers: { 13 | 'serverid': { 14 | // valid_grant: function(_, _, callback) { 15 | // callback({}); 16 | // } 17 | } 18 | } 19 | }); 20 | //client.methods = {'serverid': client}; 21 | client.serializer = serializer; 22 | callback(); 23 | }; 24 | 25 | 26 | exports.tests = [ 27 | 28 | ['Missing code', 3, function() { 29 | var req = {url: '/'}; 30 | var res = tools.get_expected_res(400); 31 | client.auth_process_login(req, res); 32 | }], 33 | 34 | ['Missing state', 3, function() { 35 | var req = {url: '/?code=somecode'}; 36 | var res = tools.get_expected_res(400); 37 | client.auth_process_login(req, res); 38 | }], 39 | 40 | ['Invalid state', 3, function() { 41 | var req = {url: '/?code=somecode&state=toto'}; 42 | var res = tools.get_expected_res(400); 43 | client.auth_process_login(req, res); 44 | }], 45 | 46 | ['Invalid grant (no error)', 3, function() { 47 | var client = oauth2_client.createClient({ 48 | client: {}, 49 | servers: {'serverid': {}}}, 50 | {'serverid': { 51 | valid_grant: function(_, _, callback) {callback(null);} 52 | }} 53 | ); 54 | client.serializer = serializer; 55 | state = serializer.stringify(['serverid', 'nexturl', null]); 56 | var req = {url: '/?code=somecode&state='+state}; 57 | var res = tools.get_expected_res(400); 58 | client.auth_process_login(req, res); 59 | }], 60 | 61 | ['Invalid grant (error)', 3, function() { 62 | var client = oauth2_client.createClient({ 63 | client: {}, 64 | servers: {'serverid': {}}}, 65 | {'serverid':{ 66 | valid_grant: function(_, _, callback){callback('error')}}} 67 | ); 68 | client.serializer = serializer; 69 | var state = serializer.stringify(['serverid', 'nexturl', null]); 70 | var req = {url: '/?code=somecode&state='+state}; 71 | var res = tools.get_expected_res(500); 72 | client.auth_process_login(req, res); 73 | }], 74 | 75 | ['Valid grant, treat_access_token fallback', 3, function() { 76 | var client = oauth2_client.createClient({ 77 | client: {}, 78 | servers: {'serverid': {}}}, 79 | {'serverid':{ 80 | valid_grant: function(_, _, callback){callback('token')}, 81 | treat_access_token: function(_, _, _, callback) {callback('err')}}} 82 | ); 83 | client.serializer = serializer; 84 | var state = serializer.stringify(['serverid', 'nexturl', null]); 85 | var req = {url: '/?code=somecode&state='+state}; 86 | var res = tools.get_expected_res(500); 87 | client.auth_process_login(req, res); 88 | }], 89 | 90 | ['Valid grant', 2, function() { 91 | var client = oauth2_client.createClient({ 92 | client: {}, 93 | servers: {'serverid': {}}}, 94 | {'serverid':{ 95 | valid_grant: function(_, _, callback){callback(null, 'token')}, 96 | treat_access_token: function(_, _, _, callback) {callback()}}} 97 | ); 98 | client.serializer = serializer; 99 | var state = serializer.stringify(['serverid', 'next_url', null]); 100 | var req = {url: '/?code=somecode&state='+state}; 101 | var res = tools.get_expected_redirect_res("next_url"); 102 | client.auth_process_login(req, res); 103 | }] 104 | 105 | ]; 106 | -------------------------------------------------------------------------------- /tests/test_redirects_for_login.js: -------------------------------------------------------------------------------- 1 | var assert = require('nodetk/testing/custom_assert') 2 | , tools = require('nodetk/testing/tools') 3 | , querystring = require('querystring') 4 | , oauth2_client = require('../lib/oauth2_client') 5 | , serializer = require('serializer') 6 | ; 7 | 8 | 9 | var client; 10 | 11 | exports.module_init = function(callback) { 12 | client = oauth2_client.createClient({ 13 | client: { 14 | redirect_uri: 'http://site/process' 15 | } 16 | , default_server: "test" 17 | , servers: { 18 | "test": { 19 | server_authorize_endpoint: 'http://oauth2server/auth' 20 | , client_id: 'CLIENTID' 21 | } 22 | } 23 | }); 24 | client.serializer = serializer; 25 | callback(); 26 | }; 27 | 28 | exports.module_close = function(callback) { 29 | client.serializer = {}; 30 | callback(); 31 | }; 32 | 33 | 34 | exports.tests = [ 35 | 36 | ['no given state', 2, function() { 37 | var state = serializer.stringify(['test', 'http://next_url', null]); 38 | var qs = querystring.stringify({ 39 | client_id: 'CLIENTID' 40 | , redirect_uri: 'http://site/process' 41 | , response_type: 'code' 42 | , state: state 43 | }); 44 | var res = tools.get_expected_redirect_res("http://oauth2server/auth?" + qs); 45 | client.redirects_for_login('test', res, 'http://next_url'); 46 | }], 47 | 48 | ['given state', 2, function() { 49 | var state = serializer.stringify(['test', 'http://next_url', {"key": "val"}]); 50 | var qs = querystring.stringify({ 51 | client_id: 'CLIENTID' 52 | , redirect_uri: 'http://site/process' 53 | , response_type: 'code' 54 | , state: state 55 | }); 56 | var res = tools.get_expected_redirect_res("http://oauth2server/auth?" + qs); 57 | client.redirects_for_login('test', res, 'http://next_url', {'key': 'val'}); 58 | }], 59 | 60 | ]; 61 | 62 | -------------------------------------------------------------------------------- /tests/test_transform_token_response.js: -------------------------------------------------------------------------------- 1 | var assert = require('nodetk/testing/custom_assert'); 2 | var oauth2_client = require('../lib/oauth2_client'); 3 | 4 | exports.tests = [ 5 | 6 | ['JSON.parse is called and its result returned', 2, function() { 7 | var original_parse = JSON.parse 8 | , arg = "some body" 9 | , res = "some result" 10 | , res2 11 | ; 12 | JSON.parse = function(arg2) { 13 | assert.equal(arg, arg2); 14 | return res; 15 | }; 16 | var client = oauth2_client.createClient({ 17 | client: {} 18 | }); 19 | res2 = client.transform_token_response(arg); 20 | JSON.parse = original_parse; 21 | assert.equal(res, res2); 22 | }], 23 | 24 | ]; 25 | 26 | -------------------------------------------------------------------------------- /tests/test_valid_grant.js: -------------------------------------------------------------------------------- 1 | var assert = require('nodetk/testing/custom_assert') 2 | , oauth2_client = require('../lib/oauth2_client') 3 | , querystring = require('querystring') 4 | , extend = require('nodetk/utils').extend 5 | , request = require('request') 6 | ; 7 | 8 | var client; 9 | var original_post = request.post; 10 | 11 | exports.setup = function(callback) { 12 | client = oauth2_client.createClient({ 13 | client: { 14 | redirect_uri: 'REDIRECT_URI' 15 | } 16 | , default_server: 'serverid' 17 | , servers: { 18 | 'serverid': { 19 | client_id: "CLIENT_ID" 20 | , client_secret: "CLIENT_SECRET" 21 | } 22 | } 23 | }); 24 | request.post = original_post; 25 | callback(); 26 | } 27 | 28 | // Reinit stuff that whould have been mocked/faked... 29 | exports.module_close = function(callback) { 30 | request.post = original_post; 31 | callback(); 32 | }; 33 | 34 | exports.tests = [ 35 | 36 | ['Oauth2 client should HTTP request OAuth2 server with right parameters', 1, 37 | function() { 38 | var expected_sent_params = { 39 | code: "CODE" 40 | , grant_type: "authorization_code" 41 | , client_id: "CLIENT_ID" 42 | , client_secret: "CLIENT_SECRET" 43 | , redirect_uri: 'REDIRECT_URI' 44 | }; 45 | request.post = function(options, callback) { 46 | assert.deepEqual(querystring.parse(options.body), expected_sent_params) 47 | }; 48 | var data = {oauth2_server_id: 'serverid'}; 49 | client.valid_grant(data, 'CODE', null, null); 50 | }], 51 | 52 | ['OAuth2 server replies 400', 1, function() { 53 | // Callback must be called with null as token 54 | request.post = function(_, callback) {callback(null, {statusCode: 400}, '')}; 55 | var data = {oauth2_server_id: 'serverid'}; 56 | client.valid_grant(data, "some code", function(token) { 57 | assert.equal(token, null); 58 | }, function() { 59 | assert.ok(false, 'Should not be called'); 60 | }); 61 | }], 62 | 63 | ['OAuth2 server replies 200, grant valid, invalid answer', 1, function() { 64 | request.post = function(options, callback) {callback(null, {statusCode: 200}, 'invalid answer')}; 65 | var data = {oauth2_server_id: 'serverid'}; 66 | client.valid_grant(data, 'some code', function(err) { 67 | assert.ok(err); 68 | }); 69 | }], 70 | 71 | ['OAuth2 server replies 200, grant valid, valid answer', 2, function() { 72 | var expected_token = {access_token: 'sometoken'}; 73 | request.post = function(options, callback) { 74 | callback(null, {statusCode: 200}, JSON.stringify(expected_token)); 75 | }; 76 | client.methods = {'serverid': client}; 77 | var data = {oauth2_server_id: 'serverid'}; 78 | client.valid_grant(data, 'code', function(err, token) { 79 | assert.deepEqual(err, null, "should be null"); 80 | assert.deepEqual(expected_token, token); 81 | }); 82 | }], 83 | 84 | ]; 85 | --------------------------------------------------------------------------------