├── .gitignore ├── README.md ├── index.coffee ├── index.js ├── lib └── google-login-phantomjs-script.coffee ├── package.json └── test └── test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | config 2 | node_modules 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-google-oauth2 2 | ================== 3 | 4 | Google OAuth2 authentication tools for GoogleDrive, GMail and many other Google APIs. 5 | With borrowed code from [node-gAUth by Ben Lyaunzon](https://github.com/lyaunzbe/node-gAuth). 6 | 7 | For some reason Google decided to document their new APIs on an SDK level only, e.g. they do not provide documentation for the REST APIs. 8 | Instead they talk about how to use their client libraries for "popular" platforms. Server-side JS is not among them. This sucks. 9 | 10 | Earlier versions of Google APIs were documented on a REST level. They are deprecated and, at least in one instance, [Google broke an API](http://stackoverflow.com/questions/13552687/google-document-list-api-v2-regression-feed-does-not-contain-all-documents) and [does not even acknowledge the fact](http://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=3274). 11 | This sucks even more. 12 | 13 | end of rant. 14 | 15 | Preparation 16 | ---------- 17 | 18 | First you need to register your application or service with [Google's API console](https://code.google.com/apis/console) aka Dashboard 19 | - go to *Services* and enable the API you want to use 20 | - go to *API Access* and create a Client ID 21 | - copy the strings labeled *Client-ID* and *Client-Secret* 22 | - paste them into a file called oauth2-config.js and put it into your project (but don't check it in!) 23 | 24 | oauth2-config.js should look something like this: 25 | 26 | module.exports = { 27 | // these are from the Google API console web interface 28 | // https://code.google.com/apis/console 29 | // (Section CLient ID for installed Application) 30 | client_id: "xxxxxxxxxxxx.apps.googleusercontent.com", 31 | client_secret: "xxxxxxxx_xxxxxxxxxxxxxxx", 32 | }; 33 | 34 | 35 | Auth Code 36 | --------- 37 | 38 | Before your app can use a user's data, it must be authorized by the user for a certain set of permissions (a "scope"). 39 | More about scopes [here](https://developers.google.com/drive/training/drive-apps/auth/scopes). 40 | 41 | With the client id, client secret and the scope, you can now request an auth code for a particular google account. 42 | 43 | config = require("oauth2-config.js") 44 | goauth2 = require("google-oauth2")(config) 45 | scope = "https://www.googleapis.com/auth/userinfo.profile" 46 | 47 | goauth2.getAuthCode scope, (err, auth_code) -> 48 | console.log auth_code 49 | 50 | Normally this would happen in a web session. Because the user needs a way to grant permissions, the code above will open a 51 | local web browser. Here you can log into your account and grant the permissions defined by the scope. 52 | The Google Auth server will then redirect to localhost:3000 where a temporary http server (created inside *getAuthCode*) will 53 | receive the auth code. 54 | 55 | Because opening a web browser in a headless server environment often is not an option, we should think about a different solution. See [Authorization Automation](#authorization-automation) below. 56 | 57 | Tokens 58 | ------ 59 | 60 | With the auth code, you can now request tokens. 61 | 62 | goauth2.getTokensForAuthCode auth_code, (err, result) -> 63 | console.log result.access_token 64 | console.log result.refresh_token 65 | 66 | The access token is needed to make actual API calls. However, it will expire after a while. 67 | (typically one hour). The refresh token on the other hand can be used to get a fresh access token for another 68 | hour of API fun. 69 | 70 | goauth2.getAccessTokenForRefreshToken refresh_token, (err, result) -> 71 | console.log result.access_token 72 | 73 | Make API calls 74 | -------------- 75 | 76 | Here's an example of how you would use an access token in an HTTP Authorization header: 77 | 78 | curl -H "Authorization: Bearer {access_token}" \ 79 | -H "Content-Type: application/json" \ 80 | https://www.googleapis.com/drive/v2/files 81 | 82 | NOTE: This will only succeed if you requested the google drive metadata scope 83 | 84 | Authorization Automation 85 | ------------------------ 86 | 87 | As said before, in a headless server environment it is not really an opten to open a web browser to grant access permissions. 88 | If your service want to access its own google account rather than arbitrary user accounts, for example to store and share google drive documents with your users, there might be another option. 89 | 90 | In that case you could put a google account name and password in oauth2-config.js and automate the following steps: 91 | - open a headless phantomJS browser session that is instrumented with a script 92 | - navigate to the google account login page 93 | - autofill account name and password and login 94 | - navigate to the grant permission page and automatically click the blue button 95 | 96 | This would not only improve this module's unit tests, it would also be a solution for server-side 97 | authorization of the service's own google account. And this process could be integrated with automatic provisioning. (Infrastructure is Code) 98 | 99 | What can I say? It nearly works! [This phantomjs script](https://github.com/regular/node-google-oauth2/blob/master/lib/google-login-phantomjs-script.coffee) tries to performs the steps above. All you need to do to see it fail is uncomment the first test in [test/test.coffee](https://github.com/regular/node-google-oauth2/blob/master/test/test.coffee#L12-L19) 100 | and run 101 | 102 | npm test 103 | 104 | Everything works except for one thing: clicking the blue button. And I have no idea why it doesn't. When the test times out it automatically makes a screenshot of the browser session for you to check out. 105 | Pull Requests are very welcome! 106 | 107 | And now go ahead and write some Google API wrapers! 108 | 109 | -- Jan 110 | -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | request = require("request") 2 | exec = require("child_process").exec 3 | querystring = require("querystring") 4 | 5 | endpoint_auth = "https://accounts.google.com/o/oauth2/auth" 6 | endpoint_token = "https://accounts.google.com/o/oauth2/token" 7 | 8 | module.exports = (opts) -> 9 | opts.redirect_uri or= "http://localhost:3000/callback" 10 | opts.refresh_tokens or= [] 11 | 12 | ### 13 | Constructs a google OAuth2 request url using the provided opts. 14 | Spawns an http server to handle the redirect. Once user authenticates 15 | and the server parses the auth code, the server process is closed 16 | (Assuming the user has closed the window. For future, add redirect after 17 | authentication to a page with instructions to close tab/window). 18 | 19 | Some scopes: 20 | https://www.googleapis.com/auth/userinfo.profile 21 | https://www.googleapis.com/auth/drive.readonly.metadata 22 | ### 23 | 24 | getAuthCode = (scope, openURICallback, callback) -> 25 | if arguments.length is 2 26 | callback = openURICallback 27 | openURICallback = null 28 | 29 | # default: open the system's web browser 30 | openURICallback or= (uri, cb) -> 31 | #TODO: Make OS agnostic w/ xdg-open, open, etc. 32 | exec "open '#{uri}'", cb 33 | 34 | qs = 35 | response_type: "code" 36 | client_id: opts.client_id 37 | redirect_uri: opts.redirect_uri 38 | scope: scope 39 | 40 | uri = endpoint_auth + "?" + querystring.stringify(qs) 41 | 42 | console.log "Starting server ..." 43 | 44 | opened = true 45 | 46 | server = require("http").createServer((req, res) -> 47 | if opened 48 | console.log "server receives request for", req.url 49 | console.log "Stopping server ..." 50 | server.close() 51 | res.end "ok" 52 | 53 | code = querystring.parse(req.url.split("?")[1]).code 54 | if code 55 | opened = false 56 | callback null, code 57 | 58 | ).listen(3000) 59 | 60 | openURICallback uri, (err) -> 61 | if (err) then callback err 62 | console.log "uri opened ..." 63 | 64 | ### 65 | Given the acquired authorization code and the provided opts, 66 | construct a POST request to acquire the access token and refresh 67 | token. 68 | 69 | @param {String} code Can be acquired with getAuthCode 70 | ### 71 | getTokensForAuthCode = (code, callback) -> 72 | form = 73 | code: code 74 | client_id: opts.client_id 75 | client_secret: opts.client_secret 76 | redirect_uri: opts.redirect_uri 77 | grant_type: "authorization_code" 78 | 79 | request.post 80 | url: endpoint_token 81 | form: form 82 | , (err, req, body) -> 83 | if err? then return callback err 84 | callback null, JSON.parse(body) 85 | 86 | ### 87 | Given a refresh token and provided opts, returns a new 88 | access token. Tyically the access token is valid for an hour. 89 | 90 | @param {String} refresh_token The refresh token. Can be acquired 91 | through getTokensForAuthCode function. 92 | ### 93 | getAccessTokenForRefreshToken = (refresh_token, callback) -> 94 | form = 95 | refresh_token: refresh_token 96 | client_id: opts.client_id 97 | client_secret: opts.client_secret 98 | grant_type: "refresh_token" 99 | 100 | request.post 101 | url: endpoint_token 102 | form: form 103 | , (err, res, body) -> 104 | if err? or res.statusCode isnt 200 then return callback err or body 105 | callback null, JSON.parse(body) 106 | 107 | ### 108 | Given google account name and password, use phantomJS to login a user into google services. 109 | Afterwards navigate the browser to a given URL. 110 | The purpose of this is to allow a command line tool to authorize 111 | an application (or itself) to access the user's data. 112 | ### 113 | 114 | automaticGoogleWebLogin = (username, password, followUpURI, cb) -> 115 | childProcess = require "child_process" 116 | phantomjs = require "phantomjs" 117 | path = require "path" 118 | 119 | childArgs = [ 120 | path.join(__dirname, "lib/google-login-phantomjs-script.coffee") 121 | username 122 | password 123 | followUpURI 124 | ] 125 | 126 | child = childProcess.spawn phantomjs.path, childArgs 127 | 128 | child.stdout.on "data", (data) -> 129 | process.stdout.write data 130 | 131 | child.stderr.on "data", (data) -> 132 | process.stderr.write data 133 | 134 | child.on "exit", (code) -> 135 | console.log "phantomjs exited with code:", code 136 | if code isnt 0 137 | return cb "phantomjs exited with code #{code}", code 138 | cb null 139 | 140 | 141 | authorizeApplication = (username, password, scope, cb) -> 142 | getAuthCode scope, (uri, cb) -> 143 | automaticGoogleWebLogin username, password, uri, cb 144 | , cb 145 | 146 | ### 147 | Convenience dunction 148 | Use this to get an access token for a specific scope 149 | ### 150 | getAccessToken: (scope, cb) -> 151 | refresh_token = opts.refresh_tokens[scope] 152 | if refresh_token 153 | getAccessTokenForRefreshToken refresh_token, cb 154 | else 155 | async.waterfall [ 156 | getAuthCode, 157 | getTokensForAuthCode 158 | ], (err, result) -> 159 | # store the refresh_token for future use 160 | opts.refresh_token[scope] = result?.refresh_token 161 | cb err, result?.access_token 162 | 163 | return { 164 | getAuthCode: getAuthCode 165 | authorizeApplication: authorizeApplication 166 | getTokensForAuthCode: getTokensForAuthCode 167 | getAccessTokenForRefreshToken: getAccessTokenForRefreshToken 168 | } 169 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var endpoint_auth, endpoint_token, exec, querystring, request; 4 | 5 | request = require("request"); 6 | 7 | exec = require("child_process").exec; 8 | 9 | querystring = require("querystring"); 10 | 11 | endpoint_auth = "https://accounts.google.com/o/oauth2/auth"; 12 | 13 | endpoint_token = "https://accounts.google.com/o/oauth2/token"; 14 | 15 | module.exports = function(opts) { 16 | var authorizeApplication, automaticGoogleWebLogin, getAccessTokenForRefreshToken, getAuthCode, getTokensForAuthCode; 17 | opts.redirect_uri || (opts.redirect_uri = "http://localhost:3000/callback"); 18 | opts.refresh_tokens || (opts.refresh_tokens = []); 19 | /* 20 | Constructs a google OAuth2 request url using the provided opts. 21 | Spawns an http server to handle the redirect. Once user authenticates 22 | and the server parses the auth code, the server process is closed 23 | (Assuming the user has closed the window. For future, add redirect after 24 | authentication to a page with instructions to close tab/window). 25 | 26 | Some scopes: 27 | https://www.googleapis.com/auth/userinfo.profile 28 | https://www.googleapis.com/auth/drive.readonly.metadata 29 | */ 30 | 31 | getAuthCode = function(scope, openURICallback, callback) { 32 | var opened, qs, server, uri; 33 | if (arguments.length === 2) { 34 | callback = openURICallback; 35 | openURICallback = null; 36 | } 37 | openURICallback || (openURICallback = function(uri, cb) { 38 | return exec("open '" + uri + "'", cb); 39 | }); 40 | qs = { 41 | response_type: "code", 42 | client_id: opts.client_id, 43 | redirect_uri: opts.redirect_uri, 44 | scope: scope 45 | }; 46 | uri = endpoint_auth + "?" + querystring.stringify(qs); 47 | console.log("Starting server ..."); 48 | opened = true; 49 | server = require("http").createServer(function(req, res) { 50 | var code; 51 | if (opened) { 52 | console.log("server receives request for", req.url); 53 | console.log("Stopping server ..."); 54 | server.close(); 55 | res.end("ok"); 56 | } 57 | code = querystring.parse(req.url.split("?")[1]).code; 58 | if (code) { 59 | opened = false; 60 | return callback(null, code); 61 | } 62 | }).listen(3000); 63 | return openURICallback(uri, function(err) { 64 | if (err) { 65 | callback(err); 66 | } 67 | return console.log("uri opened ..."); 68 | }); 69 | }; 70 | /* 71 | Given the acquired authorization code and the provided opts, 72 | construct a POST request to acquire the access token and refresh 73 | token. 74 | 75 | @param {String} code Can be acquired with getAuthCode 76 | */ 77 | 78 | getTokensForAuthCode = function(code, callback) { 79 | var form; 80 | form = { 81 | code: code, 82 | client_id: opts.client_id, 83 | client_secret: opts.client_secret, 84 | redirect_uri: opts.redirect_uri, 85 | grant_type: "authorization_code" 86 | }; 87 | return request.post({ 88 | url: endpoint_token, 89 | form: form 90 | }, function(err, req, body) { 91 | if (err != null) { 92 | return callback(err); 93 | } 94 | return callback(null, JSON.parse(body)); 95 | }); 96 | }; 97 | /* 98 | Given a refresh token and provided opts, returns a new 99 | access token. Tyically the access token is valid for an hour. 100 | 101 | @param {String} refresh_token The refresh token. Can be acquired 102 | through getTokensForAuthCode function. 103 | */ 104 | 105 | getAccessTokenForRefreshToken = function(refresh_token, callback) { 106 | var form; 107 | form = { 108 | refresh_token: refresh_token, 109 | client_id: opts.client_id, 110 | client_secret: opts.client_secret, 111 | grant_type: "refresh_token" 112 | }; 113 | return request.post({ 114 | url: endpoint_token, 115 | form: form 116 | }, function(err, res, body) { 117 | if ((err != null) || res.statusCode !== 200) { 118 | return callback(err || body); 119 | } 120 | return callback(null, JSON.parse(body)); 121 | }); 122 | }; 123 | /* 124 | Given google account name and password, use phantomJS to login a user into google services. 125 | Afterwards navigate the browser to a given URL. 126 | The purpose of this is to allow a command line tool to authorize 127 | an application (or itself) to access the user's data. 128 | */ 129 | 130 | automaticGoogleWebLogin = function(username, password, followUpURI, cb) { 131 | var child, childArgs, childProcess, path, phantomjs; 132 | childProcess = require("child_process"); 133 | phantomjs = require("phantomjs"); 134 | path = require("path"); 135 | childArgs = [path.join(__dirname, "lib/google-login-phantomjs-script.coffee"), username, password, followUpURI]; 136 | child = childProcess.spawn(phantomjs.path, childArgs); 137 | child.stdout.on("data", function(data) { 138 | return process.stdout.write(data); 139 | }); 140 | child.stderr.on("data", function(data) { 141 | return process.stderr.write(data); 142 | }); 143 | return child.on("exit", function(code) { 144 | console.log("phantomjs exited with code:", code); 145 | if (code !== 0) { 146 | return cb("phantomjs exited with code " + code, code); 147 | } 148 | return cb(null); 149 | }); 150 | }; 151 | authorizeApplication = function(username, password, scope, cb) { 152 | return getAuthCode(scope, function(uri, cb) { 153 | return automaticGoogleWebLogin(username, password, uri, cb); 154 | }, cb); 155 | }; 156 | ({ 157 | /* 158 | Convenience dunction 159 | Use this to get an access token for a specific scope 160 | */ 161 | 162 | getAccessToken: function(scope, cb) { 163 | var refresh_token; 164 | refresh_token = opts.refresh_tokens[scope]; 165 | if (refresh_token) { 166 | return getAccessTokenForRefreshToken(refresh_token, cb); 167 | } else { 168 | return async.waterfall([getAuthCode, getTokensForAuthCode], function(err, result) { 169 | opts.refresh_token[scope] = result != null ? result.refresh_token : void 0; 170 | return cb(err, result != null ? result.access_token : void 0); 171 | }); 172 | } 173 | } 174 | }); 175 | return { 176 | getAuthCode: getAuthCode, 177 | authorizeApplication: authorizeApplication, 178 | getTokensForAuthCode: getTokensForAuthCode, 179 | getAccessTokenForRefreshToken: getAccessTokenForRefreshToken 180 | }; 181 | }; 182 | 183 | }).call(this); 184 | -------------------------------------------------------------------------------- /lib/google-login-phantomjs-script.coffee: -------------------------------------------------------------------------------- 1 | page = require('webpage').create() 2 | system = require('system') 3 | 4 | debug = true 5 | jqueryURI = "http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" 6 | 7 | if debug 8 | page.onConsoleMessage = (msg) -> console.log(msg) 9 | page.onAlert = (msg) -> console.log(msg) 10 | 11 | #page.onResourceRequested = (request) -> 12 | # console.log 'Request ' + JSON.stringify request, undefined, 4 13 | # console.log request.url 14 | #page.onResourceReceived = (response) -> 15 | # console.log 'Receive ' + JSON.stringify response, undefined, 4 16 | 17 | page.onUrlChanged = (url) -> 18 | #console.log "new url: #{url}" 19 | if /settings\/account/.test url 20 | console.log "logged in #{system.args[1]}" 21 | followUpURI = system.args[3] 22 | # do on next tick (to prevent from crashing when exit is called from an event handler) 23 | setTimeout -> 24 | confirmPermissions followUpURI 25 | , 1 26 | 27 | page.open "https://accounts.google.com/ServiceLogin", (status) -> 28 | if status is "success" 29 | page.includeJs jqueryURI, -> 30 | page.evaluate (username, password) -> 31 | #alert "* Script running in the Page context." 32 | $("input").each -> 33 | console.log "input", $(this).attr("type"), this.id, this.name, $(this).val() 34 | $("input[type=email]").val(username) 35 | $("input[type=password]").val(password) 36 | $("input[type=submit]").click() 37 | , system.args[1], system.args[2] 38 | 39 | setTimeout -> 40 | page.render "timeout.png" 41 | console.log("TIMEOUT! See timeout.png.") 42 | phantom.exit 1 43 | , 15000 44 | 45 | else 46 | console.log "... fail! Check the $PWD?!" 47 | phantom.exit 1 48 | 49 | confirmPermissions = (uri) -> 50 | console.log "navigating to: #{uri}" 51 | 52 | confirmPage = require('webpage').create() 53 | confirmPage.onConsoleMessage = (msg) -> console.log(msg) 54 | confirmPage.onAlert = (msg) -> console.log(msg) 55 | confirmPage.onUrlChanged = (url) -> 56 | console.log "*NEW* confirm url #{url}" 57 | 58 | confirmPage.open uri, (status) -> 59 | console.log status 60 | if status isnt "success" 61 | console.log "page failed to load, see fail.png" 62 | confirmPage.render "fail.png" 63 | phantom.exit 1 64 | 65 | pos = null 66 | 67 | confirmPage.includeJs jqueryURI, -> 68 | pos = confirmPage.evaluate -> 69 | $("button").each -> 70 | console.log "button", $(this).attr("type"), this.id, this.name, $(this).val() 71 | 72 | button = $('#submit_approve_access').first() 73 | {left, top} = button.offset() 74 | [width, height] = [button.width(), button.height()] 75 | 76 | # window.setTimeout -> 77 | # #button[0].click() 78 | # #document.location.href = "http://localhost:3000/callback?code=4/evxJ1gaSk09OOmvtw1y3V5avi-gB.Ahort6xHLlYTOl05ti8ZT3a3ER_2eAI" 79 | # , 1000 80 | 81 | return { 82 | x: Math.round left + width/2 83 | y: Math.round top + height/2 84 | } 85 | 86 | setTimeout -> 87 | console.log "approving by clicking at", pos.x, pos.y 88 | # confirmPage.sendEvent 'mousedown', pos.x, pos.y, "left" 89 | # confirmPage.sendEvent 'mouseup', pos.x, pos.y, "left" 90 | confirmPage.sendEvent 'click', pos.x, pos.y 91 | , 2000 92 | 93 | 94 | # setTimeout -> 95 | # confirmPage.render "timeoui.png" 96 | # console.log("TIMEOUT while approving! See timeout.png.") 97 | # phantom.exit 1 98 | # , 8000 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google-oauth2", 3 | "description": "Google API OAuth2 helpers", 4 | "version": "0.1.0", 5 | "author": "Ben Lyaunzon ", 6 | "contributors": [ 7 | { 8 | "name": "Jan Bölsche", 9 | "email": "jan@lagomorph.de" 10 | } 11 | ], 12 | "keywords": [ 13 | "google", 14 | "outh", 15 | "oauth2", 16 | "google-api", 17 | "api" 18 | ], 19 | "homepage": "https://github.com/regular/node-google-oauth2", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/regular/node-google-oauth2.git" 23 | }, 24 | "engines": { 25 | "node": "0.5.x" 26 | }, 27 | "main": "index.js", 28 | "dependencies": { 29 | "request": "2.12.x", 30 | "async": "0.1.x" 31 | }, 32 | "devDependencies": { 33 | "mocha": "1.7.x", 34 | "chai": "1.4.x", 35 | "coffee-script": "1.4.x" 36 | }, 37 | "scripts": { 38 | "test": "./node_modules/mocha/bin/mocha --timeout 20000 --compilers coffee:coffee-script --reporter spec test/*" 39 | }, 40 | "optionalDependencies": { 41 | "phantomjs": "1.8.x" 42 | }, 43 | "licence": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /test/test.coffee: -------------------------------------------------------------------------------- 1 | should = require("chai").should() 2 | request = require("request") 3 | config = require("../config/config.js") 4 | gAuth = require("../index.js")(config) 5 | {inspect} = require 'util' 6 | 7 | describe "Google Oauth", -> 8 | auth_code = undefined 9 | refresh_token = undefined 10 | access_token = undefined 11 | 12 | # describe "#authorizeApplication()", -> 13 | # it "Responds with authorization code", (done) -> 14 | # gAuth.authorizeApplication config.username, config.password, config.scope,(err, code) -> 15 | # should.not.exist err 16 | # should.exist code 17 | # code.should.be.a "string" 18 | # auth_code = code 19 | # done() 20 | 21 | describe "#getAuthCode()", -> 22 | it "Responds with authorization code", (done) -> 23 | gAuth.getAuthCode config.scope, (err, code) -> 24 | should.not.exist err 25 | should.exist code 26 | code.should.be.a "string" 27 | auth_code = code 28 | done() 29 | 30 | describe "#getTokensForAuthCode()", -> 31 | it "Respond with an access token and a refresh token", (done) -> 32 | gAuth.getTokensForAuthCode auth_code, (err, body) -> 33 | should.not.exist err 34 | should.exist body 35 | body.should.be.an "object" 36 | console.log inspect body 37 | 38 | should.exist body.access_token 39 | should.exist body.refresh_token 40 | 41 | refresh_token = body.refresh_token 42 | 43 | done() 44 | 45 | describe "#getAccessTokenForRefreshToken()", -> 46 | it "Respond with new access token and expiration time", (done) -> 47 | gAuth.getAccessTokenForRefreshToken refresh_token, (err, body) -> 48 | should.not.exist err 49 | 50 | should.exist body 51 | body.should.be.an "object" 52 | 53 | should.exist body.access_token 54 | body.access_token.should.be.a "string" 55 | 56 | should.exist body.token_type 57 | body.token_type.should.equal 'Bearer' 58 | 59 | should.exist body.expires_in 60 | body.expires_in.should.be.a "number" 61 | body.expires_in.should.be.above 0 62 | 63 | done() 64 | --------------------------------------------------------------------------------