Prevent session sharing between browser tabs - angularjs

I have a nasty bug: open two tabs with login pages and log in with different users in each one. All requests from first tab that logged in return with 'unauthorized' error.
Frontend uses SESSION cookie and it looks like that cookie is overwritten by second successful login of the second tab and it tries to use this new cookie when browsing in the first tab.
Using Spring Boot 1.5.8, Spring-session 2, AngularJS 1.7.2
Configuration is very standard, so I don't think these boilerplates would be useful.
Until now I tried to set up a filter on backend that works before authentication, to somehow filter out requests that have known cookie, but I failed at this.
UPD:
Some way of preventing that situation when a user is logged in but with incorrect session is what I seek. Either blocking second login attempt in this browser, or kicking already logged in user when another one logs in in the same broser - all will do.

You can log out user from other tabs if you set some sort of token on local storage like the code below(in sucessfull login response from server )
localStorage.setItem('logout', 'logout-' + Math.random());
and have this function as a run block in your main app module:
function logoutFromOtherTabs(authService, $timeout) {
'ngInject';
window.addEventListener('storage', function (event) {
if (event.key === 'logout') {
$timeout(function () {
authService.logout();
}, 1000);
}
});

Related

Auto-Logout after failed checksession using Identity Server 4, VueJS, and oidc-client

I'm testing both server and client on my machine and I'm experiencing the following: I log in to the client fine, do some work, close the browser without logging out. Then I open the client again and I am still logged in (expected) but then a minute later I am auto logged out (NOT-expected).
I am using the oidc-client.js configured like this:
var mgr = new Oidc.UserManager({
userStore: new Oidc.WebStorageStateStore({ store: window.localStorage }),
authority: 'http://localhost:5000',
client_id: 'TST_PORTAL',
redirect_uri: window.location.origin + '/static/callback.html',
response_type: 'code id_token token',
scope: 'api47 openid profile read write offline_access active_dir email',
post_logout_redirect_uri: window.location.origin + '/',
silent_redirect_uri: window.location.origin + '/static/silent-renew.html',
accessTokenExpiringNotificationTime: 10,
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true
})
After further investigation I see the client is calling /connect/checksession (returns status 200) to support single sign-out and then calls /connect/authorize?client_id... which fails (302 redirects to /home/error). The identity server logs say "no user present in authorize request" and "invalid grant type for client: implicit. I have hybrid and client_credentials configured. I read something here so I added this code to my IdentityServer startup:
services.ConfigureApplicationCookie(options =>
{
options.Cookie.SameSite = SameSiteMode.None;
});
But this did not seem to help.
Thinking out loud, could this be a cross domain issue since these run on different ports or I don't have CORs correctly setup? I don't see cors errors. Also should the checksession GET request have parameters? I've been reading the spec and it talks about iframes but not the network traffic so I'm not sure what this traffic should look like.
Update:
The first page of my app is an anonymous auth landing page which checks if they are logged in. If so, it redirects them to the home page. The code for checking is this:
// Get signed in status without prompting to log in
getIsSignedIn() {
console.log('Checking if signed in');
return new Promise((resolve, reject) => {
mgr.getUser().then(function (user) {
if (user == null) {
console.log('Not Signed In');
return resolve(false)
} else {
if (user.expired) {
console.log('User expired');
return resolve(false)
} else {
console.log('Signed In');
return resolve(true)
}
}
}).catch(function (err) {
console.log('Error when checking if signed in');
return reject(false)
});
})
}
This seems to be returning true even when I open a fresh browser. I even changed the Oidc.WebStorageStateStore to use the default rather than localStorage.
There are a lot of things, that could cause this behavior.
1) Single-signout (monitorSession: true)
What does this do? -- After you login into your application (post redirect from IDP server, OIDC-Client JS will include the CheckSession endpoint in an iframe and OIDC library internally pings this iframe every 2 seconds (default) to verify that the idsrv.session cookie value matches with the value inside the applications id token, If they do not match, OIDC will raise user signed out event addUserSignedOut. It is up to your application, how you want to handle when this even get raised from OIDC. Just because you enable single sign out, unless application handles, it will not take the user back to login page.
2) Silent-Renew (automaticSilentRenew: true)
When you have this flag enabled to true, you don't have control on when the silent renew is supposed to happen, by default, it happens before 1 min of access token expiration time. So if silentrenew fails, will raise silentrenew event addSilentRenewError. It is up to your application, how you want to handle when this even get raised from OIDC.
3) From what you are saying if you close the entire browser, and go to url application URL, Identity server cookies should be deleted, as they are session cookies. So, if you had handled single signout event, then OIDC will raise the signout event within 2 seconds after the application gets loaded. Since you are not seeing the login page, I assume that you might not have handled this event in your application.
4) When the silent renew happens (before 1 min of token exp time), This time your app will communicate to IDP server, but you'll not have session cookies as you closed the browser and you will get silentrenew error (probably this time, you might have seen the error you described) and You must have handled silentrenew error in this case and that is why you are seeing login screen. (Just a wild assumption based on ur input).
Hope this helps !!

Service Worker mishandling user login on App Engine

I'm having a particular problem with my Service Worker in conjunction with my Google App Engine app. The service worker installs and activates perfectly. Also, I used the exact same service worker in another app that I built (which didn't require any user login) and it worked fine. However, on my current app, there is only one page (App Engine renders the page and sends it on successful login using the built-in user API). My problem is that I don't want the root address, a rendered page from the server, to be cached. Here is my files to cache array:
var filesToCache = ['/manifest.json','/js/app.js','/js/jquery-3.js','/js/jquery.matchHeight.js','/js/materialize2.min.js','/js/vue.js','/js/vuex.min.js','/js/chart.js','/css/materialize.min.css','/css/style.css', '/images/case.jpg'];
As you can see, I'm only trying to cache the scripts, since the HTML must be rendered server-side, and it is extremely compact, so I don't mind the small network request to be made. And here is my service worker which resides in the root folder.
self.addEventListener('install', function(e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName)
.then(function(cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
.catch(function(e) {
console.error(e);
})
);
});
self.addEventListener('activate', function(e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (key !== cacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
});
self.addEventListener('fetch', function(e) {
console.log('[ServiceWorker] Fetch')
e.respondWith(
caches.match(e.request)
.then(function(response) {
return response || fetch(e.request)
})
);
});
The problem is, despite the fact that I don't try to cache the root address, the service worker will try to return a response instead of a fetch for a user's initial try. This is the sequence of events after a user navigates to the app:
1 - Landing page with button showing 'Sign into Google'.
2 - Redirect to app upon successful login
Expected Result: user is redirected to Google's own solution, after which, the user will be redirected back into my app fully logged in.
Actual Result: service worker intercepts request, serves undefined response, redirects user back to login page ad infinitum.
I'm pretty new to service workers, so perhaps there is something in missing in intercepting response and some edge case I need to account for, but I'm not sure what it is. I've tried splitting up the return statement to first check if there is a response, so if the response is undefined, it skips the return response statement, cloning the event, and then returning the event request in a fetch request. However, I got exactly the same result.
Has anybody come across this before or know what to do? I spent all yesterday on this to no avail and just gave up on returning any response at all, serving everything from the network, but I really want to solve it. Thanks for any help in advance.

Angularfire Auth session persistence

This is my first time using AngularJs, not sure whether my authentication handle correctly or not.
I expect $firebaseAuth() will keep the authentication status even if I refresh the app. But every time I refresh the app, $firebaseAuth() will not re-authenticate the user, and I need to re-login.
I read through the document and search in the source code, but can't find the function that allows me to configure the session persistence. Only 2 parameters email and password are accepted.
versions:
angularjs v1.4.10
angularfire v2.0.1
firebase v3.0.3
my authenticationService litcoffee script
app.factory "authenticationService", [
"$firebaseAuth"
"$location"
"$rootScope"
($firebaseAuth, $location, $rootScope) ->
login: (email, password, redirectUrl = "/") ->
$rootScope.authObj.$signInWithEmailAndPassword(email, password)
.then(
(firebaseUser) ->
console.log firebaseUser
$rootScope.firebaseUser = firebaseUser
return true
(error) ->
$rootScope.firebaseUser = null
console.log error
alert error.message
return false
)
logout: (redirectUrl = "/login") ->
$rootScope.authObj.$signOut()
$location.path redirectUrl
isLogged: () ->
if $rootScope.authObj.$getAuth()
return true
else
return false
]
I check user authentication status on a controller which will call authenticationService.isLogged(), it will redirect to login page if user is not logged.
What I want to achieve is just a simple authentication, user will remain authentication status even if they refresh.
Please correct my if I'm on the wrong direction. Any help would be greatly appreciated.
It's my fault actually.
Firebase and AngularFire keep the authentication session persistence by default.
What I did wrong is check the authentication status immediately when the page loaded. At the moment AngularFire hasn't fired yet, it will take some time to re-authenticate the application.
Thus, $firebaseAuth.$onAuthStateChanged can be very helpful.
Listen to it when your page loaded. Once the event fired, it will notify you whether the user authenticated.

Authentication Logic - Server vs Client

I'm trying to get my head around where authentication logic should live in my application, the approach I am trying to take is to have any auth responsibility handled by the server, redirecting to a login page that's separate from the main client side app - which I think is sensible?
I have an angularjs application which uses ui-router and makes api requests which are routed via the server.
I am using an Express server which is configured to use a couple of directories like so:
app.use(express.static('./dist/client'));
app.use(express.static('public'));
I then have middleware that performs an auth check (Im using express-session as well) and redirecting to login when required.
//A request to '/login' will serve the login page
app.use('/login', function(req, res){
res.sendFile(path.join(__dirname+'/public/login.html'))
});
//This will listen for all requests
app.use(function(req, res, next) {
if (req.url!== '/auth/login' && !req.session.accessToken) {
res.redirect('/login');
return;
}
next();
});
On initial page load, when no session cookie exists, express immediately redirects to the login view as expected.
After logon, and the main application loads, if I then manually delete the cookie in the browser and perform a state change that requires an api request (in a state resolve), the server returns the login view but this gets rendered inside the ui-view component being used by ui-router, rather than a full redirect to /login by the server.
Also, if I navigate to a page (after deleting cookie) that does not perform an api request, that page is served back, I guess as its not covered by my app.use middleware that does the redirect.
I feel I'm missing something obvious here, can someone help me please?
One way to handle this, there are others:
Make the API server return a 401 (unauthorized) error if the user is not authenticated, rather than redirecting them to the login page.
Then, in a run block, add a $stateChangeError event handler to the $rootScope. This way, if an API request is made from an unauthenticated user, it will trigger the event handler. From there you can redirect the user to your login page:
angular.module('myApp').run(function($rootScope, $window) {
$rootScope.$on('$stateChangeError', function() {
$window.location.href = '/login';
});
});
I'm not sure if it makes sense to worry about the other scenario where you delete the cookie and navigate to a page that does not make any API requests. What is such a user going to gain? In this hypothetical scenario, they are already looking at a page in your app (that might have sensitive data or not). How did they get there to begin with?
You could use a similar event handler for the $stateChangeStart event that checks for the presence of the cookie and redirect if it's missing. But, you don't want to put code in your client that validates the cookie, b/c then any curious visitor could read that code and learn how to create a cookie to fool your server.

Firebase $onAuth has wrong authData after $authWithOAuthRedirect from Facebook

I am trying to authenticate users of my Firebase (Angularfire) app with Facebook Login.
Everything works as expected when I authenticate with a pop-up window, but to support as many browsers as possible (Chrome on iOS doesn't support pop-ups, for e.g.) I want to fallback to authenticating with a redirect ($authWithOAuthRedirect).
I have confirmed my setting in Facebook are correct (my app ID and secret, for e.g.) but when I am redirected back to my app after Facebook authenticating with a redirect, $onAuth fires but I don't have my Facebook authData.
Instead, I have anonymous authData. For a bit of background; all users are authenticated anonymously if they are not otherwise authenticated (with Facebook, in this e.g.).
I can't see to find why this would be - the user should now be authenticated with Facebook, and have the Facebook authData.
Excepts of my code are below for some context:
Triggered when a user clicks the login button
function logIn () {
firebaseAuth
.$authWithOAuthRedirect('facebook', function (error) {
if (error) {
console.log(error);
}
});
}
$onAuth (inside my Angular app's run)
function run ($rootScope, firebaseAuth, sessionStore) {
$rootScope
.$on('$routeChangeError', function (event, next, prev, error) {
if (error === 'AUTH_REQUIRED') {
console.log(error);
}
});
$rootScope
.$on('$routeChangeSuccess', function (event, current, prev) {
$rootScope.title = current.$$route.title;
});
firebaseAuth
.$onAuth(onAuth);
function onAuth (authData) {
console.log(authData);
}
}
Route resolver to otherwise anonymously authenticates users
function sessionState ($q, firebaseAuth) {
var deferred = $q.defer();
firebaseAuth
.$requireAuth()
.then(deferred.resolve, guest);
return deferred.promise;
function guest () {
firebaseAuth
.$authAnonymously()
.then(deferred.resolve, rejected);
}
function rejected () {
deferred.reject('AUTH_REQUIRED');
}
}
The route resolver (sessionState) checks to see if the user is authenticated already, and if not, tries to anonymously authenticate them.
After the Facebook authentication redirect, the user will already be authenticated, and therefore does not need to be anonymously authenticated.
But, it appears that they are? As $onAuth logs the authData to the console, and it is anonymous.
Any help with this would be much appreciated! I am sure it has something to do with my route resolver, as pop-up authentication works fine (the route is already resolved).
EDIT: I tried completely removing my route resolver in case it was that causing an issue, but it made no difference. The user was just 'unauthenticated' instead of being either authenticated with Facebook (after $authWithOAuthRedirect) or anonymously.
UPDATE: I tried authenticating with Twitter and the redirect transport and I have encountered the exact same problem. I have also tried using port 80, instead of port 3000 that my app was being served on locally, but no joy.
UPDATE: When I turn off html5Mode mode in my app - and routes now begin with #s - $authWithOAuthRedirect works perfectly. From this I can only assume that $authWithOAuthRedirect does not support AngularJS's html5Mode. Can anyone confirm this is an issue, or do I need to change my code to support html5Mode and authWithOAuthRedirect?
EXAMPLE REPO Here is an example repo demonstrating the problem: https://github.com/jonathonoates/myapp
Look in the dist directory - you should be able to download this and run the app to reproduce the problem. In scripts/main.js is the app's JS; I've added a couple of comments but it's pretty self explanatory.
To reproduce the problem: click on the 'Facebook Login' button, and you'll be redirected to Facebook to authenticate. FB will redirect you back to the app, but here lies the problem - you won't be authenticated, and the returned authData will be null - you'll see this in the console
UPDATE: When I add a hashPrefix in html5Mode e.g.
$locationProvider
.html5Mode(true)
.hashPrefix('!');
The app works as I would expect - authenticating with Facebook and the redirect transport works.
Couple of niggles though:
The URL has #%3F appended to it, and is available/visible in the browser's history.
This would rewrite URLs with #! in browsers that do not support History.pushState (html5Mode), and some less advanced search engines might look for a HTML fragment because of the 'hashbang'.
I'll look into highjacking the URL upon being redirected back from Facebook instead of using hashPrefix. In the URL there is a __firebase_request_key which may be significant e.g.
http://localhost:3000/#%3F&__firebase_request_key=
It looks like this is indeed an incompatibility between Firebase and AngularJS's html5mode as you suspected. At the end of the redirect flow, Firebase was leaving the URL as "http://.../#?", and Angular apparently doesn't like that so it did a redirect to "http://.../" This redirect interrupts Firebase (the page reloads while we're trying to auth against the backend) and so it is unable to complete the authentication process.
I've made an experimental fix that ensures we revert the URL to http://.../#" at the end of the redirect flow, which Angular is happy with, thus preventing the problematic redirect. You can grab it here if you like: https://mike-shared.firebaseapp.com/firebase.js
I'll make sure this fix gets into the next version of the JS client. You can keep an eye on our changelog to see when it is released.

Resources