├── .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 = '
';
11 | loginout += '';
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 |
--------------------------------------------------------------------------------