pax_global_header00006660000000000000000000000064127466072560014531gustar00rootroot0000000000000052 comment=07da76d8aaa129a988304a4337d8b2c7593b05bb xoauth2-1.2.0/000077500000000000000000000000001274660725600131235ustar00rootroot00000000000000xoauth2-1.2.0/.gitignore000066400000000000000000000000261274660725600151110ustar00rootroot00000000000000node_modules .DS_Storexoauth2-1.2.0/.jshintrc000066400000000000000000000005171274660725600147530ustar00rootroot00000000000000{ "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" ] }xoauth2-1.2.0/.travis.yml000066400000000000000000000007311274660725600152350ustar00rootroot00000000000000language: node_js node_js: - "0.10" - "0.11" before_install: - npm install -g grunt-cli notifications: email: recipients: - andris@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: falsexoauth2-1.2.0/CHANGELOG.md000066400000000000000000000004701274660725600147350ustar00rootroot00000000000000# Changelog ## v1.2.0 2016-07-29 * Added support for webtokens ## 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 xoauth2-1.2.0/Gruntfile.js000066400000000000000000000012031274660725600154140ustar00rootroot00000000000000'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']); };xoauth2-1.2.0/LICENSE000066400000000000000000000020421274660725600141260ustar00rootroot00000000000000Copyright (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. xoauth2-1.2.0/README.md000066400000000000000000000102771274660725600144110ustar00rootroot00000000000000xoauth2 ======= 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/identity/protocols/OAuth2WebServer#offline](https://developers.google.com/identity/protocols/OAuth2WebServer#offline) for generating the required credentials For Google service account the option values are: * **service** _(Required)_ Service account email. * **user** _(Required)_ User e-mail address * **scope** _(Required)_ OAuth2 scope. * **privateKey** _(Required)_ Private key issued for the service account in PEM format, as a string. * **serviceRequestTimeout** _(Optional)_ Expiration value to use in the token request in **seconds**. Maximum is 3600. * **accessUrl** _(Optional)_ Endpoint for token generation (defaults to *https://accounts.google.com/o/oauth2/token*) * **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 * **customParams** _(Optional)_ custom payload to send on getToken request ### 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@gmail.com", clientId: "{Client ID}", clientSecret: "{Client Secret}", refreshToken: "{User Refresh Token}", customHeaders: { "HeaderName": "HeaderValue" }, customPayload: { "payloadParamName": "payloadValue" } }); // ... or for a Google service account xoauth2gen = xoauth2.createXOAuth2Generator({ user: "user@gmail.com", service: '{Service Email Address}', scope: 'https://mail.google.com/', privateKey: '{Private Key in PEM format}' }); // 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** xoauth2-1.2.0/package.json000066400000000000000000000011001274660725600154010ustar00rootroot00000000000000{ "name": "xoauth2", "version": "1.2.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": "*" } } xoauth2-1.2.0/src/000077500000000000000000000000001274660725600137125ustar00rootroot00000000000000xoauth2-1.2.0/src/xoauth2.js000066400000000000000000000235221274660725600156460ustar00rootroot00000000000000'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'); var crypto = require('crypto'); /** * 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 || {}; if (options && options.service) { if (!options.scope || !options.privateKey || !options.user) { throw new Error('Options "scope", "privateKey" and "user" are required for service account!'); } var serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600); this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60; } 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; if (this.options.service) { // service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount var iat = Math.floor(Date.now() / 1000); // unix time var token = jwtSignRS256({ iss: this.options.service, scope: this.options.scope, sub: this.options.user, aud: this.options.accessUrl, iat: iat, exp: iat + this.options.serviceRequestTimeout, }, this.options.privateKey); urlOptions = { grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: token }; } else { // web app - https://developers.google.com/identity/protocols/OAuth2WebServer 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); } /** * Encodes a buffer or a string into Base64url format * * @param {Buffer|String} data The data to convert * @return {String} The encoded string */ function toBase64URL(data) { if (typeof data === 'string') { data = new Buffer(data); } return data.toString('base64') .replace(/=+/g, '') // remove '='s .replace(/\+/g, '-') // '+' → '-' .replace(/\//g, '_'); // '/' → '_' } /** * Header used for RS256 JSON Web Tokens, encoded as Base64URL. */ var JWT_RS256_HEADER = toBase64URL('{"alg":"RS256","typ":"JWT"}'); /** * Creates a JSON Web Token signed with RS256 (SHA256 + RSA) * Only this specific operation is needed so it's implemented here * instead of depending on jsonwebtoken. * * @param {Object} payload The payload to include in the generated token * @param {String} privateKey Private key in PEM format for signing the token * @return {String} The generated and signed token */ function jwtSignRS256(payload, privateKey) { var signaturePayload = JWT_RS256_HEADER + '.' + toBase64URL(JSON.stringify(payload)); var rs256Signer = crypto.createSign('RSA-SHA256'); rs256Signer.update(signaturePayload); var signature = toBase64URL(rs256Signer.sign(privateKey)); return signaturePayload + '.' + signature; } xoauth2-1.2.0/test/000077500000000000000000000000001274660725600141025ustar00rootroot00000000000000xoauth2-1.2.0/test/server.js000066400000000000000000000051531274660725600157520ustar00rootroot00000000000000'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); };xoauth2-1.2.0/test/xoauth2-test.js000066400000000000000000000074771274660725600170260ustar00rootroot00000000000000'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@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@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@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@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@example.com']); done(); }); }); it('should generate a fresh access token after timeout', function(done) { var xoauth2gen = xoauth2.createXOAuth2Generator({ user: 'test@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@example.com']); done(); }); }, 3000); }); it('should emit access token update', function(done) { var xoauth2gen = xoauth2.createXOAuth2Generator({ user: 'test@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@example.com', accessToken: users['test@example.com'], timeout: 3600 }); done(); }); xoauth2gen.getToken(function() {}); }); });