Location.hash empty only in Safari 7 - javascript

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.

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...

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

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.

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);
}

Firefox Extension : Stopping the page load when suspicious url found

I am working on a simple firefox extension that tracks the url requested and call a web service at the background which detects whether the URL is suspicious or not and based on the result returned by the service, extension decides to stop the page load and alert the user about the case of forgery or whatever, and if user still wishes to go to that page he can get redirected to the original page he has requested for
I have added a http-on-modify-request observer
var observerService = Components.classes["#mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);
observerService.addObserver(requestObserverListener.observe, "http-on-modify-request", false);
and the observer
var requestObserverListener = {observe: function (subject, topic, data) {
//alert("Inside observe");
if (topic == "http-on-modify-request") {
subject.QueryInterface(Components.interfaces.nsIHttpChannel);
var url = subject.URI.spec; //url being requested. you might want this for something else
//alert("inside modify request");
var urlbarvalue = document.getElementById("urlbar").value;
urlbarvalue = processUrl(urlbarvalue, url);
//alert("url bar: "+urlbarvalue);
//alert("url: "+url);
document.getElementById("urlbar").style.backgroundColor = "white";
if(urlbarvalue == url && url != "")
{
var browser = getWindowForRequest(subject);
if (browser != null) {
//alert(""+browser.contentDocument.body.innerHTML);
alert("inside browser: "+url);
getXmlHttpRequest(url);
}
}
}
},
}
so when the URL in the URLbar and the requested url matches REST service will be called through ajax getXmlHttpRequest(url); method
now when i am running this extension call is made to the service but before the service return any response the page gets loaded which is not appropriate because user might enter his credentials in the meanwhile and get compromised
I want to first display user a warning message on the browser tab and if he still wanted to visit to that page he can then be redirected to that page on a link click in warning message window
I haven't tried this code out so I'm not sure that suspend and resume will work well but here's what I would try. You're working with an nsIRequest object as your subject so you can call subject.suspend() on it. From there use callbacks to your XHR call to either cancel() or resume() the nsIRequest.
Here's the relevant (untested) snippet of code. My XHR assumes some kind of promise .the() return but hopefully you understand the intention:
if(urlbarvalue == url && url != "")
{
var browser = getWindowForRequest(subject);
if (browser != null) {
// suspend the pending request
subject.suspend();
getXmlHttpRequest(url).then(
function success() { subject.resume(); },
function failure() { subject.cancel(Components.results.NS_BINDING_ABORTED); });
}
}
Just some fair warning that you actually don't want to implement an add-on in this way.
It's going to be extremely slow to do a remote call for every HTTP request. The safe browsing module does a single call to download a database of sites considered 'unsafe', it then can quickly check the database against the HTTP request page such that it doesn't have to make individual calls every time.
Here's some more info on this kind of intercepting worth reading: https://developer.mozilla.org/en-US/docs/XUL/School_tutorial/Intercepting_Page_Loads#HTTP_Observers
Also I'd worry that your XHR request will actually loop because XHR calls creates an http-on-modify-request event so your code might actually check that your XHR request is valid before being able to check the current URL. You probably want a safety check for your URL checking domain.
And here's another stackoverflow similar question to yours that might be useful: How to block HTTP request on a particular tab?
Good luck!

How to logout my application when I closed the window?

In my chat application i am having the logout button and it works fine.
Now I need to logout the application when I closed the browser window also..How can I achieve this...
Thanks in advance...
There is no exact way to do this with the clientside. There is no event that is fired when the page is exited. It should be done with the Session End event on the server.
You can try to use onbeforeunload or unload, but race conditions will prevent that from happening. AND they do not fire for browsers crashing, lost internet connection, etc.
I dealt with this issue recently in my angularJS app - The main issue was that I don't want to log you out if you refresh, but I do want to if you close the tab.. Ajax requests with onbeforeunload/onunload aren't guaranteed to wait for response, so here is my solution:
I set a sessionStorage cookie on login that is just a bool - set to true when I get login response
sessionStorage.setItem('activeSession', 'true');
Obviously, on logout, we set this flag to false
Either when controller initializes or using window.onload (in my app.js file) - I check for this activeSession bool.. if it is false, I have this small if statement - where if conditions are met I call my logout method ONLOAD instead of onunload
var activeSession = sessionStorage.activeSession;
if (sessionStorage.loggedOutOnAuth) {
console.log('Logged out due to expiry already')
}
else if (!activeSession) {
sessionStorage.loggedOutOnAuth = true;
_logout()
}
Basically, the "loggedOutAuth" bool let's me know that I just expired you on page load due to the absence of an activeSession in sessionStorage so you don't get stuck in a loop
This was a great solution for me since I didn't want to implement a heartbeat/websocket
Add your logout code to the on onunload event.
window.onunload = function () {
//logout code here...
}
In JQuery you can use the .unload() function. Remember that you don't have much time so you may send the Ajax request but the result may not reach the client.
Another trick is to open a small new window and handle the logout there.
window.open("logout url","log out","height=10,width=10,location=no,menubar=no,status=no,titlebar=no,toolbar=no",true);
If you want to disable closing the window (or at least warn the user), you can use this code:
window.onbeforeunload = function(event) {
//if you return anything but null, it will warn the user.
//optionally you can return a string which most browsers show to the user as the warning message.
return true;
}
Another trick is to keep pinging the client every few seconds. If no reply comes back, assume the user has closed the window, browser has crashed or there is a network issue that ended the chat session anyway. On the client side, if you don't receive this ping package, you can assume that network connection or server has a problem and you can show the logout warning (and optionally let the user login again).
Some websites are using the following script to detect whether window is closed or not.
if(window.screenTop > 10000)
alert("Window is closed");
else
alert("Window stillOpen");
You need to add the correct action instead of alert()
also take a look HERE - I think this is somthing you need to detect the window closing
I got the Solution by,
window.onunload = function () {
//logout code here...
}
Thanks for all who supported me...
Another approach is some sort of "keepalive": the browser page "pings" the server with a small ajax request every minute or so. If the server doesn't get the regular pings, the session is closed and can no longer be used.
As an optimization, the pings can be skipped if we have made another request to the server in the interim.
Advantages:
still works with multiple windows open
no problem with F5 / refresh
can provides some usage statistics to the server
Disadvantages:
when the window is closed, there is a delay before the user is logged out
uses a little network bandwidth
additional load on the server
users might have concerns about the page constantly "phoning home"
more difficult to implement
I've never actually done this in a web app, and not sure if I would; just putting it out there as an alternative. It seems like a good option for a chat app, where the server does need to know if you are still there.
Rather than polling / pinging, another possibility is to keep a "long running request" open while the page is open. A chat app needs some such socket to receive new messages and notifications. If the page is closed, the socket is closed too, and the server can notice that it has been closed. It then waits a brief time for the client to establish a new socket, and if it doesn't we assume the page is closed and delete the session. This would require some slightly unusual software on the server.
I was with this problem here and I come with a different solution:
checkSessionTime();
$interval(checkSessionTime, 2000);
function checkSessionTime() {
var now = (new Date()).getTime();
if (!$localStorage.lastPing) {
$localStorage.lastPing = now;
}
if ($localStorage.lastPing < now - 5000) {
$localStorage.lastPing = undefined;
AuthService.logout();
} else {
$localStorage.lastPing = now;
}
}
I like this solution cause it doesnt add overhead pinging the server nor rely on the window unload event. This code was put inside the $app.run.
I am using angular with a JWT auth, this way to me to log out just mean to get rid of the auth token. However, if you need to finish up the session server-side you can just build the Auth service to do one ping when finishing the session instead of keep pinging to maitain session alive.
This solutionsolves my case cause my intetion is just to prevent unwanted users to access someones account when they closed the tab and went away from the PC.
After lots of search I wrote the below customized code in javascript and server side code for session kill in c#.
The below code is extended in case of same website is open in multiple tabs so the session is alive till one tab of website is open
//Define global varible
var isCloseWindow = false;
//Jquery page load function to register the events
$(function () {
//function for onbeforeuload
window.onbeforeunload = ConfirmLeave;
//function for onload
window.onload = ConfirmEnter;
//mouseover for div which spans the whole browser client area
$("div").on('mouseover', (function () {
//for postback from the page make isCloseWindow global varible to false
isCloseWindow = false;
}));
//mouseout event
$("div").on('mouseout', (function () {
//for event raised from tabclose,browserclose etc. the page make isCloseWindow global varible to false
isCloseWindow = true;
}));
});
//Key board events to track the browser tab or browser closed by ctrl+w or alt+f4 key combination
$(document).keydown(function (e) {
if (e.key.toUpperCase() == "CONTROL") {
debugger;
isCloseWindow = true;
}
else if (e.key.toUpperCase() == "ALT") {
debugger;
isCloseWindow = true;
}
else {
debugger;
isCloseWindow = false;
}
});
function ConfirmEnter(event) {
if (localStorage.getItem("IsPostBack") == null || localStorage.getItem("IsPostBack") == "N") {
if (localStorage.getItem("tabCounter") == null || Number(localStorage.getItem("tabCounter")) == 0) {
//cookie is not present
localStorage.setItem('tabCounter', 1);
} else {
localStorage.setItem('tabCounter', Number(localStorage.getItem('tabCounter')) + 1);
}
}
localStorage.setItem("IsPostBack", "N");
}
function ConfirmLeave(event) {
if (event.target.activeElement.innerText == "LOGOUT") {
localStorage.setItem('tabCounter', 0);
localStorage.setItem("IsPostBack", "N");
} else {
localStorage.setItem("IsPostBack", "Y");
}
if ((Number(localStorage.getItem('tabCounter')) == 1 && isCloseWindow == true)) {
localStorage.setItem('tabCounter', 0);
localStorage.setItem("IsPostBack", "N");
**Call Web Method Kill_Session using jquery ajax call**
} else if (Number(localStorage.getItem('tabCounter')) > 1 && isCloseWindow == true) {
localStorage.setItem('tabCounter', Number(localStorage.getItem('tabCounter')) - 1);
}
}
//C# server side WebMethod
[WebMethod]
public static void Kill_Session()
{
HttpContext.Current.Session.Abandon();
}
For this issue I tried 2 solutions: window.onbeforeunload event and sessionStorage
Since window.onbeforeunload is not only for closing the browser but also redirect, tab refresh, new tab, it was not a robust solution. Also there are cases which the event does not happen: closing the browser through the command line, shutting down the computer
I switched to using sessionStorage. When the user logs in I set a sessionStorage variable to 'true'; when the application is loaded I would check to see if this variable is there, otherwise I would force the user to log in. However I need to share the sessionStorage variable across tabs so that a user is not forced to log in when they open a new tab in the same browser instance, I was able to do this by leveraging the storage event; a great example of this can be found here
tabOrBrowserStillAliveInterval;
constructor() {
// system should logout if the browser or last opened tab was closed (in 15sec after closing)
if (this.wasBrowserOrTabClosedAfterSignin()) {
this.logOut();
}
// every 15sec update browserOrTabActiveTimestamp property with new timestamp
this.setBrowserOrTabActiveTimestamp(new Date());
this.tabOrBrowserStillAliveInterval = setInterval(() => {
this.setBrowserOrTabActiveTimestamp(new Date());
}, 15000);
}
signin() {
// ...
this.setBrowserOrTabActiveTimestamp(new Date());
}
setBrowserOrTabActiveTimestamp(timeStamp: Date) {
localStorage.setItem(
'browserOrTabActiveSessionTimestamp',
`${timeStamp.getTime()}`
);
}
wasBrowserOrTabClosedAfterSignin(): boolean {
const value = localStorage.getItem('browserOrTabActiveSessionTimestamp');
const lastTrackedTimeStampWhenAppWasAlive = value
? new Date(Number(value))
: null;
const currentTimestamp = new Date();
const differenceInSec = moment(currentTimestamp).diff(
moment(lastTrackedTimeStampWhenAppWasAlive),
'seconds'
);
// if difference between current timestamp and last tracked timestamp when app was alive
// is more than 15sec (if user close browser or all opened *your app* tabs more than 15sec ago)
return !!lastTrackedTimeStampWhenAppWasAlive && differenceInSec > 15;
}
How it works:
If the user closes the browser or closes all opened your app tabs then after a 15sec timeout - logout will be triggered.
it works with multiple windows open
no additional load on the server
no problem with F5 / refresh
Browser limitations are the reason why we need 15sec timeout before logout. Since browsers cannot distinguish such cases: browser close, close of a tab, and tab refresh. All these actions are considered by the browser as the same action. So 15sec timeout is like a workaround to catch only the browser close or close of all the opened your app tabs (and skip refresh/F5).
I posted this originally here but I will repost here for continuity.
There have been updates to the browser to better tack the user when leaving the app. The event 'visibilitychange' lets you tack when a page is being hidden from another tab or being closed. You can track the document visibility state. The property document.visibilityState will return the current state. You will need to track the sign in and out but its closer to the goal.
This is supported by more newer browser but safari (as we know) never conforms to standards. You can use 'pageshow' and 'pagehide' to work in safari.
You can even use new API's like sendBeacon to send a one way request to the server when the tab is being closed and shouldn't expect a response.
I build a quick port of a class I use to track this. I had to remove some calls in the framework so it might be buggy however this should get you started.
export class UserLoginStatus
{
/**
* This will add the events and sign the user in.
*/
constructor()
{
this.addEvents();
this.signIn();
}
/**
* This will check if the browser is safari.
*
* #returns {bool}
*/
isSafari()
{
if(navigator && /Safari/.test(navigator.userAgent) && /Chrome/.test(navigator.userAgent))
{
return (/Google Inc/.test(navigator.vendor) === false);
}
return false;
}
/**
* This will setup the events array by browser.
*
* #returns {array}
*/
setupEvents()
{
let events = [
['visibilitychange', document, () =>
{
if (document.visibilityState === 'visible')
{
this.signIn();
return;
}
this.signOut();
}]
];
// we need to setup events for safari
if(this.isSafari())
{
events.push(['pageshow', window, (e) =>
{
if(e.persisted === false)
{
this.signIn();
}
}]);
events.push(['pagehide', window, (e) =>
{
if(e.persisted === false)
{
this.signOut();
}
}]);
}
return events;
}
/**
* This will add the events.
*/
addEvents()
{
let events = this.setupEvents();
if(!events || events.length < 1)
{
return;
}
for(var i = 0, length = events.length; i < length; i++)
{
var event = events[i];
if(!event)
{
continue;
}
event[1].addEventListener(event[0], event[3]);
}
}
/**
*
* #param {string} url
* #param {string} params
*/
async fetch(url, params)
{
await fetch(url,
{
method: 'POST',
body: JSON.stringify(params)
});
}
/**
* This will sign in the user.
*/
signIn()
{
// user is the app
const url = '/auth/login';
let params = 'userId=' + data.userId;
this.fetch(url, params);
}
/**
* This will sign out the user.
*/
signOut()
{
// user is leaving the app
const url = '/auth/logout';
let params = 'userId=' + data.userId;
if(!('sendBeacon' in window.navigator))
{
// normal ajax request here
this.fetch(url, params);
return;
}
// use a beacon for a more modern request the does not return a response
navigator.sendBeacon(url, new URLSearchParams(params));
}
}

Categories