chrome.identity.launchWebAuthFlow, how to clear user cache? - javascript

I'm developing an extension that works with Gmail and I'd like to be able to allow users to switch between Gmail accounts and still make use of the Google REST APIs.
I'm using chrome.identity.launchWebAuthFlow to acquire OAuth2 access tokens to the Google APIs.
This workflow opens a modal sort of chrome webview. With no url bar at the top. Upon entering a username and password the first time, then allowing for the requested scopes, the webview closes. My app then receives the redirect URI with the access token included. Great.
The problem comes when switching users. One would think it would be as simple as checking that a new email is logged in, then doing the chrome.identity.launchWebAuthFlow again to grab a new token.
Unfortunately the first logged in user seems to remain cached in the system.
function webAuthFlow(userEmail, forceApprovalPrompt, xhrCallback) {
var baseUrl = 'https://accounts.google.com/o/oauth2/auth';
var forceApprovalPrompt = forceApprovalPrompt || 'auto';
var urlParams = {
'redirect_uri' : 'https://inobjcmbajbmllkgkigemcfnikdmlidn.chromiumapp.org/callback',
'response_type' : 'token',
'client_id' : 'not shown here',
'scope' : 'https://mail.google.com/ https://www.google.com/m8/feeds/',
'approval_prompt' : 'force',
'include_granted_scopes' : 'true'
};
var providerDetails = {
url : baseUrl + '?' + stringify(urlParams),
interactive : true
}
var xhrCallback = xhrCallback || false;
console.log(xhrCallback);
var callback = function(responseUrl) {
var params = {},
queryString = responseUrl.split('#')[1],
regex = /([^&=]+)=([^&]*)/g,
m,
validateUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo'
while (m = regex.exec(queryString)) {
params[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
}
validateToken(params.access_token, function() { storeToken(params.access_token, userEmail) }, xhrCallback);
};
chrome.identity.launchWebAuthFlow(providerDetails, callback);
};
I've tried to inspect the chrome webview that pops up by setting approval_prompt to 'force'. It appears there are some cookies associated with it. I'd like to know how to clear persistent data from the webview.
The only thing that seems to work is completely closing out chrome. Not an acceptable UX for my extension.
Thanks in advance if anyone has any pointers on this.

Use chrome.identity.removeCachedAuthToken(object details, function callback) method.
Removes an OAuth2 access token from the Identity API's token cache.
If an access token is discovered to be invalid, it should be passed to removeCachedAuthToken to remove it from the cache. The app may then retrieve a fresh token with getAuthToken.

Related

Implementing Dropbox API V2 in Cordova Application

I have a Cordova application with previous Dropbox implementation using rossmartin/phonegap-dropbox-sync-android. Now as the API V1 is going to be deprecated I want to upgrade to Dropbox API V2. I have searched for plugins for Cordova applications using Dropbox API V2 but didn't find any.So I am trying to implement it using dropbox/dropbox-sdk-js.
For Authentication, I am using authenticateWithCordova method which returns me the Access token (Full documentation here).This method returns Access token once the user completes authentication with Dropbox and uses the redirect URL to redirect the user to Cordova application.
This method works perfectly when the user clicks the button for the first time, but when the user clicks the button again calling this method shows a blank screen and return a new access token. How to avoid seeing the blank screen?
This is the method from Dropbox-sdk.js file, which I have called from my application,
DropboxBase.prototype.authenticateWithCordova = function (successCallback, errorCallback)
{
var redirect_url = 'https://www.dropbox.com/1/oauth2/redirect_receiver';
var url = this.getAuthenticationUrl(redirect_url);
var browser = window.open(url, '_blank');
var removed = false;
var onLoadError = function(event) {
// Try to avoid a browser crash on browser.close().
window.setTimeout(function() { browser.close() }, 10);
errorCallback();
}
var onLoadStop = function(event) {
var error_label = '&error=';
var error_index = event.url.indexOf(error_label);
if (error_index > -1) {
// Try to avoid a browser crash on browser.close().
window.setTimeout(function() { browser.close() }, 10);
errorCallback();
} else {
var access_token_label = '#access_token=';
var access_token_index = event.url.indexOf(access_token_label);
var token_type_index = event.url.indexOf('&token_type=');
if (access_token_index > -1) {
access_token_index += access_token_label.length;
// Try to avoid a browser crash on browser.close().
window.setTimeout(function() { browser.close() }, 10);
var access_token = event.url.substring(access_token_index, token_type_index);
successCallback(access_token);
}
}
};
Here is my code which I use to call the method,
function authenticateWithCordova()
{
var dbx = new Dropbox({ clientId: CLIENT_ID });
dbx.authenticateWithCordova(AuthSuccess,AuthFail);
}
function AuthSuccess(accessToken)
{
localStorage.accessToken = accessToken;
}
function AuthFail()
{
alert("Auth Fail");
}
I have found an analog issue right yesterday. This is the way I solved it.
First, I have set var dbx as global. In my index.js I put these lines immediately after app.initialize():
var CLIENT_ID = 'xxxxxxxxxxxxxxx';
var dbxt;
var dbx = new Dropbox({clientId: CLIENT_ID});
Then I check if dbxt is null: if it is, I create a new Dropbox object using accessToken, otherwise I go with the dropbox connection already established:
if (dbxt == null) {
dbx.authenticateWithCordova(function (accessToken) {
dbxt = new Dropbox({accessToken: accessToken});
dbxt.filesUpload({
path: '/mydump.sql',
contents: sql,
mode: 'overwrite',
mute: true
}).then(function (response) {
alert('Your backup has been successfully uploaded to your Dropbox!')
}).catch(function (error) {
alert('Error saving file to your Dropbox!')
console.error(error);
});
}, function (e){
console.log("failed Dropbox authentication");
}
}else{//dbxt already created
dbxt.filesUpload... //and the rest
}
This is just to avoid to create a new connection and get a new access token everytime and I confess I'm not sure this is a good practice: I only know that before to apply this code I got a lot of bad requests responses by Dropbox server:)
When I used the above code, after the first login, I started to see the blank page: that's is the inappbrowser page which Dropbox OAuth2 uses as redirect URI (set to https://www.dropbox.com/1/oauth2/redirect_receiver in your Dropbox app page).
So the problem was how to make this page invisible. I found a dirty trick applying a small tweak to inappbrowser.js script.
Near the bottom of the script, immediately before this line:
strWindowFeatures = strWindowFeatures || "";
I have put this small block:
if (strUrl.indexOf('dropbox') > -1){
strWindowFeatures += "location=no,hidden=yes";
}
I would have expected to can just use 'hidden=yes' but surprisingly if I remoce 'location=no' the blkank page appears again.
Notice 1: you don't have to modify the script inappbrowser.js located at plugins\cordova-plugin-inappbrowser\www\ but the one you find in platforms\android\platform_www\plugins\cordova-plugin-inappbrowser\www\
Notice 2: I have found this workaround right now so I'm not 100% sure it works perfectly.
Notice 3: making the inappbrowser page invisible, depending on the Internet connection, it could look like nothing is happening for a while, so you'll have to add some loader to inform your user that the app is working.
Hope this help.
UPDATE
I've just realized we can tweak directly the dropbox-sdk instead of inappbrowser.
If you are using Dropbox with browserify you have to open dropbox-base.js and look for authenticateWithCordova() method (it should be at line 107. Then change the line
var browser = window.open(url, '_blank');
to
var browser = window.open(url, '_blank', "location=no,hidden=yes");
If you are using Dropbox-sdk.min.js, you have to look for 'window.open' using the search function of your code editor. It will be easy because 'window.open' is used only once. So you'll have to change the following:
i=window.open(n,"_blank"),
to
i=window.open(n,"_blank","location=no,hidden=yes"),
And this seems to work fine (I prefer to be careful before I get excited).
UPDATE 2
Forgive previous update. My previous check:
if (strUrl.indexOf('dropbox') > -1){
strWindowFeatures += "location=no,hidden=yes";
}
is wrong because it makes invisible any inappbrowser window which tries to connect to dropbox so it prevent us from even logging into Dropbox. So we need to change it to
if (strUrl == 'https://www.dropbox.com/1/oauth2/redirect_receiver') {
strWindowFeatures += "location=no,hidden=yes";
}
This way we can do the login correctly and next connections won't show the inappbrowser window, as we want.
So summarizing:
Ignore my first update
Use UPDATE 2 to modify the url check in inappbrowser.js
Forgive me for the confusion...

Connecting Chrome Extension to StackExchange oAuth API

TL;DR -- I am trying to build a Chrome extension that displays the number of current notifications for a user from StackOverflow.
I am following the Chrome Extension tutorial on creating an extension that utilizes oAuth. Swapping out the Google oAuth bits for (what I think are) the StackExchange API bits, I end up with the following in my background.js file:
var oauth = ChromeExOAuth.initBackgroundPage({
//ES6 template strings!
'request_url' : `https://stackexchange.com/oauth?client_id=${STACK_APP.id}&scope=read_inbox,no_expiry`,
'authorize_url' : 'https://stackexchange.com/oauth/login_success',
'access_url' : 'https://stackexchange.com/oauth/access_token',
'consumer_key' : STACK_APP.key,
'consumer_secret' : STACK_APP.secret,
//'scope' : '', //not sure I need this...
'app_name' : 'StackOverflow Notifications (Chrome Extension)'
});
oauth.authorize(function () {
console.log('authorize...');
});
When I run the extension locally, it attempts to open a new browser tab to complete the oAuth handshake -- which is great, except that I end up at the following URL:
https://stackexchange.com/oauth/login_success?oauth_token=undefined
The browser tab gets stuck here.
I'm not really sure what the problem is - I don't know if I have the wrong URLs listed in my initBackgroundPage() call, or if my StackApp has the wrong oAuth domain (since it's a Chrome extension... this question didn't really answer things for me).
Any ideas would be most appreciated!
From what I can tell, the oAuth tutorial I mentioned in my OP is outdated -- you no longer need any of that, you can use chrome.identity instead.
To be clear, there are a few things I had to do:
STACK_APP = {
id : 1234,
scope : 'read_inbox,no_expiry',
request_uri : 'https://stackexchange.com/oauth/dialog',
redirect_uri : chrome.identity.getRedirectURL("oauth2")
};
//ES6 template string!
var requestUrl = `${STACK_APP.request_uri}?client_id=${STACK_APP.id}&scope=${STACK_APP.scope}&redirect_uri=${STACK_APP.redirect_uri}`;
//https://developer.chrome.com/extensions/identity
//https://developer.chrome.com/extensions/app_identity#update_manifest
chrome.identity.launchWebAuthFlow({
url : requestUrl,
interactive : true
}, function (url) {
console.log('redirected to: ' + url);
});
Note the redirect_uri -- this ends up being a subdomain of chromiumapp.org, so you need to list that in StackApps as the domain for your app (allowing oAuth to continue).

Location.hash empty only in Safari 7

I am attempting to authenticate with Constant Contact via OAuth2 in a popup window. I am using $.postMessage to send the data between windows, and for the most part, it works beautifully.
My problem is with Safari. A normal request has a URL that looks like this:
https://example.com/oauth-v2/#access_token=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx&token_type=Bearer&expires_in=xxxxxxxxx
But while using Safari to make the request, the entire hash is cut off the URL and location.hash, window.location.hash, window.parent.location.hash are all empty.
The authentication flow is fairly standard:
User clicks auth button
Popup window to auth with Constant Contact
Allow application
List item
Return to app site to capture token
Here's the javascript we're using to get the URL hash info
jQuery(document).ready(function ($) {
$.extend({
getQueryParameters: function (str) {
return (str || document.location.search || document.location.hash)
.replace(/(^\?)|(^\#)/, '')
.split("&")
.map(function (n) { return n = n.split("="), this[n[0]] = n[1], this }.bind({}))[0];
}
});
$.receiveMessage(function (event) {
$.postMessage($.getQueryParameters(), event.origin, event.source);
setTimeout(function () {
window.close()
}, 5000);
});
});
Is the missing hash a known bug in Safari? Should I be doing something else to get the info from Constant Contact? It works in every other browser so I would hate to re-write this part of the application.

Facebook OAuth "Unsupported" in Chrome on iOS

The Facebook OAuth popup is throwing an error in Chrome on iOS only. Both developers.facebook.com and google have turned up nothing about this. Ideas?
You can use the redirection method as follow for this case (by detecting the user agent being chrome ios):
https://www.facebook.com/dialog/oauth?client_id={app-id}&redirect_uri={redirect-uri}
See more info here https://developers.facebook.com/docs/facebook-login/login-flow-for-web-no-jssdk/
Remark: I personnaly use the server OAuth in that case but this should do the trick and is quite simple
This is how I did it (fixing iOS chrome specifically)
// fix iOS Chrome
if( navigator.userAgent.match('CriOS') )
window.open('https://www.facebook.com/dialog/oauth?client_id='+appID+'&redirect_uri='+ document.location.href +'&scope=email,public_profile', '', null);
else
FB.login(null, {scope: 'email,public_profile'});
Here is a complete workaround for your FB JS Auth on Chrome iOS issue http://seanshadmand.com/2015/03/06/facebook-js-login-on-chrome-ios-workaround/
JS functions to check auth, open FB auth page manually and refresh auth tokens on original page once complete:
function openFBLoginDialogManually(){
// Open your auth window containing FB auth page
// with forward URL to your Opened Window handler page (below)
var redirect_uri = "&redirect_uri=" + ABSOLUTE_URI + "fbjscomplete";
var scope = "&scope=public_profile,email,user_friends";
var url = "https://www.facebook.com/dialog/oauth?client_id=" + FB_ID + redirect_uri + scope;
// notice the lack of other param in window.open
// for some reason the opener is set to null
// and the opened window can NOT reference it
// if params are passed. #Chrome iOS Bug
window.open(url);
}
function fbCompleteLogin(){
FB.getLoginStatus(function(response) {
// Calling this with the extra setting "true" forces
// a non-cached request and updates the FB cache.
// Since the auth login elsewhere validated the user
// this update will now asyncronously mark the user as authed
}, true);
}
function requireLogin(callback){
FB.getLoginStatus(function(response) {
if (response.status != "connected"){
showLogin();
}else{
checkAuth(response.authResponse.accessToken, response.authResponse.userID, function(success){
// Check FB tokens against your API to make sure user is valid
});
}
});
}
And the Opener Handler that FB auth forwards to and calls a refresh to the main page. Note the window.open in Chrome iOS has bugs too so call it correctly as noted above:
<html>
<head>
<script type="text/javascript">
function handleAuth(){
// once the window is open
window.opener.fbCompleteLogin();
window.close();
}
</script>
<body onload="handleAuth();">
<p>. . . </p>
</body>
</head>
</html>
My 2 cents on this as noone of the answers were clear to me. Im firing the login js dialog on a button click, so now when it's chrome ios I check first if the user it's logged into facebook and if not I send them to the login window. The problem with this is that chome ios users needs to click connect button twice if they are not logged into facebook. If they are logged into facebook one click is enough.
$( 'body' ).on( 'click', '.js-fbl', function( e ) {
e.preventDefault();
if( navigator.userAgent.match('CriOS') ) {
// alert users they will need to click again, don't use alert or popup will be blocked
$('<p class="fbl_error">MESSAGE HERE</p>').insertAfter( $(this));
FB.getLoginStatus( handleResponse );
} else {
// regular users simple login
try {
FB.login( handleResponse , {
scope: fbl.scopes,
return_scopes: true,
auth_type: 'rerequest'
});
} catch (err) {
$this.removeClass('fbl-loading');
}
}
});
That bit of code make it works for chrome ios users. On handle response I simple take care of fb response and send it to my website backend for login/register users.
var handleResponse = function( response ) {
var $form_obj = window.fbl_button.parents('.flp_wrapper').find('form') || false,
$redirect_to = $form_obj.find('input[name="redirect_to"]').val() || window.fbl_button.data('redirect');
/**
* If we get a successful authorization response we handle it
*/
if (response.status == 'connected') {
var fb_response = response;
/**
* Make an Ajax request to the "facebook_login" function
* passing the params: username, fb_id and email.
*
* #note Not all users have user names, but all have email
* #note Must set global to false to prevent gloabl ajax methods
*/
$.ajax({...});
} else {
//if in first click user is not logged into their facebook we then show the login window
window.fbl_button.removeClass('fbl-loading');
if( navigator.userAgent.match('CriOS') )
window.open('https://www.facebook.com/dialog/oauth?client_id=' + fbl.appId + '&redirect_uri=' + document.location.href + '&scope=email,public_profile', '', null);
}
};
Hope it helps!
Not a real answer but based on this thread worth noting that is started working for our app, on Chrome when on the iPhone we did General>Reset>Reset Location & Privacy
I got a solution for ios facebook website login in google chrome . Actually the issue was with google chrome in ios when we click on facebook login button it give internally null to the window.open in ios chrome .
There are two solution either to check it is chrome in ios(chrios) and then generate custom login screen ( still not chance that it will we correct ).
Second what i have used. Is to use facebook login from backhand create a api hit will populate facebook screen and then when login is done it will redirect to your server from where you will redirect to your website page with facebook data.
one other benefit of it is that you create 2 website for same website owner you can not set two website url in facebook developer account .In this way you can create many website facebook login with same facebook appid .
This is a very common issue which all developers have faced while implementing the FB login feature. I have tried most of the Internet solutions but none of them worked. Either window.opener do not work in Chrome iOS or sometime FB object is not loaded while using /dialog/oauth.
Finally I solved this by myself after trying all the hacks!
function loginWithFacebook()
{
if( navigator.userAgent.match('CriOS') )
{
var redirect_uri = document.location.href;
if(redirect_uri.indexOf('?') !== -1)
{
redirect_uri += '&back_from_fb=1';
}
else
{
redirect_uri += '?back_from_fb=1';
}
var url = 'https://www.facebook.com/dialog/oauth?client_id=[app-id]&redirect_uri='+redirect_uri+'&scope=email,public_profile';
var win = window.open(url, '_self');
}
else
{
FB.login(function(response)
{
checkLoginState();
},
{
scope:'public_profile,email,user_friends,user_photos'
});
}
}
Notice that above I'm passing an extra param to the redirect url so that once the new window opens with above redirect uri I could read the values and can say yes this call is from Chrome iOS window. Also make sure this code runs on page load.
if (document.URL.indexOf('back_from_fb=1') != -1 && document.URL.indexOf('code=') != -1)
{
pollingInterval = setInterval(function()
{
if(typeof FB != "undefined")
{
FB.getLoginStatus(function(response) {}, true);
checkLoginState();
}
else
{
alert("FB is not responding, Please try again!", function()
{
return true;
});
}
}, 1000);
}

Is there a way to not send cookies when making an XMLHttpRequest on the same origin?

I'm working on an extension that parses the gmail rss feed for users. I allow the users to specify username/passwords if they don't want to stay signed-in. But this breaks for multiple sign-in if the user is signed-in and the username/password provided is for a different account. So I want to avoid sending any cookies but still be able to send the username/password in the send() call.
As of Chrome 42, the fetch API allows Chrome extensions (and web applications in general) to perform cookie-less requests. HTML5 Rocks offers an introductory tutorial on using the fetch API.
Advanced documentation on fetch is quite sparse at the moment, but the API interface from the specification is a great starting point. The fetch algorithm described below the interface shows that requests generated by fetch have no credentials by default!
fetch('http://example.com/').then(function(response) {
return response.text(); // <-- Promise<String>
}).then(function(responseText) {
alert('Response body without cookies:\n' + responseText);
}).catch(function(error) {
alert('Unexpected error: ' + error);
});
If you want truly anonymous requests, you could also disable the cache:
fetch('http://example.com/', {
// credentials: 'omit', // this is the default value
cache: 'no-store',
}).then(function(response) {
// TODO: Handle the response.
// https://fetch.spec.whatwg.org/#response-class
// https://fetch.spec.whatwg.org/#body
});
You can do that by using the chrome.cookies module. The idea is to get the current cookies, save them, remove them from the browser's cookie store, send your request, and finally restore them:
var cookies_temp = []; // where you put the cookies first
var my_cookie_store = []; // the cookies will be there during the request
var details = {/*your code*/}; // the first parameter for chrome.cookies.getAll()
var start_kidnapping = function(cookies) {
cookies_temp = cookies.slice();
kidnap_cookie();
};
var kidnap_cookie = function() {
// This recursive function will store the cookies from cookies_temp to
// my_cookie_store and then remove them from the browser's cookie store.
if (cookies_temp.length == 0) { // when no more cookies, end recursion
send_request();
};
else {
var cookie = cookies_temp.pop();
// We store url as a property since it is useful later.
// You may want to change the scheme.
cookie.url = "http://" + cookie.domain + cookie.path;
my_cookie_store.push(cookie); // save it
chrome.cookies.remove({url: cookie.url, name: cookie.name}, kidnap_cookie);
};
};
var send_request = function() {
// Send your request here. It can be asynchronous.
for (var i = 0, i < my_cookie_store.length; i++){
delete cookie.hostOnly; // these 2 properties are not part of the
delete cookie.session; // object required by chrome.cookies.set()
// note that at this point, cookie is no longer a Cookie object
chrome.cookies.set(my_cookie_store[i]); // restore cookie
};
my_cookie_store = []; // empty it for new adventures
};
chrome.cookies.getAll(details, start_kidnapping); // start
Alternatively, a simpler solution is to open an incognito window which will send the request, using the chrome.windows module, but this will prevent you from communicating with the rest of your extension. Note that you may have to change the incognito property of your manifest to split:
var incognito_window = {
"url": "incognito.html",
"focused": false, // do not bother user
"incognito": true
}
chrome.windows.create(incognito_window);

Categories