├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── package.json ├── src └── xoauth2.js └── test ├── server.js └── xoauth2-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 4, 3 | "node": true, 4 | "globalstrict": true, 5 | "evil": true, 6 | "unused": true, 7 | "undef": true, 8 | "newcap": true, 9 | "esnext": true, 10 | "curly": true, 11 | "eqeqeq": true, 12 | "expr": true, 13 | "node": true, 14 | 15 | "predef": [ 16 | "describe", 17 | "it", 18 | "beforeEach", 19 | "afterEach" 20 | ] 21 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.11" 5 | 6 | before_install: 7 | - npm install -g grunt-cli 8 | 9 | notifications: 10 | email: 11 | recipients: 12 | - andris@kreata.ee 13 | on_success: change 14 | on_failure: change 15 | webhooks: 16 | urls: 17 | - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc 18 | on_success: change # options: [always|never|change] default: always 19 | on_failure: always # options: [always|never|change] default: always 20 | on_start: false # default: false -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.2.0 2016-07-29 4 | 5 | * Added support for webtokens 6 | 7 | ## v1.1.0 2015-07-14 8 | 9 | * Added support for Yahoo specific headers (AVVS) 10 | 11 | ## v1.0.0 2014-10-13 12 | 13 | * Changed version numbering scheme 14 | * Added tests 15 | * Changed timeout values to always indicate seconds instead of milliseconds or Date objects 16 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | jshint: { 8 | all: ['src/*.js', 'test/*.js'], 9 | options: { 10 | jshintrc: '.jshintrc' 11 | } 12 | }, 13 | 14 | mochaTest: { 15 | all: { 16 | options: { 17 | reporter: 'spec' 18 | }, 19 | src: ['test/*-test.js'] 20 | } 21 | } 22 | }); 23 | 24 | // Load the plugin(s) 25 | grunt.loadNpmTasks('grunt-contrib-jshint'); 26 | grunt.loadNpmTasks('grunt-mocha-test'); 27 | 28 | // Tasks 29 | grunt.registerTask('default', ['jshint', 'mochaTest']); 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xoauth2 2 | ======= 3 | 4 | XOAuth2 token generation with node.js 5 | 6 | ## Installation 7 | 8 | npm install xoauth2 --save 9 | 10 | ## Usage 11 | 12 | **xoauth2** generates XOAUTH2 login tokens from provided Client and User credentials. 13 | 14 | Use `xoauth2.createXOAuth2Generator(options)` to initialize Token Generator 15 | 16 | Possible options values: 17 | 18 | * **user** _(Required)_ User e-mail address 19 | * **accessUrl** _(Optional)_ Endpoint for token generation (defaults to *https://accounts.google.com/o/oauth2/token*) 20 | * **clientId** _(Required)_ Client ID value 21 | * **clientSecret** _(Required)_ Client secret value 22 | * **refreshToken** _(Required)_ Refresh token for an user 23 | * **accessToken** _(Optional)_ initial access token. If not set, a new one will be generated 24 | * **timeout** _(Optional)_ TTL in **seconds** 25 | * **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) 26 | * **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) 27 | 28 | See [https://developers.google.com/identity/protocols/OAuth2WebServer#offline](https://developers.google.com/identity/protocols/OAuth2WebServer#offline) for generating the required credentials 29 | 30 | For Google service account the option values are: 31 | 32 | * **service** _(Required)_ Service account email. 33 | * **user** _(Required)_ User e-mail address 34 | * **scope** _(Required)_ OAuth2 scope. 35 | * **privateKey** _(Required)_ Private key issued for the service account in PEM format, as a string. 36 | * **serviceRequestTimeout** _(Optional)_ Expiration value to use in the token request in **seconds**. Maximum is 3600. 37 | * **accessUrl** _(Optional)_ Endpoint for token generation (defaults to *https://accounts.google.com/o/oauth2/token*) 38 | * **accessToken** _(Optional)_ initial access token. If not set, a new one will be generated 39 | * **timeout** _(Optional)_ TTL in **seconds** 40 | * **customHeaders** _(Optional)_ custom headers to send during token generation request 41 | * **customParams** _(Optional)_ custom payload to send on getToken request 42 | 43 | ### Methods 44 | 45 | #### Request an access token 46 | 47 | 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. 48 | 49 | #### Request for generating a new access token 50 | 51 | Use `xoauth2obj.generateToken(callback)` to get an access token. Cache will not be used and a new token is generated. 52 | 53 | #### Update access token values 54 | 55 | Use `xoauth2obj.updateToken(accessToken, timeout)` to set the new value for the xoauth2 access token. This function emits 'token' 56 | 57 | ### Events 58 | 59 | If a new token value has been set, `'token'` event is emitted. 60 | 61 | xoauth2obj.on("token", function(token){ 62 | console.log("User: ", token.user); // e-mail address 63 | console.log("New access token: ", token.accessToken); 64 | console.log("New access token timeout: ", token.timeout); // TTL in seconds 65 | }); 66 | 67 | ### Example 68 | 69 | var xoauth2 = require("xoauth2"), 70 | xoauth2gen; 71 | 72 | xoauth2gen = xoauth2.createXOAuth2Generator({ 73 | user: "user@gmail.com", 74 | clientId: "{Client ID}", 75 | clientSecret: "{Client Secret}", 76 | refreshToken: "{User Refresh Token}", 77 | customHeaders: { 78 | "HeaderName": "HeaderValue" 79 | }, 80 | customPayload: { 81 | "payloadParamName": "payloadValue" 82 | } 83 | }); 84 | 85 | // ... or for a Google service account 86 | xoauth2gen = xoauth2.createXOAuth2Generator({ 87 | user: "user@gmail.com", 88 | service: '{Service Email Address}', 89 | scope: 'https://mail.google.com/', 90 | privateKey: '{Private Key in PEM format}' 91 | }); 92 | 93 | // SMTP/IMAP 94 | xoauth2gen.getToken(function(err, token){ 95 | if(err){ 96 | return console.log(err); 97 | } 98 | console.log("AUTH XOAUTH2 " + token); 99 | }); 100 | 101 | // HTTP 102 | xoauth2gen.getToken(function(err, token, accessToken){ 103 | if(err){ 104 | return console.log(err); 105 | } 106 | console.log("Authorization: Bearer " + accessToken); 107 | }); 108 | 109 | ## License 110 | 111 | **MIT** 112 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xoauth2", 3 | "version": "1.2.0", 4 | "description": "XOAuth2 token generation for accessing GMail SMTP and IMAP", 5 | "main": "src/xoauth2.js", 6 | "scripts": { 7 | "test": "grunt" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/andris9/xoauth2.git" 12 | }, 13 | "keywords": [ 14 | "XOAUTH", 15 | "XOAUTH2", 16 | "Yahoo", 17 | "GMail", 18 | "SMTP", 19 | "IMAP" 20 | ], 21 | "author": "Andris Reinman", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "chai": "*", 25 | "grunt": "*", 26 | "grunt-contrib-jshint": "*", 27 | "grunt-mocha-test": "*", 28 | "sinon": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/xoauth2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Stream = require('stream').Stream; 4 | var utillib = require('util'); 5 | var querystring = require('querystring'); 6 | var http = require('http'); 7 | var https = require('https'); 8 | var urllib = require('url'); 9 | var crypto = require('crypto'); 10 | 11 | /** 12 | * Wrapper for new XOAuth2Generator. 13 | * 14 | * Usage: 15 | * 16 | * var xoauthgen = createXOAuth2Generator({}); 17 | * xoauthgen.getToken(function(err, xoauthtoken){ 18 | * socket.send('AUTH XOAUTH2 ' + xoauthtoken); 19 | * }); 20 | * 21 | * @param {Object} options See XOAuth2Generator for details 22 | * @return {Object} 23 | */ 24 | module.exports.createXOAuth2Generator = function(options) { 25 | return new XOAuth2Generator(options); 26 | }; 27 | 28 | /** 29 | * XOAUTH2 access_token generator for Gmail. 30 | * Create client ID for web applications in Google API console to use it. 31 | * See Offline Access for receiving the needed refreshToken for an user 32 | * https://developers.google.com/accounts/docs/OAuth2WebServer#offline 33 | * 34 | * @constructor 35 | * @param {Object} options Client information for token generation 36 | * @param {String} options.user (Required) User e-mail address 37 | * @param {String} options.clientId (Required) Client ID value 38 | * @param {String} options.clientSecret (Required) Client secret value 39 | * @param {String} options.refreshToken (Required) Refresh token for an user 40 | * @param {String} options.accessUrl (Optional) Endpoint for token generation, defaults to 'https://accounts.google.com/o/oauth2/token' 41 | * @param {String} options.accessToken (Optional) An existing valid accessToken 42 | * @param {int} options.timeout (Optional) TTL in seconds 43 | */ 44 | function XOAuth2Generator(options) { 45 | Stream.call(this); 46 | this.options = options || {}; 47 | 48 | if (options && options.service) { 49 | if (!options.scope || !options.privateKey || !options.user) { 50 | throw new Error('Options "scope", "privateKey" and "user" are required for service account!'); 51 | } 52 | 53 | var serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600); 54 | this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60; 55 | } 56 | 57 | this.options.accessUrl = this.options.accessUrl || 'https://accounts.google.com/o/oauth2/token'; 58 | this.options.customHeaders = this.options.customHeaders || {}; 59 | this.options.customParams = this.options.customParams || {}; 60 | 61 | this.token = this.options.accessToken && this.buildXOAuth2Token(this.options.accessToken) || false; 62 | this.accessToken = this.token && this.options.accessToken || false; 63 | 64 | var timeout = Math.max(Number(this.options.timeout) || 0, 0); 65 | this.timeout = timeout && Date.now() + timeout * 1000 || 0; 66 | } 67 | utillib.inherits(XOAuth2Generator, Stream); 68 | 69 | /** 70 | * Returns or generates (if previous has expired) a XOAuth2 token 71 | * 72 | * @param {Function} callback Callback function with error object and token string 73 | */ 74 | XOAuth2Generator.prototype.getToken = function(callback) { 75 | if (this.token && (!this.timeout || this.timeout > Date.now())) { 76 | return callback(null, this.token, this.accessToken); 77 | } 78 | this.generateToken(callback); 79 | }; 80 | 81 | /** 82 | * Updates token values 83 | * 84 | * @param {String} accessToken New access token 85 | * @param {Number} timeout Access token lifetime in seconds 86 | * 87 | * Emits 'token': { user: User email-address, accessToken: the new accessToken, timeout: TTL in seconds} 88 | */ 89 | XOAuth2Generator.prototype.updateToken = function(accessToken, timeout) { 90 | this.token = this.buildXOAuth2Token(accessToken); 91 | this.accessToken = accessToken; 92 | timeout = Math.max(Number(timeout) || 0, 0); 93 | this.timeout = timeout && Date.now() + timeout * 1000 || 0; 94 | 95 | this.emit('token', { 96 | user: this.options.user, 97 | accessToken: accessToken || '', 98 | timeout: Math.max(Math.floor((this.timeout - Date.now()) / 1000), 0) 99 | }); 100 | }; 101 | 102 | /** 103 | * Generates a new XOAuth2 token with the credentials provided at initialization 104 | * 105 | * @param {Function} callback Callback function with error object and token string 106 | */ 107 | XOAuth2Generator.prototype.generateToken = function(callback) { 108 | var urlOptions; 109 | if (this.options.service) { 110 | // service account - https://developers.google.com/identity/protocols/OAuth2ServiceAccount 111 | var iat = Math.floor(Date.now() / 1000); // unix time 112 | var token = jwtSignRS256({ 113 | iss: this.options.service, 114 | scope: this.options.scope, 115 | sub: this.options.user, 116 | aud: this.options.accessUrl, 117 | iat: iat, 118 | exp: iat + this.options.serviceRequestTimeout, 119 | }, this.options.privateKey); 120 | 121 | urlOptions = { 122 | grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', 123 | assertion: token 124 | }; 125 | } 126 | else { 127 | // web app - https://developers.google.com/identity/protocols/OAuth2WebServer 128 | urlOptions = { 129 | client_id: this.options.clientId || '', 130 | client_secret: this.options.clientSecret || '', 131 | refresh_token: this.options.refreshToken, 132 | grant_type: 'refresh_token' 133 | }; 134 | } 135 | 136 | for (var param in this.options.customParams) { 137 | urlOptions[param] = this.options.customParams[param]; 138 | } 139 | 140 | var payload = querystring.stringify(urlOptions); 141 | var self = this; 142 | postRequest(this.options.accessUrl, payload, this.options, function (error, response, body) { 143 | var data; 144 | 145 | if (error) { 146 | return callback(error); 147 | } 148 | 149 | try { 150 | data = JSON.parse(body.toString()); 151 | } catch (E) { 152 | return callback(E); 153 | } 154 | 155 | if (!data || typeof data !== 'object') { 156 | return callback(new Error('Invalid authentication response')); 157 | } 158 | 159 | if (data.error) { 160 | return callback(new Error(data.error)); 161 | } 162 | 163 | if (data.access_token) { 164 | self.updateToken(data.access_token, data.expires_in); 165 | return callback(null, self.token, self.accessToken); 166 | } 167 | 168 | return callback(new Error('No access token')); 169 | }); 170 | }; 171 | 172 | /** 173 | * Converts an access_token and user id into a base64 encoded XOAuth2 token 174 | * 175 | * @param {String} accessToken Access token string 176 | * @return {String} Base64 encoded token for IMAP or SMTP login 177 | */ 178 | XOAuth2Generator.prototype.buildXOAuth2Token = function(accessToken) { 179 | var authData = [ 180 | 'user=' + (this.options.user || ''), 181 | 'auth=Bearer ' + accessToken, 182 | '', 183 | '' 184 | ]; 185 | return new Buffer(authData.join('\x01'), 'utf-8').toString('base64'); 186 | }; 187 | 188 | /** 189 | * Custom POST request handler. 190 | * This is only needed to keep paths short in Windows – usually this module 191 | * is a dependency of a dependency and if it tries to require something 192 | * like the request module the paths get way too long to handle for Windows. 193 | * As we do only a simple POST request we do not actually require complicated 194 | * logic support (no redirects, no nothing) anyway. 195 | * 196 | * @param {String} url Url to POST to 197 | * @param {String|Buffer} payload Payload to POST 198 | * @param {Function} callback Callback function with (err, buff) 199 | */ 200 | function postRequest(url, payload, params, callback) { 201 | var options = urllib.parse(url), 202 | finished = false, 203 | response = null, 204 | req; 205 | 206 | options.method = 'POST'; 207 | 208 | /** 209 | * Cleanup all the event listeners registered on the request, and ensure that *callback* is only called one time 210 | * 211 | * @note passes all the arguments passed to this function to *callback* 212 | */ 213 | var cleanupAndCallback = function() { 214 | if (finished === true) { 215 | return; 216 | } 217 | finished = true; 218 | req.removeAllListeners(); 219 | if (response !== null) { 220 | response.removeAllListeners(); 221 | } 222 | callback.apply(null, arguments); 223 | }; 224 | 225 | req = (options.protocol === 'https:' ? https : http).request(options, function(res) { 226 | response = res; 227 | var data = []; 228 | var datalen = 0; 229 | 230 | res.on('data', function(chunk) { 231 | data.push(chunk); 232 | datalen += chunk.length; 233 | }); 234 | 235 | res.on('end', function() { 236 | return cleanupAndCallback(null, res, Buffer.concat(data, datalen)); 237 | }); 238 | 239 | res.on('error', function(err) { 240 | return cleanupAndCallback(err); 241 | }); 242 | }); 243 | 244 | req.on('error', function(err) { 245 | return cleanupAndCallback(err); 246 | }); 247 | 248 | if (payload) { 249 | req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); 250 | req.setHeader('Content-Length', typeof payload === 'string' ? Buffer.byteLength(payload) : payload.length); 251 | } 252 | 253 | for (var customHeaderName in params.customHeaders) { 254 | req.setHeader(customHeaderName, params.customHeaders[customHeaderName]); 255 | } 256 | 257 | req.end(payload); 258 | } 259 | 260 | /** 261 | * Encodes a buffer or a string into Base64url format 262 | * 263 | * @param {Buffer|String} data The data to convert 264 | * @return {String} The encoded string 265 | */ 266 | function toBase64URL(data) { 267 | if (typeof data === 'string') { 268 | data = new Buffer(data); 269 | } 270 | 271 | return data.toString('base64') 272 | .replace(/=+/g, '') // remove '='s 273 | .replace(/\+/g, '-') // '+' → '-' 274 | .replace(/\//g, '_'); // '/' → '_' 275 | } 276 | 277 | /** 278 | * Header used for RS256 JSON Web Tokens, encoded as Base64URL. 279 | */ 280 | var JWT_RS256_HEADER = toBase64URL('{"alg":"RS256","typ":"JWT"}'); 281 | 282 | /** 283 | * Creates a JSON Web Token signed with RS256 (SHA256 + RSA) 284 | * Only this specific operation is needed so it's implemented here 285 | * instead of depending on jsonwebtoken. 286 | * 287 | * @param {Object} payload The payload to include in the generated token 288 | * @param {String} privateKey Private key in PEM format for signing the token 289 | * @return {String} The generated and signed token 290 | */ 291 | function jwtSignRS256(payload, privateKey) { 292 | var signaturePayload = JWT_RS256_HEADER + '.' + toBase64URL(JSON.stringify(payload)); 293 | 294 | var rs256Signer = crypto.createSign('RSA-SHA256'); 295 | rs256Signer.update(signaturePayload); 296 | var signature = toBase64URL(rs256Signer.sign(privateKey)); 297 | 298 | return signaturePayload + '.' + signature; 299 | } 300 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Mock server for serving Oauth2 tokens 4 | 5 | var http = require('http'); 6 | var crypto = require('crypto'); 7 | var querystring = require('querystring'); 8 | 9 | module.exports = function(options) { 10 | return new OAuthServer(options); 11 | }; 12 | 13 | function OAuthServer(options) { 14 | this.options = options || {}; 15 | this.users = {}; 16 | this.tokens = {}; 17 | 18 | this.options.port = Number(this.options.port) || 3080; 19 | this.options.expiresIn = Number(this.options.expiresIn) || 3600; 20 | } 21 | 22 | OAuthServer.prototype.addUser = function(username, refreshToken) { 23 | 24 | var user = { 25 | username: username, 26 | refreshToken: refreshToken || crypto.randomBytes(10).toString('base64') 27 | }; 28 | 29 | this.users[username] = user; 30 | this.tokens[user.refreshToken] = username; 31 | 32 | return this.generateAccessToken(user.refreshToken); 33 | }; 34 | 35 | OAuthServer.prototype.generateAccessToken = function(refreshToken) { 36 | var username = this.tokens[refreshToken]; 37 | var accessToken = crypto.randomBytes(10).toString('base64'); 38 | 39 | if (!username) { 40 | return { 41 | error: 'Invalid refresh token' 42 | }; 43 | } 44 | 45 | this.users[username].accessToken = accessToken; 46 | this.users[username].expiresIn = Date.now + this.options.expiresIn * 1000; 47 | 48 | if (this.options.onUpdate) { 49 | this.options.onUpdate(username, accessToken); 50 | } 51 | 52 | return { 53 | access_token: accessToken, 54 | expires_in: this.options.expiresIn, 55 | token_type: 'Bearer' 56 | }; 57 | }; 58 | 59 | OAuthServer.prototype.validateAccessToken = function(username, accessToken) { 60 | if (!this.users[username] || 61 | this.users[username].accessToken !== accessToken || 62 | this.users[username].expiresIn < Date.now()) { 63 | 64 | return false; 65 | } else { 66 | return true; 67 | } 68 | }; 69 | 70 | OAuthServer.prototype.start = function(callback) { 71 | this.server = http.createServer((function(req, res) { 72 | var data = []; 73 | var datalen = 0; 74 | 75 | req.on('data', function(chunk) { 76 | if (!chunk || !chunk.length) { 77 | return; 78 | } 79 | 80 | data.push(chunk); 81 | datalen += chunk.length; 82 | }); 83 | 84 | req.on('end', (function() { 85 | var query = querystring.parse(Buffer.concat(data, datalen).toString()), 86 | response = this.generateAccessToken(query.refresh_token); 87 | 88 | res.writeHead(!response.error ? 200 : 401, { 89 | 'Content-Type': 'application/json' 90 | }); 91 | 92 | res.end(JSON.stringify(response)); 93 | }).bind(this)); 94 | }).bind(this)); 95 | 96 | this.server.listen(this.options.port, callback); 97 | }; 98 | 99 | OAuthServer.prototype.stop = function(callback) { 100 | this.server.close(callback); 101 | }; -------------------------------------------------------------------------------- /test/xoauth2-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | chai.Assertion.includeStack = true; 6 | 7 | var xoauth2 = require('../src/xoauth2'); 8 | var mockServer = require('./server'); 9 | 10 | describe('XOAuth2 tests', function() { 11 | this.timeout(10000); 12 | 13 | var server; 14 | var users = {}; 15 | var XOAUTH_PORT = 8993; 16 | 17 | beforeEach(function(done) { 18 | server = mockServer({ 19 | port: XOAUTH_PORT, 20 | onUpdate: function(username, accessToken) { 21 | users[username] = accessToken; 22 | } 23 | }); 24 | server.addUser('test@example.com', 'saladus'); 25 | server.start(done); 26 | }); 27 | 28 | afterEach(function(done) { 29 | server.stop(done); 30 | }); 31 | 32 | it('should get an existing access token', function(done) { 33 | var xoauth2gen = xoauth2.createXOAuth2Generator({ 34 | user: 'test@example.com', 35 | clientId: '{Client ID}', 36 | clientSecret: '{Client Secret}', 37 | refreshToken: 'saladus', 38 | accessUrl: 'http://localhost:' + XOAUTH_PORT + '/', 39 | accessToken: 'abc', 40 | timeout: 3600 41 | }); 42 | 43 | xoauth2gen.getToken(function(err, token, accessToken) { 44 | expect(err).to.not.exist; 45 | expect(accessToken).to.equal('abc'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should get an existing access token, no timeout', function(done) { 51 | var xoauth2gen = xoauth2.createXOAuth2Generator({ 52 | user: 'test@example.com', 53 | clientId: '{Client ID}', 54 | clientSecret: '{Client Secret}', 55 | refreshToken: 'saladus', 56 | accessUrl: 'http://localhost:' + XOAUTH_PORT + '/', 57 | accessToken: 'abc' 58 | }); 59 | 60 | xoauth2gen.getToken(function(err, token, accessToken) { 61 | expect(err).to.not.exist; 62 | expect(accessToken).to.equal('abc'); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should generate a fresh access token', function(done) { 68 | var xoauth2gen = xoauth2.createXOAuth2Generator({ 69 | user: 'test@example.com', 70 | clientId: '{Client ID}', 71 | clientSecret: '{Client Secret}', 72 | refreshToken: 'saladus', 73 | accessUrl: 'http://localhost:' + XOAUTH_PORT + '/', 74 | timeout: 3600 75 | }); 76 | 77 | xoauth2gen.getToken(function(err, token, accessToken) { 78 | expect(err).to.not.exist; 79 | expect(accessToken).to.equal(users['test@example.com']); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should generate a fresh access token after timeout', function(done) { 85 | var xoauth2gen = xoauth2.createXOAuth2Generator({ 86 | user: 'test@example.com', 87 | clientId: '{Client ID}', 88 | clientSecret: '{Client Secret}', 89 | refreshToken: 'saladus', 90 | accessUrl: 'http://localhost:' + XOAUTH_PORT + '/', 91 | accessToken: 'abc', 92 | timeout: 1 93 | }); 94 | 95 | setTimeout(function() { 96 | xoauth2gen.getToken(function(err, token, accessToken) { 97 | expect(err).to.not.exist; 98 | expect(accessToken).to.equal(users['test@example.com']); 99 | done(); 100 | }); 101 | }, 3000); 102 | }); 103 | 104 | it('should emit access token update', function(done) { 105 | var xoauth2gen = xoauth2.createXOAuth2Generator({ 106 | user: 'test@example.com', 107 | clientId: '{Client ID}', 108 | clientSecret: '{Client Secret}', 109 | refreshToken: 'saladus', 110 | accessUrl: 'http://localhost:' + XOAUTH_PORT + '/', 111 | timeout: 3600 112 | }); 113 | 114 | xoauth2gen.once('token', function(tokenData) { 115 | expect(tokenData).to.deep.equal({ 116 | user: 'test@example.com', 117 | accessToken: users['test@example.com'], 118 | timeout: 3600 119 | }); 120 | done(); 121 | }); 122 | 123 | xoauth2gen.getToken(function() {}); 124 | }); 125 | }); --------------------------------------------------------------------------------