Security question - auth tokens in local storage?

Do you ever put auth tokens in local-storage - so that auth can flow accross multiple tabs or page refreshes? Are there alternative approaches that are more secure, and have you used them in Elm?

Having some discussions on slack around improving the auth packages I have published, but I’d love to get some more input.

4 Likes

Storing the auth token in an HTTP-only cookie is the most secure, I’d say. Then random third-party JS can’t see it. The cookie is automatically sent with all XHR requests made by Elm.

3 Likes

I would agree. Cookies marked http-only and secure are the safest way.

Cookies are also nice, because a page refresh or opening a second instance in another tab still picks up the same cookies, so auth flows over meaning your users do not have to sign in again.

The limitations of cookies are:

  1. Most HTTP APIs want the auth tokens in an Authorization header, instead of a cookie.
  2. Cookies are only sent back to the issuing domain, and sometimes you authenticate against one domain but use an API on another. For example, an app that uses several APIs but only authenticates once to get access to all of them.

And these days, remember to tag with SameSite too

1 Like

What I’m really interested in, is in situations where an API uses the Authorization header, is it possible for auth to flow accross tabs or page re-loads, but keep the credentials secure?

The issue with local-storage is that it is vulnerable to XSS attacks. A succesfull attack could lead to auth tokens being taken from local-storage. Also the browser keeps local-storage on disk, and malware running on a machine could also result in access tokens being stolen.

Elm is less susceptable to XSS attacks, but not 100% fool proof. People have given examples of being able to embed scripts in Elm programs, and also an Elm based site may well pull in other javascript libraries which could potentially be compromised.

At the same time, users want a nice experience and would find it annoying to have to log-in a second time, if they accidentally refreshed the page, for example.

My impression is that localStorage has XSS as an attack-vector, and that cookies has both CSRF and XSS. I’m not sure either will save you from XSS - sure, cookies can’t be read, but I suppose they can still be sent on your behalf?

I would guess ruling out XSS is better to think about, compared to choosing between localStorage and cookies, when it comes to security?

This is also my primary concern with importing from NPM :confused:

I think this can be prevented through the use of a CSRF-token or anti-forgery token. If the attacker making a fake request has no way of reading the anti-forgery token, they cannot succesfuly forge a request.

I’ll read more about it, thank you :sunny: my current impression is that localstorage is simple and xss is so problematic, I might aswell focus on ruling only that out.

I guess the difference with tokens in local-storage vs not, is that an XSS attack could steal the tokens from local storage. But an XSS attack could also install a key logger and obtain username/password, or forge requests to your API, why should it even care about getting the tokens? Also the stolen tokens might be sent somewhere else and used from another IP address, but perhaps the back-end security would notice the change in IP address and shut that session down. So I do see your point about XSS being a bigger threat than local storage.

Local storage is vulnerable to malware running on a machine grabbing it from disc. Also shared computers. So there are possibly some other ways than XSS in which local storage opens up attacks.

At the moment, I am adding the ability to export the LoggedIn state from my auth module, and to try and restore it, with a pair of functions like this:

saveState : Model -> Value
restore : Value -> Result String Model

The idea is that an application can take that Value JSON (which will contain the tokens) and put it in local storage, and on page refresh or new tab, it can retrieve that JSON and attempt to go straight back into the LoggedIn state.

I still feel this is risky, but I think so long as I document the potential risks so that users can make an informed choice that should be ok?

I have another way I would like to try, which may be more secure…

1 Like

The other solution I would like to try, is to do auth through a service worker.

I would take my auth package written in Elm and set up a service worker to run it. The main application would also have an auth module with an identical API, but all its Cmds would go out a port and talk to the service worker, so the application level API would really just be a proxy onto the service worker.

The service worker would also intercept all XMLHttpRequests, and add the tokens to the headers. The tokens would never be stored anywhere, only held in memory, but authentication will flow accross tabs and page refreshes this way.

fwiw There was a blog post linked on the slack some while back where the author arrived at the conclusion that a combo of short lived access token (in session storage e.g.) and a long lived refresh token in cookie (same site, http only, secure) was a good compromise.

2 Likes

That would prevent an XSS attack from stealing the refresh token. Which means the max it can get is the short lived access token - it cannot mount a more persistent attack using the refresh token. Seems a good combination.

Another way might be to keep the access token in the application only, still keeping the refresh token in a cookie. On page refresh or new tab, a new access token is created from the refresh token - so 2 app instances would share the same refresh token, but have unique access tokens. A new CSRF anti-forgery token could also be generated at that time, which is handy otherwise 2 app instances would need to share an anti-forgery token somehow.

I’m starting to like this idea better than the service worker one.

===

In this case I am authenticating against AWS Cognito. The way to use the refresh token there is to invoke this API and pass in the refresh token in the message body:

https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_InitiateAuth.html

Which is annoying, since that prevents using the cookie approach. Perhaps there is a way around that though - write my own Lambda function that takes the refresh token from a cookie, and calls the above API on behalf of the web application.

There does seem to be various life-cycle points in Cognito where you can put your own lambda functions to add custom behaviour around authentication.

Another technique I read about, is that it is better to put sensitive information inside a closure. Is this really true?

Alluded to in the first link, and an example linked to in the second:

So in Elm you might do:

Sensitive a = 
    Sensitive () -> a

protect : AccessToken -> Sensitive AccessToken
protect token = 
    Sensitive (\() -> token)

Does this really make it harder for a script to obtain the token?

I see in the Elm generated javascript code, the whole program is structured like this:

(function(scope){

...

_Platform_export({'Top':{'init':$author$project$Top$main(
	$elm$json$Json$Decode$succeed(_Utils_Tuple0))(0)}});}(this));

Does this effectively make the Elm program private and not accessable to malicious scripts? So there is no need to do something like Sensitive above?

I think the Auth0 article is trying to say that if you do something like this:

class Api {
  constructor(accessToken) {
    this.accessToken = accessToken;
  }

  getItems() {
    return fetch(API_URL, { headers: { Authorization: this.accessToken } });
  }
}

const api = new Api("the_access_token");

Then anyone who have access to the api variable can read api.accessToken, because JavaScript does not have private class fields (yet).

So a different approach would be:

function makeApi(accessToken) {
  return {
    getItems() {
      return fetch(API_URL, { headers: { Authorization: accessToken } });
    },
  };
}

const api = makeApi("the_access_token");

Now, those who have access to api can’t read the access token.

But I would surprised if closuers provide magic/more security. As long as Elm stores the model in a way that cannot be accessed from outside its (function(scope){ wrapper it should be “safe”. An attacker could of course override XMLHttpRequest to see what data you pass to it. Or maybe some String.prototype methods.

This is one of those rabbit holes I keep falling down and as far as I’m concerned there’s no one-size-fits-all solution. It all depends on context.

Most of what I work on are “enterprise” systems that don’t have any third party scripts and typically only accessible from authorised hardware (sitting in the office or over VPN from company laptop). Here I have absolutely no problems dumping the tokens in local/sessionStorage working in this way greatly simplifies frontend work as you don’t have to handle strange things like API calls performing redirects due to tokens expiring. In this case if malware on the client is part of my risk model, another department has really messed up.

On the other end of the scale, if I was doing a public website for a bank I would go with a BFF (Backend for frontend) strategy with all API calls going through a single gateway that uses cookies for authentication.

For everything else in between ItDepends™, although I am very curious about your service worker idea, I’ve generally not got too deep into understanding service workers yet, but at the end of the day, if your app has an XSS exploit they can still execute commands locally from the web-application instead of sending the tokens elsewhere. If the user’s computer is comprimised they could have a browser extention installed that could manipulate your app in many ways I’m not sure the extra security is worth the effort.

As far as I’m aware this is still the leading document regarding token security if you’ve not already seen it https://tools.ietf.org/id/draft-ietf-oauth-security-topics-06.html interestingly this doesn’t seem to cover stolen tokens in the way you describe.

Interestingly the Appendix there mentions

removed refresh token leakage as respective considerations have been given in section 10.4 of RFC 6749

Reading RFC 6749 it sounds like all responsibility is placed on the authorization server and not the client.

3 Likes

Yes. And you would likely have different back-ends for each of your front ends - web, android, iphone, public API and so on. Just last year, I designed some new functionality to integrate with open banking and another 3rd party that does personal finance all for a banks mobile app. The app did not call the 3rd party APIs directly, everything went through the BFF gateway.

At the moment, I am writing an authentication package that will interface onto AWS Cognito. Aiming for the best solution that will work out of the box with that.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.