I'm trying to implement an authentication mechanism where each browser tab may be logged in as a different user.
Here are the rules of this system:
- An authentication token indicates which user is logged in.
- There are 2 kinds of authentication tokens: private and public.
- Each private token is bound to a single tab and determines its account information.
- The public token may be read/written to by any tab and indicates the last account that was logged into (across all tabs).
- When a user logs out in any tab, both the private and public tokens are removed.
- Each time a tab hits a page that requires authentication, the system tries reading the private token. If it is not set (as in the case of a new/blank tab), it tries copying the value of the public token into the private token. If the public token is not set then the user is redirected to an authentication screen.
- When a tab is already logged in and a user clicks on a link, the request must include the private token in a custom HTTP header. Sending this information in the URI is not an option, for security reasons.
- Ability to navigate using the back/forward button the same as you would for normal links (meaning, no prompts to re-submit form data).
What I've tried so far:
Using cookies
for both the private and public tokens: this doesn't work because the server has no way of knowing which cookie to look in. If a user clicks on a link from inside a tab, the request sends all cookies across all tabs and the server has no way of knowing which one clicked on the link.
Storing private tokens in sessionStorage
: This doesn't work because when a user clicks on a link, there is no way to specify custom headers that should be sent alongside the HTTP GET request.
Requesting the page using AJAX, then navigating to the page in memory using Data URIs: For security reasons, Internet Explorer doesn't allow the use of DATA URIs for HTML content. See http://msdn.microsoft.com/en-us/library/cc848897%28v=vs.85%29.aspx
Using <form method="get" enctype="multipart/form-data">
and passing the token using hidden fields: enctype="multipart/form-data" is only supported for POST.
Using <form method="post" enctype="multipart/form-data">
and passing the token using hidden fields: in theory, this should work but now the user gets prompted to re-submit form data if he uses the back/forward button.
Requesting the page using AJAX, then rewriting the current page using document.open(); document.write(); document.close()
. I tried both https://stackoverflow.com/a/4404659/14731 and http://forums.mozillazine.org/viewtopic.php?p=5767285&sid=d6a5a2e8e311598cdbad124e277e0f52#p5767285 and in both cases the scripts in the new <head>
block never gets executed.
Any ideas?
Okay, after going through many different iterations, here is the implementation we ended up with:
Variables
- There are two kind of data stores:
- IndexedDB, which is shared across all tabs.
- sessionStorage which is unique per tab.
- We store the following variables:
- IndexedDB contains
publicToken
, nextTabId
.
- sessionStorage contains
privateToken
, tabId
.
publicToken, privateToken
- See https://stackoverflow.com/a/1592572/14731 for a definition of an authentication token.
- There are two kinds of authentication tokens: public and private.
publicToken
is the token returned by the last login operation, across all tabs.
privateToken
is the token returned by the last login operation of the current tab.
tabId
- Each tab is uniquely identified by a token called
tabId
.
nextTabId
is a number that is accessible across all tabs.
- If a tab does not have an id, it creates a new one based on
nextTabId
and increments its value.
- For example,
tabId
could have a value of "com.company.TabX
" where X
is the number returned by nextTabId
.
Login/Logout
- Every time a tab logs in,
privateToken
and publicToken
are overwritten using the authentication token returned by the server.
- When a user logs out, we delete
privateToken
and publicToken
on the browser side, and privateToken
on the server side. We do not delete publicToken
on the server side.
- This means that anytime a tab logs out, all all tabs sharing the same
privateToken
will get logged out as well. Any tabs using a different token will be unaffected.
- When do multiple tabs share the same
privateToken
? When you open a link in a new window or tab, it inherits the privateToken
of the parent tab.
- If we were to delete
publicToken
on the server, a tab logging out with privateToken
X, publicToken
Y would cause tabs with privateToken
Y to get logged out (which is undesirable).
On page load
- Scan the page for HTML links.
- For each link, append a
tabId
query parameter to the URL. The parameter value is equal to the value of tabId
.
- Strip the
tabId
URL parameter from the current page using history.replaceState() so users can share links with their friends (tabId
is user-specific and cannot be shared).
- Delete the
tabId
cookie (more on this below).
When a link is clicked
- The tab creates a
tabId
cookie and follows the link.
- The cookie has a name equal to the value of
tabId
and a value equal to the value of privateToken
When a server receives a request
- If the
tabId
parameter is missing, then redirect the browser to GetTabId.html?referer=X
where X
is the current URL.
- If
tabId
is present but the authentication token is invalid or expired, then redirect the browser to the login screen.
GetTabId.html
- If the tab does not have a
privateToken
, copy publicToken
into privateToken
.
- If both
privateToken
and publicToken
are undefined, redirect to the login page.
- The page takes a URL parameter called
referer
which indicates where to redirect to on success.
- If the tab has a
privateToken
, append the tabId
parameter to the referer
page and redirect back to it.
- Use
window.location.replace()
when redirecting to remove GetTabId.html
from the browser history.
Why do we keep on deleting/adding cookies?
- We try to minimize the number of cookies sent to the server on each request.
- If we did not delete the
tabId
cookie on page load, then each time a tab would make a request all of the other tabs' cookies would get sent as well.
Known issues
- "View Source" opens the URL missing
tabId
. As result, it gets the source-code of the page which redirects to GetTabId.html
instead of the actual page.
- Awkwardly long page reloads (client is redirected to
GetTabId.html
and back to the original page).
Apologies for the long implementation details, but I could not find an easier/shorter solution.