freedom-social-xmpp
Advanced tools
Comparing version 0.3.15 to 0.4.6
@@ -12,3 +12,8 @@ /*globals freedom:true,setTimeout,VCardStore,XMPPSocialProvider */ | ||
XMPPSocialProvider.prototype.oAuthScope = "email%20profile%20https://www.googleapis.com/auth/googletalk&"; | ||
XMPPSocialProvider.prototype.clientSecret = "h_hfPI4jvs9fgOgPweSBKnMu"; | ||
// These credentials are for a native app.. but we can't change the redirect URL. | ||
// XMPPSocialProvider.prototype.oAuthClientId = "746567772449-mv4h0e34orsf6t6kkbbht22t9otijip0.apps.googleusercontent.com"; | ||
// XMPPSocialProvider.prototype.clientSecret = "M-EGTuFRaWLS5q_hygpJZMBu"; | ||
/** | ||
@@ -23,2 +28,5 @@ * Begin the login view, potentially prompting for credentials. | ||
* network - A string used to differentiate this provider in events. | ||
* interactive - Login will attempt to re-use the last remembered | ||
* login credentials if this is set to false. | ||
* rememberLogin: Login credentials will be cached if true. | ||
*/ | ||
@@ -31,40 +39,34 @@ XMPPSocialProvider.prototype.login = function(loginOpts, continuation) { | ||
if (!this.credentials) { | ||
this.oauth = freedom["core.oauth"](); | ||
this.oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
var oauthUrl = "https://accounts.google.com/o/oauth2/auth?" + | ||
"client_id=" + this.oAuthClientId + | ||
"&scope=" + this.oAuthScope + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
"&state=" + encodeURIComponent(stateObj.state) + | ||
"&response_type=token"; | ||
var url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(oauthUrl); | ||
return this.oauth.launchAuthFlow(url, stateObj); | ||
}.bind(this)).then(function(continuation, responseUrl) { | ||
var token = responseUrl.match(/access_token=([^&]+)/)[1]; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function(continuation, token, xhr) { | ||
xhr.getResponseText().then(function(continuation, token, responseText) { | ||
var response = JSON.parse(responseText); | ||
var credentials = { | ||
userId: response.email, | ||
jid: response.email, | ||
oauth2_token: token, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this, continuation, token)); | ||
}.bind(this, continuation, token, xhr)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + token); | ||
xhr.send(); | ||
}.bind(this, continuation)).catch(function (continuation, err) { | ||
this.logger.error(err); | ||
if (loginOpts.interactive && !loginOpts.rememberLogin) { | ||
// Old login logic that gets accessToken and skips refresh tokens | ||
this.getAccessTokenWithOAuth_(continuation); | ||
return; | ||
} | ||
if (!this.storage) { | ||
this.storage = freedom['core.storage'](); | ||
} | ||
var getCredentialsPromise = loginOpts.interactive ? | ||
this.getCredentialsInteractive_() : this.getCredentialsFromStorage_(); | ||
getCredentialsPromise.then(function(data) { | ||
var refreshToken = data.refreshToken; | ||
var accessToken = data.accessToken; | ||
var email = data.email; | ||
if (this.loginOpts.rememberLogin) { | ||
this.saveLastRefreshTokenAndEmail_(refreshToken, email); | ||
this.saveRefreshTokenForEmail_(refreshToken, email); | ||
} | ||
var credentials = { | ||
userId: email, jid: email, oauth2_token: accessToken, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this)).catch(function(e) { | ||
this.logger.error('Error getting credentials: ', e); | ||
continuation(undefined, { | ||
errcode: 'LOGIN_OAUTHERROR', | ||
message: err.message | ||
//message: this.ERRCODE.LOGIN_OAUTHERROR | ||
errcode: 'LOGIN_OAUTHERROR', message: 'Error getting refreshToken: ' + e | ||
}); | ||
}.bind(this, continuation)); | ||
}.bind(this)); | ||
return; | ||
@@ -78,1 +80,256 @@ } | ||
}; | ||
// Returns Promise<{refreshToken, accessToken, email}> | ||
XMPPSocialProvider.prototype.getCredentialsFromStorage_ = function() { | ||
return new Promise(function(fulfill, reject) { | ||
this.loadLastRefreshTokenAndEmail_().then(function(data) { | ||
if (!data) { | ||
reject(new Error('Could not load last refresh token and email')); | ||
return; | ||
} | ||
var email = data.email; | ||
var refreshToken = data.refreshToken; | ||
this.getAccessTokenFromRefreshToken_(refreshToken).then( | ||
function(accessToken) { | ||
fulfill({ | ||
refreshToken: refreshToken, | ||
accessToken: accessToken, | ||
email: email}); | ||
}.bind(this)); // end of getAccessTokenFromRefreshToken_ | ||
}.bind(this)); // end of loadLastRefreshTokenAndEmail_ | ||
}.bind(this)); // end of return new Promise | ||
}; | ||
/** | ||
Returns Promise<{refreshToken, accessToken, email}> | ||
Open Google account chooser to let the user pick an account, then request a | ||
refresh token. Possible outcomes: | ||
1. Google gives us a refresh token after closing the oauth window, all good | ||
2. Google does not give us a refresh token after closing the oauth window, | ||
in this case we should use the access token to get the user's email, | ||
and see if we already have a refresh token stored for that email address. | ||
2a. If we have a refresh token for that email address, return it. | ||
2b. If we don't have a refresh token for that email address, we will have to | ||
launch another oauth window which with "approval_prompt=force", so that | ||
the user grants us "offline access" again and we can get a refresh token | ||
*/ | ||
XMPPSocialProvider.prototype.getCredentialsInteractive_ = function() { | ||
// First try to get a refresh token via the account chooser | ||
return new Promise(function(fulfill, reject) { | ||
// Check if there is any refresh token stored. If not, always force the | ||
// approval prompt. This is a small optimization to ensure that we always | ||
// get a refresh token on the 1st login attempt without needing to display | ||
// 2 oauth views. | ||
this.loadLastRefreshTokenAndEmail_().then(function(data) { | ||
var isFirstLoginAttempt = !(data && data.refreshToken); | ||
this.tryToGetRefreshToken_(isFirstLoginAttempt, null).then(function(responseObj) { | ||
// tryToGetRefreshToken_ should always give us an access_token, even | ||
// if no refresh_token is given. | ||
var accessToken = responseObj.access_token; | ||
if (!accessToken) { | ||
reject(new Error('Could not find access_token')); | ||
} | ||
// Get the user's email, needed loading/saving refresh tokens to/from | ||
// storage. | ||
this.getEmail_(accessToken).then(function(email) { | ||
if (responseObj.refresh_token) { | ||
// refresh_token was given on first attempt, all done. | ||
fulfill({ | ||
refreshToken: responseObj.refresh_token, | ||
accessToken: accessToken, | ||
email: email}); | ||
return; | ||
} | ||
// If no refresh_token is returned, it may mean that the user has already | ||
// granted this app a refresh token. We should first check to see if we | ||
// already have a refresh token stored for this user, and if not we should | ||
// prompt them again with approval_prompt=force to ensure we get a refresh | ||
// token. | ||
// Note loadRefreshTokenForEmail_ will fulfill with null if there is | ||
// no refresh token saved. | ||
this.loadRefreshTokenForEmail_(email).then(function(refreshToken) { | ||
if (refreshToken) { | ||
// A refresh token had already been saved for this email, done. | ||
fulfill({ | ||
refreshToken: refreshToken, | ||
accessToken: accessToken, | ||
email: email}); | ||
return; | ||
} | ||
// No refresh token was returned to us, or has been stored for this | ||
// email address (this would happen if the user already granted us | ||
// a token but re-installed the chrome app). Try again forcing | ||
// the approval prompt for this email address. | ||
this.tryToGetRefreshToken_(true, email).then(function(responseObj) { | ||
if (responseObj.refresh_token) { | ||
fulfill({ | ||
refreshToken: responseObj.refresh_token, | ||
accessToken: accessToken, | ||
email: email}); | ||
} else { | ||
reject(new Error('responseObj does not contain refresh_token')); | ||
} | ||
}.bind(this)).catch(function(error) { | ||
reject(new Error('Failed to get refresh_token with forcePrompt')); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.saveRefreshTokenForEmail_ = | ||
function(refreshToken, email) { | ||
this.storage.set('Google-Refresh-Token:' + email, refreshToken); | ||
}; | ||
XMPPSocialProvider.prototype.loadRefreshTokenForEmail_ = function(email) { | ||
return this.storage.get('Google-Refresh-Token:' + email); | ||
}; | ||
XMPPSocialProvider.prototype.saveLastRefreshTokenAndEmail_ = | ||
function(refreshToken, email) { | ||
this.storage.set('Google-Refresh-Token-Last', | ||
JSON.stringify({refreshToken: refreshToken, email: email})); | ||
}; | ||
XMPPSocialProvider.prototype.loadLastRefreshTokenAndEmail_ = function() { | ||
return new Promise(function(fulfill, reject) { | ||
this.storage.get('Google-Refresh-Token-Last').then(function(data) { | ||
fulfill(JSON.parse(data)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getEmail_ = function(accessToken) { | ||
return new Promise(function(fulfill, reject) { | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText).email); | ||
}.bind(this)); | ||
}.bind(this)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); | ||
xhr.send(); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getCode_ = function( | ||
oauth, stateObj, forcePrompt, authUserEmail) { | ||
var getCodeUrl = 'https://accounts.google.com/o/oauth2/auth?' + | ||
'response_type=code' + | ||
'&access_type=offline' + | ||
'&client_id=' + this.oAuthClientId + | ||
'&scope=' + this.oAuthScope + | ||
(forcePrompt ? '&approval_prompt=force' : '') + | ||
'&redirect_uri=' + encodeURIComponent(stateObj.redirect) + | ||
'&state=' + encodeURIComponent(stateObj.state); | ||
var url; | ||
if (authUserEmail) { | ||
// Skip account chooser and set the authuser param | ||
url = getCodeUrl + '&authuser=' + authUserEmail; | ||
} else { | ||
// Got to account chooser. | ||
url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(getCodeUrl); | ||
} | ||
return oauth.launchAuthFlow(url, stateObj).then(function(responseUrl) { | ||
return responseUrl.match(/code=([^&]+)/)[1]; | ||
}.bind(this)); | ||
}; | ||
// Returns a Promise which fulfills with the object Google gives us upon | ||
// requesting a refresh token. This object will always have an access_token, | ||
// but may not have a refresh_token if forcePrompt==false. | ||
XMPPSocialProvider.prototype.tryToGetRefreshToken_ = function( | ||
forcePrompt, authUserEmail) { | ||
return new Promise(function(fulfill, reject) { | ||
var oauth = freedom["core.oauth"](); | ||
return oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
this.getCode_( | ||
oauth, stateObj, forcePrompt, authUserEmail).then(function(code) { | ||
var data = 'code=' + code + | ||
'&client_id=' + this.oAuthClientId + | ||
'&client_secret=' + this.clientSecret + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
'&grant_type=authorization_code'; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('POST', 'https://www.googleapis.com/oauth2/v3/token', true); | ||
xhr.setRequestHeader( | ||
'content-type', 'application/x-www-form-urlencoded'); | ||
xhr.on('onload', function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText)); | ||
}); | ||
}); | ||
xhr.send({string: data}); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getAccessTokenWithOAuth_ = function(continuation) { | ||
this.oauth = freedom["core.oauth"](); | ||
this.oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
var oauthUrl = "https://accounts.google.com/o/oauth2/auth?" + | ||
"client_id=" + this.oAuthClientId + | ||
"&scope=" + this.oAuthScope + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
"&state=" + encodeURIComponent(stateObj.state) + | ||
"&response_type=token"; | ||
var url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(oauthUrl); | ||
return this.oauth.launchAuthFlow(url, stateObj); | ||
}.bind(this)).then(function(continuation, responseUrl) { | ||
var token = responseUrl.match(/access_token=([^&]+)/)[1]; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function(continuation, token, xhr) { | ||
xhr.getResponseText().then(function(continuation, token, responseText) { | ||
var response = JSON.parse(responseText); | ||
var credentials = { | ||
userId: response.email, | ||
jid: response.email, | ||
oauth2_token: token, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this, continuation, token)); | ||
}.bind(this, continuation, token, xhr)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + token); | ||
xhr.send(); | ||
}.bind(this, continuation)).catch(function (continuation, err) { | ||
this.logger.error('Error in getAccessTokenWithOAuth_', err); | ||
continuation(undefined, { | ||
errcode: 'LOGIN_OAUTHERROR', | ||
message: err.message | ||
}); | ||
}.bind(this, continuation)); | ||
}; | ||
XMPPSocialProvider.prototype.getAccessTokenFromRefreshToken_ = | ||
function(refreshToken) { | ||
return new Promise(function(fulfill, resolve) { | ||
var data = 'refresh_token=' + refreshToken + | ||
'&client_id=' + this.oAuthClientId + | ||
'&client_secret=' + this.clientSecret + | ||
'&grant_type=refresh_token'; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('POST', 'https://www.googleapis.com/oauth2/v3/token', true); | ||
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); | ||
xhr.on('onload', function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText).access_token); | ||
}); | ||
}); | ||
xhr.send({string: data}); | ||
}.bind(this)); | ||
}; |
@@ -125,2 +125,10 @@ /*jslint white:true,sloppy:true */ | ||
XMPPSocialProvider.prototype.connect = function(continuation) { | ||
if (this.client) { | ||
// Store our new credentials since logging out the old client | ||
// will clear this.credentials. | ||
var newCredentials = this.credentials; | ||
this.logout(); | ||
this.credentials = newCredentials; | ||
} | ||
var key, jid, connectOpts = { | ||
@@ -170,4 +178,3 @@ xmlns: 'jabber:client', | ||
if (this.client) { | ||
this.client.end(); | ||
delete this.client; | ||
this.logout(); | ||
} | ||
@@ -212,3 +219,3 @@ }.bind(this)); | ||
XMPPSocialProvider.prototype.clearCachedCredentials = function(continuation) { | ||
delete this.credentials; | ||
this.credentials = null; | ||
continuation(); | ||
@@ -584,3 +591,2 @@ }; | ||
this.status = 'offline'; | ||
this.credentials = null; | ||
this.lastMessageTimestampMs_ = null; | ||
@@ -596,2 +602,10 @@ if (this.pollForDisconnectInterval_) { | ||
this.client.end(); | ||
// end() still relies on the client's event listeners | ||
// so they can only be removed after calling end(). | ||
this.client.removeAllListeners('online'); | ||
this.client.removeAllListeners('error'); | ||
this.client.removeAllListeners('offline'); | ||
this.client.removeAllListeners('close'); | ||
this.client.removeAllListeners('end'); | ||
this.client.removeAllListeners('stanza'); | ||
this.client = null; | ||
@@ -598,0 +612,0 @@ } |
@@ -61,12 +61,2 @@ /*jshint node:true*/ | ||
}, | ||
demo_chrome_facebook: { | ||
src: [ | ||
'dist/*', | ||
'src/demo_common/*', | ||
'node_modules/freedom-for-chrome/freedom-for-chrome.js', | ||
'src/demo_chrome_facebook/**/*', | ||
], | ||
dest: 'build/demo_chrome_facebook/', | ||
flatten: true, filter: 'isFile', expand: true | ||
}, | ||
demo_firefox_google: { | ||
@@ -88,18 +78,2 @@ src: [ '**/*' ], | ||
}, | ||
demo_firefox_facebook: { | ||
src: [ '**/*' ], | ||
dest: 'build/demo_firefox_facebook/', | ||
cwd: 'src/demo_firefox_facebook/', | ||
filter: 'isFile', expand: true, | ||
}, | ||
demo_firefox_facebook_data: { | ||
src: [ | ||
'dist/*', | ||
'src/demo_common/**/*', | ||
'node_modules/freedom-for-firefox/freedom-for-firefox.jsm', | ||
'src/demo_chrome_facebook/demo.json', | ||
], | ||
dest: 'build/demo_firefox_facebook/data/', | ||
flatten: true, filter: 'isFile', expand: true | ||
}, | ||
jasmine: { | ||
@@ -196,3 +170,3 @@ src: [freedomPrefix + '/freedom.js'], | ||
'jshint', | ||
'browserify', | ||
//'browserify', | ||
'copy:dist' | ||
@@ -206,7 +180,4 @@ ]); | ||
'copy:demo_chrome_google', | ||
'copy:demo_chrome_facebook', | ||
'copy:demo_firefox_google', | ||
'copy:demo_firefox_google_data', | ||
'copy:demo_firefox_facebook', | ||
'copy:demo_firefox_facebook_data', | ||
'copy:demo_firefox_google_data' | ||
]); | ||
@@ -213,0 +184,0 @@ |
{ | ||
"name": "freedom-social-xmpp", | ||
"description": "XMPP Social provider for freedomjs", | ||
"version": "0.3.15", | ||
"version": "0.4.6", | ||
"homepage": "http://freedomjs.org", | ||
@@ -6,0 +6,0 @@ "bugs": { |
@@ -12,2 +12,5 @@ describe("Tests for message batching in Social provider", function() { | ||
}, | ||
removeAllListeners: function(eventName) { | ||
delete xmppSocialProvider.client.events[eventName]; | ||
}, | ||
end: function() {} | ||
@@ -57,6 +60,4 @@ }; | ||
xmppSocialProvider = new XMPPSocialProvider(dispatchEvent); | ||
xmppSocialProvider.client = xmppClient; | ||
xmppSocialProvider.id = 'myId'; | ||
xmppSocialProvider.loginOpts = {}; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
@@ -71,2 +72,3 @@ jasmine.clock().install(); | ||
it("add first message to batch and save time of message", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
dateSpy = spyOn(Date, "now").and.returnValue(500); | ||
@@ -79,2 +81,3 @@ xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
it("set callback after first message is added to batch", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
expect(xmppSocialProvider.sendMessagesTimeout).toBeNull(); | ||
@@ -86,2 +89,4 @@ xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
it("send message after 100ms", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
@@ -95,2 +100,4 @@ expect(xmppSocialProvider.client.send).not.toHaveBeenCalled(); | ||
it("calls callback after send", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
var spy = jasmine.createSpy('callback'); | ||
@@ -106,2 +113,4 @@ xmppSocialProvider.sendMessage('Bob', 'Hi', spy); | ||
it("timeout resets to 100ms after each message", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
@@ -126,2 +135,4 @@ expect(xmppSocialProvider.client.send).not.toHaveBeenCalled(); | ||
it("do not reset timeout if oldest message is from >=2s ago", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
dateSpy = spyOn(Date, "now").and.returnValue(500); | ||
@@ -153,2 +164,4 @@ xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
it("sends message to correct destinations", function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider.client, 'send'); | ||
xmppSocialProvider.sendMessage('Bob', 'Hi', function() {}); | ||
@@ -190,3 +203,3 @@ xmppSocialProvider.sendMessage('Alice', 'Hi', function() {}); | ||
jasmine.clock().tick(xmppSocialProvider.MAX_MS_PING_REPSONSE_ + 10); | ||
expect(xmppSocialProvider.logout).toHaveBeenCalled(); | ||
expect(xmppSocialProvider.logout.calls.count()).toEqual(1); | ||
}); | ||
@@ -236,28 +249,31 @@ | ||
it('detects sleep and pings immediately', function() { | ||
var nowMs = 0; | ||
dateSpy = spyOn(Date, "now").and.callFake(function() { return nowMs; }); | ||
spyOn(window.XMPP, 'Client').and.returnValue(xmppClient); | ||
var setIntervalCallbacks = []; | ||
spyOn(window, 'setInterval').and.callFake(function(callback, intervalMs) { | ||
setIntervalCallbacks.push(callback); | ||
}); | ||
spyOn(xmppSocialProvider, 'ping_'); | ||
// TODO: re-enable this test when we figure out | ||
// https://github.com/freedomjs/freedom-social-xmpp/issues/118 | ||
// it('detects sleep and pings immediately', function() { | ||
// var nowMs = 0; | ||
// dateSpy = spyOn(Date, "now").and.callFake(function() { return nowMs; }); | ||
// spyOn(window.XMPP, 'Client').and.returnValue(xmppClient); | ||
// var setIntervalCallbacks = []; | ||
// spyOn(window, 'setInterval').and.callFake(function(callback, intervalMs) { | ||
// setIntervalCallbacks.push(callback); | ||
// }); | ||
// spyOn(xmppSocialProvider, 'ping_'); | ||
// Connect and emit online event to start polling loop. | ||
xmppSocialProvider.connect(function() {}); | ||
xmppSocialProvider.client.events['online'](); | ||
// // Connect and emit online event to start polling loop. | ||
// xmppSocialProvider.connect(function() {}); | ||
// xmppSocialProvider.client.events['online'](); | ||
// Advance the clock by 2010 ms and invoke callbacks. | ||
nowMs = 2010; | ||
jasmine.clock().tick(2010); | ||
setIntervalCallbacks.map(function(callback) { callback(); }); | ||
// // Advance the clock by 2010 ms and invoke callbacks. | ||
// nowMs = 2010; | ||
// jasmine.clock().tick(2010); | ||
// setIntervalCallbacks.map(function(callback) { callback(); }); | ||
// Expect sleep to have been detected and ping to be invoked. | ||
expect(xmppSocialProvider.ping_).toHaveBeenCalled(); | ||
// logout must be called to clearInterval on the polling loop | ||
xmppSocialProvider.logout(); | ||
}); | ||
// // Expect sleep to have been detected and ping to be invoked. | ||
// expect(xmppSocialProvider.ping_).toHaveBeenCalled(); | ||
// // logout must be called to clearInterval on the polling loop | ||
// xmppSocialProvider.logout(); | ||
// }); | ||
it('parses JSON encoded arrays', function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider, 'dispatchEvent'); | ||
@@ -274,2 +290,3 @@ var fromClient = xmppSocialProvider.vCardStore.getClient('fromId'); | ||
it('does not parse JSON that is not an array', function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider, 'dispatchEvent'); | ||
@@ -285,2 +302,3 @@ var jsonString = '{key: "value"}'; | ||
it('does not parse non-JSON messages', function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider, 'dispatchEvent'); | ||
@@ -311,3 +329,3 @@ var fromClient = xmppSocialProvider.vCardStore.getClient('fromId'); | ||
xmppSocialProvider.client.events['end'](); | ||
expect(xmppSocialProvider.logout).toHaveBeenCalled(); | ||
expect(xmppSocialProvider.logout.calls.count()).toEqual(1); | ||
}); | ||
@@ -329,2 +347,3 @@ | ||
function() { | ||
xmppSocialProvider.client = xmppClient; | ||
spyOn(xmppSocialProvider, 'dispatchEvent'); | ||
@@ -331,0 +350,0 @@ |
@@ -12,3 +12,8 @@ /*globals freedom:true,setTimeout,VCardStore,XMPPSocialProvider */ | ||
XMPPSocialProvider.prototype.oAuthScope = "email%20profile%20https://www.googleapis.com/auth/googletalk&"; | ||
XMPPSocialProvider.prototype.clientSecret = "h_hfPI4jvs9fgOgPweSBKnMu"; | ||
// These credentials are for a native app.. but we can't change the redirect URL. | ||
// XMPPSocialProvider.prototype.oAuthClientId = "746567772449-mv4h0e34orsf6t6kkbbht22t9otijip0.apps.googleusercontent.com"; | ||
// XMPPSocialProvider.prototype.clientSecret = "M-EGTuFRaWLS5q_hygpJZMBu"; | ||
/** | ||
@@ -23,2 +28,5 @@ * Begin the login view, potentially prompting for credentials. | ||
* network - A string used to differentiate this provider in events. | ||
* interactive - Login will attempt to re-use the last remembered | ||
* login credentials if this is set to false. | ||
* rememberLogin: Login credentials will be cached if true. | ||
*/ | ||
@@ -31,40 +39,34 @@ XMPPSocialProvider.prototype.login = function(loginOpts, continuation) { | ||
if (!this.credentials) { | ||
this.oauth = freedom["core.oauth"](); | ||
this.oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
var oauthUrl = "https://accounts.google.com/o/oauth2/auth?" + | ||
"client_id=" + this.oAuthClientId + | ||
"&scope=" + this.oAuthScope + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
"&state=" + encodeURIComponent(stateObj.state) + | ||
"&response_type=token"; | ||
var url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(oauthUrl); | ||
return this.oauth.launchAuthFlow(url, stateObj); | ||
}.bind(this)).then(function(continuation, responseUrl) { | ||
var token = responseUrl.match(/access_token=([^&]+)/)[1]; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function(continuation, token, xhr) { | ||
xhr.getResponseText().then(function(continuation, token, responseText) { | ||
var response = JSON.parse(responseText); | ||
var credentials = { | ||
userId: response.email, | ||
jid: response.email, | ||
oauth2_token: token, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this, continuation, token)); | ||
}.bind(this, continuation, token, xhr)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + token); | ||
xhr.send(); | ||
}.bind(this, continuation)).catch(function (continuation, err) { | ||
this.logger.error(err); | ||
if (loginOpts.interactive && !loginOpts.rememberLogin) { | ||
// Old login logic that gets accessToken and skips refresh tokens | ||
this.getAccessTokenWithOAuth_(continuation); | ||
return; | ||
} | ||
if (!this.storage) { | ||
this.storage = freedom['core.storage'](); | ||
} | ||
var getCredentialsPromise = loginOpts.interactive ? | ||
this.getCredentialsInteractive_() : this.getCredentialsFromStorage_(); | ||
getCredentialsPromise.then(function(data) { | ||
var refreshToken = data.refreshToken; | ||
var accessToken = data.accessToken; | ||
var email = data.email; | ||
if (this.loginOpts.rememberLogin) { | ||
this.saveLastRefreshTokenAndEmail_(refreshToken, email); | ||
this.saveRefreshTokenForEmail_(refreshToken, email); | ||
} | ||
var credentials = { | ||
userId: email, jid: email, oauth2_token: accessToken, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this)).catch(function(e) { | ||
this.logger.error('Error getting credentials: ', e); | ||
continuation(undefined, { | ||
errcode: 'LOGIN_OAUTHERROR', | ||
message: err.message | ||
//message: this.ERRCODE.LOGIN_OAUTHERROR | ||
errcode: 'LOGIN_OAUTHERROR', message: 'Error getting refreshToken: ' + e | ||
}); | ||
}.bind(this, continuation)); | ||
}.bind(this)); | ||
return; | ||
@@ -78,1 +80,256 @@ } | ||
}; | ||
// Returns Promise<{refreshToken, accessToken, email}> | ||
XMPPSocialProvider.prototype.getCredentialsFromStorage_ = function() { | ||
return new Promise(function(fulfill, reject) { | ||
this.loadLastRefreshTokenAndEmail_().then(function(data) { | ||
if (!data) { | ||
reject(new Error('Could not load last refresh token and email')); | ||
return; | ||
} | ||
var email = data.email; | ||
var refreshToken = data.refreshToken; | ||
this.getAccessTokenFromRefreshToken_(refreshToken).then( | ||
function(accessToken) { | ||
fulfill({ | ||
refreshToken: refreshToken, | ||
accessToken: accessToken, | ||
email: email}); | ||
}.bind(this)); // end of getAccessTokenFromRefreshToken_ | ||
}.bind(this)); // end of loadLastRefreshTokenAndEmail_ | ||
}.bind(this)); // end of return new Promise | ||
}; | ||
/** | ||
Returns Promise<{refreshToken, accessToken, email}> | ||
Open Google account chooser to let the user pick an account, then request a | ||
refresh token. Possible outcomes: | ||
1. Google gives us a refresh token after closing the oauth window, all good | ||
2. Google does not give us a refresh token after closing the oauth window, | ||
in this case we should use the access token to get the user's email, | ||
and see if we already have a refresh token stored for that email address. | ||
2a. If we have a refresh token for that email address, return it. | ||
2b. If we don't have a refresh token for that email address, we will have to | ||
launch another oauth window which with "approval_prompt=force", so that | ||
the user grants us "offline access" again and we can get a refresh token | ||
*/ | ||
XMPPSocialProvider.prototype.getCredentialsInteractive_ = function() { | ||
// First try to get a refresh token via the account chooser | ||
return new Promise(function(fulfill, reject) { | ||
// Check if there is any refresh token stored. If not, always force the | ||
// approval prompt. This is a small optimization to ensure that we always | ||
// get a refresh token on the 1st login attempt without needing to display | ||
// 2 oauth views. | ||
this.loadLastRefreshTokenAndEmail_().then(function(data) { | ||
var isFirstLoginAttempt = !(data && data.refreshToken); | ||
this.tryToGetRefreshToken_(isFirstLoginAttempt, null).then(function(responseObj) { | ||
// tryToGetRefreshToken_ should always give us an access_token, even | ||
// if no refresh_token is given. | ||
var accessToken = responseObj.access_token; | ||
if (!accessToken) { | ||
reject(new Error('Could not find access_token')); | ||
} | ||
// Get the user's email, needed loading/saving refresh tokens to/from | ||
// storage. | ||
this.getEmail_(accessToken).then(function(email) { | ||
if (responseObj.refresh_token) { | ||
// refresh_token was given on first attempt, all done. | ||
fulfill({ | ||
refreshToken: responseObj.refresh_token, | ||
accessToken: accessToken, | ||
email: email}); | ||
return; | ||
} | ||
// If no refresh_token is returned, it may mean that the user has already | ||
// granted this app a refresh token. We should first check to see if we | ||
// already have a refresh token stored for this user, and if not we should | ||
// prompt them again with approval_prompt=force to ensure we get a refresh | ||
// token. | ||
// Note loadRefreshTokenForEmail_ will fulfill with null if there is | ||
// no refresh token saved. | ||
this.loadRefreshTokenForEmail_(email).then(function(refreshToken) { | ||
if (refreshToken) { | ||
// A refresh token had already been saved for this email, done. | ||
fulfill({ | ||
refreshToken: refreshToken, | ||
accessToken: accessToken, | ||
email: email}); | ||
return; | ||
} | ||
// No refresh token was returned to us, or has been stored for this | ||
// email address (this would happen if the user already granted us | ||
// a token but re-installed the chrome app). Try again forcing | ||
// the approval prompt for this email address. | ||
this.tryToGetRefreshToken_(true, email).then(function(responseObj) { | ||
if (responseObj.refresh_token) { | ||
fulfill({ | ||
refreshToken: responseObj.refresh_token, | ||
accessToken: accessToken, | ||
email: email}); | ||
} else { | ||
reject(new Error('responseObj does not contain refresh_token')); | ||
} | ||
}.bind(this)).catch(function(error) { | ||
reject(new Error('Failed to get refresh_token with forcePrompt')); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.saveRefreshTokenForEmail_ = | ||
function(refreshToken, email) { | ||
this.storage.set('Google-Refresh-Token:' + email, refreshToken); | ||
}; | ||
XMPPSocialProvider.prototype.loadRefreshTokenForEmail_ = function(email) { | ||
return this.storage.get('Google-Refresh-Token:' + email); | ||
}; | ||
XMPPSocialProvider.prototype.saveLastRefreshTokenAndEmail_ = | ||
function(refreshToken, email) { | ||
this.storage.set('Google-Refresh-Token-Last', | ||
JSON.stringify({refreshToken: refreshToken, email: email})); | ||
}; | ||
XMPPSocialProvider.prototype.loadLastRefreshTokenAndEmail_ = function() { | ||
return new Promise(function(fulfill, reject) { | ||
this.storage.get('Google-Refresh-Token-Last').then(function(data) { | ||
fulfill(JSON.parse(data)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getEmail_ = function(accessToken) { | ||
return new Promise(function(fulfill, reject) { | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText).email); | ||
}.bind(this)); | ||
}.bind(this)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken); | ||
xhr.send(); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getCode_ = function( | ||
oauth, stateObj, forcePrompt, authUserEmail) { | ||
var getCodeUrl = 'https://accounts.google.com/o/oauth2/auth?' + | ||
'response_type=code' + | ||
'&access_type=offline' + | ||
'&client_id=' + this.oAuthClientId + | ||
'&scope=' + this.oAuthScope + | ||
(forcePrompt ? '&approval_prompt=force' : '') + | ||
'&redirect_uri=' + encodeURIComponent(stateObj.redirect) + | ||
'&state=' + encodeURIComponent(stateObj.state); | ||
var url; | ||
if (authUserEmail) { | ||
// Skip account chooser and set the authuser param | ||
url = getCodeUrl + '&authuser=' + authUserEmail; | ||
} else { | ||
// Got to account chooser. | ||
url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(getCodeUrl); | ||
} | ||
return oauth.launchAuthFlow(url, stateObj).then(function(responseUrl) { | ||
return responseUrl.match(/code=([^&]+)/)[1]; | ||
}.bind(this)); | ||
}; | ||
// Returns a Promise which fulfills with the object Google gives us upon | ||
// requesting a refresh token. This object will always have an access_token, | ||
// but may not have a refresh_token if forcePrompt==false. | ||
XMPPSocialProvider.prototype.tryToGetRefreshToken_ = function( | ||
forcePrompt, authUserEmail) { | ||
return new Promise(function(fulfill, reject) { | ||
var oauth = freedom["core.oauth"](); | ||
return oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
this.getCode_( | ||
oauth, stateObj, forcePrompt, authUserEmail).then(function(code) { | ||
var data = 'code=' + code + | ||
'&client_id=' + this.oAuthClientId + | ||
'&client_secret=' + this.clientSecret + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
'&grant_type=authorization_code'; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('POST', 'https://www.googleapis.com/oauth2/v3/token', true); | ||
xhr.setRequestHeader( | ||
'content-type', 'application/x-www-form-urlencoded'); | ||
xhr.on('onload', function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText)); | ||
}); | ||
}); | ||
xhr.send({string: data}); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}.bind(this)); | ||
}; | ||
XMPPSocialProvider.prototype.getAccessTokenWithOAuth_ = function(continuation) { | ||
this.oauth = freedom["core.oauth"](); | ||
this.oauth.initiateOAuth(this.oAuthRedirectUris).then(function(stateObj) { | ||
var oauthUrl = "https://accounts.google.com/o/oauth2/auth?" + | ||
"client_id=" + this.oAuthClientId + | ||
"&scope=" + this.oAuthScope + | ||
"&redirect_uri=" + encodeURIComponent(stateObj.redirect) + | ||
"&state=" + encodeURIComponent(stateObj.state) + | ||
"&response_type=token"; | ||
var url = 'https://accounts.google.com/accountchooser?continue=' + | ||
encodeURIComponent(oauthUrl); | ||
return this.oauth.launchAuthFlow(url, stateObj); | ||
}.bind(this)).then(function(continuation, responseUrl) { | ||
var token = responseUrl.match(/access_token=([^&]+)/)[1]; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('GET', 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json', true); | ||
xhr.on("onload", function(continuation, token, xhr) { | ||
xhr.getResponseText().then(function(continuation, token, responseText) { | ||
var response = JSON.parse(responseText); | ||
var credentials = { | ||
userId: response.email, | ||
jid: response.email, | ||
oauth2_token: token, | ||
oauth2_auth: 'http://www.google.com/talk/protocol/auth', | ||
host: 'talk.google.com' | ||
}; | ||
this.onCredentials(continuation, {cmd: 'auth', message: credentials}); | ||
}.bind(this, continuation, token)); | ||
}.bind(this, continuation, token, xhr)); | ||
xhr.setRequestHeader('Authorization', 'Bearer ' + token); | ||
xhr.send(); | ||
}.bind(this, continuation)).catch(function (continuation, err) { | ||
this.logger.error('Error in getAccessTokenWithOAuth_', err); | ||
continuation(undefined, { | ||
errcode: 'LOGIN_OAUTHERROR', | ||
message: err.message | ||
}); | ||
}.bind(this, continuation)); | ||
}; | ||
XMPPSocialProvider.prototype.getAccessTokenFromRefreshToken_ = | ||
function(refreshToken) { | ||
return new Promise(function(fulfill, resolve) { | ||
var data = 'refresh_token=' + refreshToken + | ||
'&client_id=' + this.oAuthClientId + | ||
'&client_secret=' + this.clientSecret + | ||
'&grant_type=refresh_token'; | ||
var xhr = freedom["core.xhr"](); | ||
xhr.open('POST', 'https://www.googleapis.com/oauth2/v3/token', true); | ||
xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); | ||
xhr.on('onload', function() { | ||
xhr.getResponseText().then(function(responseText) { | ||
fulfill(JSON.parse(responseText).access_token); | ||
}); | ||
}); | ||
xhr.send({string: data}); | ||
}.bind(this)); | ||
}; |
@@ -125,2 +125,10 @@ /*jslint white:true,sloppy:true */ | ||
XMPPSocialProvider.prototype.connect = function(continuation) { | ||
if (this.client) { | ||
// Store our new credentials since logging out the old client | ||
// will clear this.credentials. | ||
var newCredentials = this.credentials; | ||
this.logout(); | ||
this.credentials = newCredentials; | ||
} | ||
var key, jid, connectOpts = { | ||
@@ -170,4 +178,3 @@ xmlns: 'jabber:client', | ||
if (this.client) { | ||
this.client.end(); | ||
delete this.client; | ||
this.logout(); | ||
} | ||
@@ -212,3 +219,3 @@ }.bind(this)); | ||
XMPPSocialProvider.prototype.clearCachedCredentials = function(continuation) { | ||
delete this.credentials; | ||
this.credentials = null; | ||
continuation(); | ||
@@ -584,3 +591,2 @@ }; | ||
this.status = 'offline'; | ||
this.credentials = null; | ||
this.lastMessageTimestampMs_ = null; | ||
@@ -596,2 +602,10 @@ if (this.pollForDisconnectInterval_) { | ||
this.client.end(); | ||
// end() still relies on the client's event listeners | ||
// so they can only be removed after calling end(). | ||
this.client.removeAllListeners('online'); | ||
this.client.removeAllListeners('error'); | ||
this.client.removeAllListeners('offline'); | ||
this.client.removeAllListeners('close'); | ||
this.client.removeAllListeners('end'); | ||
this.client.removeAllListeners('stanza'); | ||
this.client = null; | ||
@@ -598,0 +612,0 @@ } |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
900891
27299
55
12