[Pkg-javascript-commits] [node-xoauth2] 01/03: Imported Upstream version 1.1.0

Thorsten Alteholz alteholz at moszumanska.debian.org
Fri Feb 19 18:06:29 UTC 2016


This is an automated email from the git hooks/post-receive script.

alteholz pushed a commit to branch master
in repository node-xoauth2.

commit 90d031dbbe44e3768c2e36fa8cd8925f2fdeac13
Author: Thorsten Alteholz <debian at alteholz.de>
Date:   Fri Feb 19 19:06:19 2016 +0100

    Imported Upstream version 1.1.0
---
 .gitignore           |   2 +
 .jshintrc            |  21 +++++
 .travis.yml          |  20 +++++
 CHANGELOG.md         |  11 +++
 Gruntfile.js         |  30 +++++++
 LICENSE              |  19 +++++
 README.md            |  90 ++++++++++++++++++++
 package.json         |  30 +++++++
 src/xoauth2.js       | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++
 test/server.js       | 101 +++++++++++++++++++++++
 test/xoauth2-test.js | 125 ++++++++++++++++++++++++++++
 11 files changed, 676 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..28f1ba7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+.DS_Store
\ No newline at end of file
diff --git a/.jshintrc b/.jshintrc
new file mode 100644
index 0000000..bba5b74
--- /dev/null
+++ b/.jshintrc
@@ -0,0 +1,21 @@
+{
+    "indent": 4,
+    "node": true,
+    "globalstrict": true,
+    "evil": true,
+    "unused": true,
+    "undef": true,
+    "newcap": true,
+    "esnext": true,
+    "curly": true,
+    "eqeqeq": true,
+    "expr": true,
+    "node": true,
+
+    "predef": [
+        "describe",
+        "it",
+        "beforeEach",
+        "afterEach"
+    ]
+}
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..3232a95
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,20 @@
+language: node_js
+node_js:
+  - "0.10"
+  - "0.11"
+
+before_install:
+  - npm install -g grunt-cli
+
+notifications:
+  email:
+    recipients:
+      - andris at kreata.ee
+    on_success: change
+    on_failure: change
+  webhooks:
+    urls:
+      - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc
+    on_success: change  # options: [always|never|change] default: always
+    on_failure: always  # options: [always|never|change] default: always
+    on_start: false     # default: false
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..e10f292
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+
+## v1.1.0 2015-07-14
+
+  * Added support for Yahoo specific headers (AVVS)
+
+## v1.0.0 2014-10-13
+
+  * Changed version numbering scheme
+  * Added tests
+  * Changed timeout values to always indicate seconds instead of milliseconds or Date objects
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..40c216b
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,30 @@
+'use strict';
+
+module.exports = function(grunt) {
+
+    // Project configuration.
+    grunt.initConfig({
+        jshint: {
+            all: ['src/*.js', 'test/*.js'],
+            options: {
+                jshintrc: '.jshintrc'
+            }
+        },
+
+        mochaTest: {
+            all: {
+                options: {
+                    reporter: 'spec'
+                },
+                src: ['test/*-test.js']
+            }
+        }
+    });
+
+    // Load the plugin(s)
+    grunt.loadNpmTasks('grunt-contrib-jshint');
+    grunt.loadNpmTasks('grunt-mocha-test');
+
+    // Tasks
+    grunt.registerTask('default', ['jshint', 'mochaTest']);
+};
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..0107a13
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2014 Andris Reinman
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e58b286
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+xoauth2
+=======
+
+XOAuth2 token generation with node.js
+
+## Installation
+
+    npm install xoauth2
+
+## Usage
+
+**xoauth2** generates XOAUTH2 login tokens from provided Client and User credentials.
+
+Use `xoauth2.createXOAuth2Generator(options)` to initialize Token Generator
+
+Possible options values:
+
+  * **user** _(Required)_ User e-mail address
+  * **accessUrl** _(Optional)_ Endpoint for token generation (defaults to *https://accounts.google.com/o/oauth2/token*)
+  * **clientId** _(Required)_ Client ID value
+  * **clientSecret** _(Required)_ Client secret value
+  * **refreshToken** _(Required)_ Refresh token for an user
+  * **accessToken** _(Optional)_ initial access token. If not set, a new one will be generated
+  * **timeout** _(Optional)_ TTL in **seconds**
+  * **customHeaders** _(Optional)_ custom headers to send during token generation request [yahoo requires `Authorization: Basic Base64(clientId:clientSecret)` ](https://developer.yahoo.com/oauth2/guide/flows_authcode/#step-5-exchange-refresh-token-for-new-access-token)
+  * **customParams** _(Optional)_ custom payload to send on getToken request [yahoo requires redirect_uri to be specified](https://developer.yahoo.com/oauth2/guide/flows_authcode/#step-5-exchange-refresh-token-for-new-access-token)
+
+See [https://developers.google.com/accounts/docs/OAuth2WebServer#offline]() for generating the required credentials
+
+### Methods
+
+#### Request an access token
+
+Use `xoauth2obj.getToken(callback)` to get an access token. If a cached token is found and it should not be expired yet, the cached value will be used.
+
+#### Request for generating a new access token
+
+Use `xoauth2obj.generateToken(callback)` to get an access token. Cache will not be used and a new token is generated.
+
+#### Update access token values
+
+Use `xoauth2obj.updateToken(accessToken, timeout)` to set the new value for the xoauth2 access token. This function emits 'token'
+
+### Events
+
+If a new token value has been set, `'token'` event is emitted.
+
+    xoauth2obj.on("token", function(token){
+        console.log("User: ", token.user); // e-mail address
+        console.log("New access token: ", token.accessToken);
+        console.log("New access token timeout: ", token.timeout); // TTL in seconds
+    });
+
+### Example
+
+    var xoauth2 = require("xoauth2"),
+        xoauth2gen;
+
+    xoauth2gen = xoauth2.createXOAuth2Generator({
+        user: "user at gmail.com",
+        clientId: "{Client ID}",
+        clientSecret: "{Client Secret}",
+        refreshToken: "{User Refresh Token}",
+        customHeaders: {
+          "HeaderName": "HeaderValue"
+        },
+        customPayload: {
+          "payloadParamName": "payloadValue"
+        }
+    });
+
+    // SMTP/IMAP
+    xoauth2gen.getToken(function(err, token){
+        if(err){
+            return console.log(err);
+        }
+        console.log("AUTH XOAUTH2 " + token);
+    });
+
+    // HTTP
+    xoauth2gen.getToken(function(err, token, accessToken){
+        if(err){
+            return console.log(err);
+        }
+        console.log("Authorization: Bearer " + accessToken);
+    });
+
+## License
+
+**MIT**
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9d9abe8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,30 @@
+{
+  "name": "xoauth2",
+  "version": "1.1.0",
+  "description": "XOAuth2 token generation for accessing GMail SMTP and IMAP",
+  "main": "src/xoauth2.js",
+  "scripts": {
+    "test": "grunt"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/andris9/xoauth2.git"
+  },
+  "keywords": [
+    "XOAUTH",
+    "XOAUTH2",
+    "Yahoo",
+    "GMail",
+    "SMTP",
+    "IMAP"
+  ],
+  "author": "Andris Reinman",
+  "license": "MIT",
+  "devDependencies": {
+    "chai": "*",
+    "grunt": "*",
+    "grunt-contrib-jshint": "*",
+    "grunt-mocha-test": "*",
+    "sinon": "*"
+  }
+}
diff --git a/src/xoauth2.js b/src/xoauth2.js
new file mode 100644
index 0000000..153d92e
--- /dev/null
+++ b/src/xoauth2.js
@@ -0,0 +1,227 @@
+'use strict';
+
+var Stream = require('stream').Stream;
+var utillib = require('util');
+var querystring = require('querystring');
+var http = require('http');
+var https = require('https');
+var urllib = require('url');
+
+/**
+ * Wrapper for new XOAuth2Generator.
+ *
+ * Usage:
+ *
+ *     var xoauthgen = createXOAuth2Generator({});
+ *     xoauthgen.getToken(function(err, xoauthtoken){
+ *         socket.send('AUTH XOAUTH2 ' + xoauthtoken);
+ *     });
+ *
+ * @param {Object} options See XOAuth2Generator for details
+ * @return {Object}
+ */
+module.exports.createXOAuth2Generator = function(options) {
+    return new XOAuth2Generator(options);
+};
+
+/**
+ * XOAUTH2 access_token generator for Gmail.
+ * Create client ID for web applications in Google API console to use it.
+ * See Offline Access for receiving the needed refreshToken for an user
+ * https://developers.google.com/accounts/docs/OAuth2WebServer#offline
+ *
+ * @constructor
+ * @param {Object} options Client information for token generation
+ * @param {String} options.user         (Required) User e-mail address
+ * @param {String} options.clientId     (Required) Client ID value
+ * @param {String} options.clientSecret (Required) Client secret value
+ * @param {String} options.refreshToken (Required) Refresh token for an user
+ * @param {String} options.accessUrl    (Optional) Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token'
+ * @param {String} options.accessToken  (Optional) An existing valid accessToken
+ * @param {int}    options.timeout      (Optional) TTL in seconds
+ */
+function XOAuth2Generator(options) {
+    Stream.call(this);
+    this.options = options || {};
+
+    this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token';
+    this.options.customHeaders = this.options.customHeaders || {};
+    this.options.customParams = this.options.customParams || {};
+
+    this.token = this.options.accessToken && this.buildXOAuth2Token(this.options.accessToken) || false;
+    this.accessToken = this.token && this.options.accessToken || false;
+
+    var timeout = Math.max(Number(this.options.timeout) || 0, 0);
+    this.timeout = timeout && Date.now() + timeout * 1000 || 0;
+}
+utillib.inherits(XOAuth2Generator, Stream);
+
+/**
+ * Returns or generates (if previous has expired) a XOAuth2 token
+ *
+ * @param {Function} callback Callback function with error object and token string
+ */
+XOAuth2Generator.prototype.getToken = function(callback) {
+    if (this.token && (!this.timeout || this.timeout > Date.now())) {
+        return callback(null, this.token, this.accessToken);
+    }
+    this.generateToken(callback);
+};
+
+/**
+ * Updates token values
+ *
+ * @param {String} accessToken New access token
+ * @param {Number} timeout Access token lifetime in seconds
+ *
+ * Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds}
+ */
+XOAuth2Generator.prototype.updateToken = function(accessToken, timeout) {
+    this.token = this.buildXOAuth2Token(accessToken);
+    this.accessToken = accessToken;
+    timeout = Math.max(Number(timeout) || 0, 0);
+    this.timeout = timeout && Date.now() + timeout * 1000 || 0;
+
+    this.emit('token', {
+        user: this.options.user,
+        accessToken: accessToken || '',
+        timeout: Math.max(Math.floor((this.timeout - Date.now()) / 1000), 0)
+    });
+};
+
+/**
+ * Generates a new XOAuth2 token with the credentials provided at initialization
+ *
+ * @param {Function} callback Callback function with error object and token string
+ */
+XOAuth2Generator.prototype.generateToken = function(callback) {
+    var urlOptions = {
+            client_id: this.options.clientId || '',
+            client_secret: this.options.clientSecret || '',
+            refresh_token: this.options.refreshToken,
+            grant_type: 'refresh_token'
+        };
+
+    for (var param in this.options.customParams) {
+        urlOptions[param] = this.options.customParams[param];
+    }
+
+    var payload = querystring.stringify(urlOptions);
+    var self = this;
+    postRequest(this.options.accessUrl, payload, this.options, function (error, response, body) {
+        var data;
+
+        if (error) {
+            return callback(error);
+        }
+
+        try {
+            data = JSON.parse(body.toString());
+        } catch (E) {
+            return callback(E);
+        }
+
+        if (!data || typeof data !== 'object') {
+            return callback(new Error('Invalid authentication response'));
+        }
+
+        if (data.error) {
+            return callback(new Error(data.error));
+        }
+
+        if (data.access_token) {
+            self.updateToken(data.access_token, data.expires_in);
+            return callback(null, self.token, self.accessToken);
+        }
+
+        return callback(new Error('No access token'));
+    });
+};
+
+/**
+ * Converts an access_token and user id into a base64 encoded XOAuth2 token
+ *
+ * @param {String} accessToken Access token string
+ * @return {String} Base64 encoded token for IMAP or SMTP login
+ */
+XOAuth2Generator.prototype.buildXOAuth2Token = function(accessToken) {
+    var authData = [
+        'user=' + (this.options.user || ''),
+        'auth=Bearer ' + accessToken,
+        '',
+        ''
+    ];
+    return new Buffer(authData.join('\x01'), 'utf-8').toString('base64');
+};
+
+/**
+ * Custom POST request handler.
+ * This is only needed to keep paths short in Windows – usually this module
+ * is a dependency of a dependency and if it tries to require something
+ * like the request module the paths get way too long to handle for Windows.
+ * As we do only a simple POST request we do not actually require complicated
+ * logic support (no redirects, no nothing) anyway.
+ *
+ * @param {String} url Url to POST to
+ * @param {String|Buffer} payload Payload to POST
+ * @param {Function} callback Callback function with (err, buff)
+ */
+function postRequest(url, payload, params, callback) {
+    var options = urllib.parse(url),
+        finished = false,
+        response = null,
+        req;
+
+    options.method = 'POST';
+
+    /**
+     * Cleanup all the event listeners registered on the request, and ensure that *callback* is only called one time
+     *
+     * @note passes all the arguments passed to this function to *callback*
+     */
+    var cleanupAndCallback = function() {
+        if (finished === true) {
+            return;
+        }
+        finished = true;
+        req.removeAllListeners();
+        if (response !== null) {
+            response.removeAllListeners();
+        }
+        callback.apply(null, arguments);
+    };
+
+    req = (options.protocol === 'https:' ? https : http).request(options, function(res) {
+        response = res;
+        var data = [];
+        var datalen = 0;
+
+        res.on('data', function(chunk) {
+            data.push(chunk);
+            datalen += chunk.length;
+        });
+
+        res.on('end', function() {
+            return cleanupAndCallback(null, res, Buffer.concat(data, datalen));
+        });
+
+        res.on('error', function(err) {
+            return cleanupAndCallback(err);
+        });
+    });
+
+    req.on('error', function(err) {
+        return cleanupAndCallback(err);
+    });
+
+    if (payload) {
+        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');
+        req.setHeader('Content-Length', typeof payload === 'string' ? Buffer.byteLength(payload) : payload.length);
+    }
+
+    for (var customHeaderName in params.customHeaders) {
+      req.setHeader(customHeaderName, params.customHeaders[customHeaderName]);
+    }
+
+    req.end(payload);
+}
diff --git a/test/server.js b/test/server.js
new file mode 100644
index 0000000..3525f76
--- /dev/null
+++ b/test/server.js
@@ -0,0 +1,101 @@
+'use strict';
+
+// Mock server for serving Oauth2 tokens
+
+var http = require('http');
+var crypto = require('crypto');
+var querystring = require('querystring');
+
+module.exports = function(options) {
+    return new OAuthServer(options);
+};
+
+function OAuthServer(options) {
+    this.options = options || {};
+    this.users = {};
+    this.tokens = {};
+
+    this.options.port = Number(this.options.port) || 3080;
+    this.options.expiresIn = Number(this.options.expiresIn) || 3600;
+}
+
+OAuthServer.prototype.addUser = function(username, refreshToken) {
+
+    var user = {
+        username: username,
+        refreshToken: refreshToken || crypto.randomBytes(10).toString('base64')
+    };
+
+    this.users[username] = user;
+    this.tokens[user.refreshToken] = username;
+
+    return this.generateAccessToken(user.refreshToken);
+};
+
+OAuthServer.prototype.generateAccessToken = function(refreshToken) {
+    var username = this.tokens[refreshToken];
+    var accessToken = crypto.randomBytes(10).toString('base64');
+
+    if (!username) {
+        return {
+            error: 'Invalid refresh token'
+        };
+    }
+
+    this.users[username].accessToken = accessToken;
+    this.users[username].expiresIn = Date.now + this.options.expiresIn * 1000;
+
+    if (this.options.onUpdate) {
+        this.options.onUpdate(username, accessToken);
+    }
+
+    return {
+        access_token: accessToken,
+        expires_in: this.options.expiresIn,
+        token_type: 'Bearer'
+    };
+};
+
+OAuthServer.prototype.validateAccessToken = function(username, accessToken) {
+    if (!this.users[username] ||
+        this.users[username].accessToken !== accessToken ||
+        this.users[username].expiresIn < Date.now()) {
+
+        return false;
+    } else {
+        return true;
+    }
+};
+
+OAuthServer.prototype.start = function(callback) {
+    this.server = http.createServer((function(req, res) {
+        var data = [];
+        var datalen = 0;
+
+        req.on('data', function(chunk) {
+            if (!chunk || !chunk.length) {
+                return;
+            }
+
+            data.push(chunk);
+            datalen += chunk.length;
+        });
+
+        req.on('end', (function() {
+            var query = querystring.parse(Buffer.concat(data, datalen).toString()),
+                response = this.generateAccessToken(query.refresh_token);
+
+            res.writeHead(!response.error ? 200 : 401, {
+                'Content-Type': 'application/json'
+            });
+
+            res.end(JSON.stringify(response));
+        }).bind(this));
+    }).bind(this));
+
+    this.server.listen(this.options.port, callback);
+};
+
+OAuthServer.prototype.stop = function(callback) {
+    this.server.close(callback);
+};
\ No newline at end of file
diff --git a/test/xoauth2-test.js b/test/xoauth2-test.js
new file mode 100644
index 0000000..fa1b775
--- /dev/null
+++ b/test/xoauth2-test.js
@@ -0,0 +1,125 @@
+'use strict';
+
+var chai = require('chai');
+var expect = chai.expect;
+chai.Assertion.includeStack = true;
+
+var xoauth2 = require('../src/xoauth2');
+var mockServer = require('./server');
+
+describe('XOAuth2 tests', function() {
+    this.timeout(10000);
+
+    var server;
+    var users = {};
+    var XOAUTH_PORT = 8993;
+
+    beforeEach(function(done) {
+        server = mockServer({
+            port: XOAUTH_PORT,
+            onUpdate: function(username, accessToken) {
+                users[username] = accessToken;
+            }
+        });
+        server.addUser('test at example.com', 'saladus');
+        server.start(done);
+    });
+
+    afterEach(function(done) {
+        server.stop(done);
+    });
+
+    it('should get an existing access token', function(done) {
+        var xoauth2gen = xoauth2.createXOAuth2Generator({
+            user: 'test at example.com',
+            clientId: '{Client ID}',
+            clientSecret: '{Client Secret}',
+            refreshToken: 'saladus',
+            accessUrl: 'http://localhost:' + XOAUTH_PORT + '/',
+            accessToken: 'abc',
+            timeout: 3600
+        });
+
+        xoauth2gen.getToken(function(err, token, accessToken) {
+            expect(err).to.not.exist;
+            expect(accessToken).to.equal('abc');
+            done();
+        });
+    });
+
+    it('should get an existing access token, no timeout', function(done) {
+        var xoauth2gen = xoauth2.createXOAuth2Generator({
+            user: 'test at example.com',
+            clientId: '{Client ID}',
+            clientSecret: '{Client Secret}',
+            refreshToken: 'saladus',
+            accessUrl: 'http://localhost:' + XOAUTH_PORT + '/',
+            accessToken: 'abc'
+        });
+
+        xoauth2gen.getToken(function(err, token, accessToken) {
+            expect(err).to.not.exist;
+            expect(accessToken).to.equal('abc');
+            done();
+        });
+    });
+
+    it('should generate a fresh access token', function(done) {
+        var xoauth2gen = xoauth2.createXOAuth2Generator({
+            user: 'test at example.com',
+            clientId: '{Client ID}',
+            clientSecret: '{Client Secret}',
+            refreshToken: 'saladus',
+            accessUrl: 'http://localhost:' + XOAUTH_PORT + '/',
+            timeout: 3600
+        });
+
+        xoauth2gen.getToken(function(err, token, accessToken) {
+            expect(err).to.not.exist;
+            expect(accessToken).to.equal(users['test at example.com']);
+            done();
+        });
+    });
+
+    it('should generate a fresh access token after timeout', function(done) {
+        var xoauth2gen = xoauth2.createXOAuth2Generator({
+            user: 'test at example.com',
+            clientId: '{Client ID}',
+            clientSecret: '{Client Secret}',
+            refreshToken: 'saladus',
+            accessUrl: 'http://localhost:' + XOAUTH_PORT + '/',
+            accessToken: 'abc',
+            timeout: 1
+        });
+
+        setTimeout(function() {
+            xoauth2gen.getToken(function(err, token, accessToken) {
+                expect(err).to.not.exist;
+                expect(accessToken).to.equal(users['test at example.com']);
+                done();
+            });
+        }, 3000);
+    });
+
+    it('should emit access token update', function(done) {
+        var xoauth2gen = xoauth2.createXOAuth2Generator({
+            user: 'test at example.com',
+            clientId: '{Client ID}',
+            clientSecret: '{Client Secret}',
+            refreshToken: 'saladus',
+            accessUrl: 'http://localhost:' + XOAUTH_PORT + '/',
+            timeout: 3600
+        });
+
+        xoauth2gen.once('token', function(tokenData) {
+            expect(tokenData).to.deep.equal({
+                user: 'test at example.com',
+                accessToken: users['test at example.com'],
+                timeout: 3600
+            });
+            done();
+        });
+
+        xoauth2gen.getToken(function() {});
+    });
+});
\ No newline at end of file

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-xoauth2.git



More information about the Pkg-javascript-commits mailing list