[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