amplify-js
amplify-js copied to clipboard
Sensitive information are being persisted in local storage
Describe the bug All cognito session tokens id, access and refresh tokens are being persisted into localstorage. This goes against all industry security best practice of storing sensitive infomation in signed httponly cookies.
To see why its bad practice this article presents a summary.
- https://dev.to/rdegges/please-stop-using-local-storage-1i04
- https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTML5_Security_Cheat_Sheet.md
Also known as Offline Storage, Web Storage. Underlying storage mechanism may vary from one user agent to the next. In other words, any authentication your application requires can be bypassed by a user with local privileges to the machine on which the data is stored. Therefore, it's recommended not to store any sensitive information in local storage.
Is this being ignored? or is there somewhere we can track the status of this?
We are evaluating this. This argument can be ambiguous as well. For example see thread here: https://www.reddit.com/r/Frontend/comments/cubcpj/local_storage_vs_cookies_for_auth_tokens/ in addition to the comments/discussion in the above thread you referenced.
However, it's an interesting problem in this particular space (client-side/browser), with the ultimate solution of simply not storing / using a persistent session (only storing in memory, which you can do with the SDK currently). Obviously, the down-side to this is needing to login in every time the page is refreshed. This issue exists storing in any storage medium i.e. localstorage, cookies / even httpOnly; even though JS can't access httpOnly tokens, this cookie is still sent with every request, so if JS is injected into your app (considering the main argument on this is generally XSS), API requests will technically still be made as an authenticated user.
That said, the underlying Cognito SDK is using temporary credentials, these credentials need to be refreshed and Signature V4 signed in order to make authenticated AWS API calls. Also, these credentials are scoped to your authenticated/unauthenticated identities. So essentially, if someone was to first access your computer, and get into your web browser, they would need to create a signed request using an AWS SDK with your secret key and access key before the credentials have expired. You can also invalidate these credentials in the event of this via the IAM console.
However, it's still an ambiguous topic with problems in all areas. We will be leaving this issue open to track feedback on this as we evaluate the path forward and most secure approach available when utilizing a client-side / serverless architecture with amplify.
Hi, thanks for reopening.
I agree that the opinions and strategies here are not universally valid.
In my case I am using amplify to manage authentication with cognito by just retrieving and using Access Bearer tokens to call API Gateway with cognito authorizers, so no IAM temporary credentials for me.
However the access token is temporary as well and the same principle applies (the attacker could use the token just until it expires). My main concern is the refresh token, which has a much longer expiration time and can be used to reclaim fresh credentials until the attacker is spotted and the token are revoked.
I will be much more confident by following a strategy similar to the silent authentication implemented in Auth0, or knowing that there is a good reason to have a refresh token in the localStorage of my angular SPAs.
https://auth0.com/docs/tokens/refresh-token/current
Otherwise, can anyone confirm if implementing the Authorization code grant pattern leverages the problem of storing such refresh token?
Ah i see, Assuming you are referring to this as to what you are using? https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html
The temporary credentials I am referring to are the ones that are signing all your AWS requests i.e. secret key and access key. Are you using the Amplify Auth Category, initialized by the CLI with your project, as in, are the services (API Gateway, Cognito etc...) in your application using authenticated / unauthenticated IAM roles?
Exactly, we are providing a manual cognito configuration to the amplify SDK for angular and we are using such cognito user pool as an authorizer for our APIs (both using direct integration and lambda authorizers depending on the microservice)
We use just the user pool with an OAuth Authorization Code Grant, so amplify redirects to the hosted UI and parses the corresponding callback query param, it is already very nifty compared with our previous implementation without amplify.
Ah ok understood now thanks! So, you are not using federated identities at all and user pools directly. I was actually thinking that potentially the lambda authorizer could be an interesting solution in this case, even to potentially add a layer of security. We are working on some authentication work now that's still in the design phase, so this is a good thread to add to these discussions, will track/keep this up-to-date here as we move along, thanks for the details.
@jacintoArias do you have device tracking turned on in Cognito User Pools? This will actually add additional security to the refresh token which it will only work on the specific device: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-device-tracking.html
Tracking the device will definitely improve the security of the refresh token and fits in our requirements, thanks for the recommendation we will give it a try.
We are currently migrating from a lambda authorizer to directly integrating with cognito beacuse we precisely think that cognito's implementation of token validation could be more advanced than our own custom token validation. Basic authorization is being done by filtering the endpoints using oauth claims in the token.
I will be very interested in knowing if If you think that a lambda authorizer is a better and more secure approach than leveraging the cognito integration directly...
Great to hear! Yes i think the device tracking is a good solution here. For the authorizer i think the Cognito one is good for what you are doing, i was more referring to a possible solution where perhaps we could utilize to add a layer of additional validation, however the device tracking does a good job of locking down the refresh token per device.
@mlabieniec I wonder how accurate device tracking is. Is there an easy way for users to reset the tracking id?
What about SPA use-cases, requiring a user to stay logged in greater than 1 hour but no more than 8 hours?
Would the Cognito team (I presume amplify works closely with them) take as feedback to please allow either:
- refresh token expiration less than 1 day (so that if stolen has very limited TTL)
- access token expiration at most 8 hours (obviates the need for refresh token, but still limited TTL)
Either of the above enhancements would be arguably safer since it limits the TTL of the very sensitive refresh token information.
Secondary concerns that compound vulnerability:
- amplify is javascript, so it can't set http-only cookies and non http-only cookies are especially prone to XSS
- Cognito doesn't support implicit grant type or PKCE, forcing SPA implementations to store refresh token in local storage or cookies
- refresh tokens are very big, such that network request payloads are more demanding to handle if malicious.
Similar security concerns expressed here: https://github.com/aws-amplify/amplify-js/issues/3224 https://github.com/aws-amplify/amplify-js/issues/1218 https://github.com/aws-amplify/amplify-js/issues/3774 https://github.com/aws-amplify/amplify-js/issues/2213 https://github.com/aws-amplify/amplify-js/issues/1735
@Jun711 If you delete local storage, a new device ID will get generated.
@evbo will look into this for you and get some details from the Cognito team on these requests/concerns.
@mlabieniec thank you.
Just wanted to emphasize that my second suggestion: increasing access token expire is really the preferred solution in lieu of silent auth features being supported.
Here's some additional useful context for the Cognito team. I hope they receive this feedback well:
-
The security issue at hand is well stated here: https://github.com/aws/amazon-cognito-auth-js/issues/92
-
Configuring access token expire is inline with the spec (there is no documented min/max required): https://tools.ietf.org/html/rfc6749#section-4.2.2
-
Configurable access token expire for Cognito is a fairly popular discussion on SO/forums as well:
-
https://stackoverflow.com/questions/42712872/how-to-modify-expiry-time-of-the-access-and-identity-tokens-for-aws-cognito-user/42725796
-
https://forums.aws.amazon.com/thread.jspa?messageID=880202󖹊
-
@evbo I have discussed with the cognito team this feature and they have informed me that this is in their backlog as a feature. I do not have a completion date on this yet tho will keep this issue open to track progress from their side.
The issue linked by @evbo suggests that the Implicit Grant flow is not suitable for SPAs due to this limitation from cognito:
Quoted from linked issue:
I would say that without Cognito implementing prompt=none on the /oauth2/authorize endpoint, and whilst the cognito cookie on <your_domain>.auth.
.amazoncognito.com/ expires after 60 minutes instead of 30 days (or what is set for the user pool) it is unsuitable to be used as an out-of-the-box solution for Single Page Applications.**
Does this imply that the Implicit Grant flow that Amplify provides is also subject to the same limitation and therefore should not be used for Single Page Applications?
Am I understanding correctly that all Authorized requests will start failing after 60 minutes in a browser?
I was going through aws amplify doc .. I see a statement about “if you want a secure storage of token than local storage then you can do that ”. Is this helps in solving the problem ?
Hello , I see in amplify documentation there is a recommendation of using store if we want to store the token in a more secured way .. has any one tried that ? Is thy something will help in getting out of this security concern ?
There is a lot of FUD in this thread, so I thought it would be helpful to provide a summary of the topic:
-
Protecting users from unauthorized access to their computers (physically or through malicious code) is outside the scope of the web application security model. Arguments against local storage based on this threat model can be discarded.
-
Any information stored in local storage can be stolen by an attacker exploiting an XSS vulnerability. So when JWT tokens are stored in local storage AND an attacker successfully exploits an XSS vulnerability, the attacker can send the tokens to their server and use them to log in from a new device. (This is in addition to all the other bad things an attacker exploiting an XSS vulnerability can do.)
The solution to mitigate the second point is to set the remembered devices functionality to "Always On" in Cognito. Doing this will make Cognito verify the device identifier associated with the refresh token on log in, thus preventing an attacker from signing in with a stolen refresh token on an other device. (docs)
@abiro I agree that the storage vulnerabilities you've pointed out aren't the primary concern, but instead I think if Cognito could relax the TTL configuration limits it would enable developers to diminish the attack surface substantially, but unfortunately there's no free way of sending Cognito this kind of feedback directly.
I think many just want to follow the OAuth documentation:
Access tokens must be kept confidential in transit and in storage. The only parties that should ever see the access token are the application itself, the authorization server, and resource server. The application should ensure the storage of the access token is not accessible to other applications on the same device. The access token can only be used over an https connection, since passing it over a non-encrypted channel would make it trivial for third parties to intercept.
These sentiments have been echoed in regards to both Access and Refresh tokens by leading competitors of Cognito: Auth0, Storm Path
So where I agree a lot of FUD and hot debates occur is surrounding the decision on how authentication proofs are stored. While there's no perfect answer, I think many agree that one very simple tuning mechanism is the TTL of these tokens, which can allow the trade off between attack surface and accessibility to be decided on a case by case basis.
For instance, to fulfill my particular need for a TTL ~8 hours for a basic SPA, even a 24 hr Refresh token was overkill, so the least attack surface I could provide at a reasonable development cost was to only persist the Refresh token in an HTTP only cookie with a path: /refresh (so CSRF is quite limited and bandwidth isn't hogged) and with the Access token only in memory. This was a bit more moving parts than I would have liked, but minimized the amount of sensitive information storage inline with the documentation.
So what could be better? Simply configuring an 8 hour Access Token would be 16 hours safer and also obviate the need for refreshing tokens. So simply being able to configure TTL beyond the current limits (1 hr for access, >= 24 hr for refresh) would be nice, as that way it is tailored to individual needs better and the worry over storage methods can be lessened.
Also, I like your suggestion of Device Tracking. Still, it requires the promise of anonymity to be trusted by users so not a silver bullet in that regard.
@abiro Device tracking keys/secrets are also stored in localStorage, so vulnerable to the same type of risks as the refresh tokens stored there. But I assume other meta-data about IP, OS, etc are also used to confirm device.
@mlabieniec any update here please? Thanks
All these problems would really be solved by a prompt=none option. The safest way to store is in transient memory. The best way to handle token expirations in my experience was the iframe solution, this is currently a feature in oidc-client.js, but i have had project where i built a "oauth2" client from scratch and have had success refresh tokens silently using the iframe solution.
Works like this.
Your main context creates an iframe and points it to the idp together with the configs (clientId, redirect, etc..). Make sure the redirectUri is set to a blank route or a blank static .html file
function silentRefresh() {
return new Promise((res) => {
let iframe = document.getElementById('my-silent-refresh-iframe');
iframe.style = 'width: 0px; height: 0px; overflow: hidden';
iframe.addEventListener('message', (mes) => {
// the token is in the ``mes`` payload
res();
// empty out the frame or destroy after
iframe.textContent = '';
})
iframe.src = 'https://my.idp?clientId=....&redirectUri=https://mysite.com/silent_refresh.html'
})
}
Your silent request context can then send the hash or parsed hash back to the main context using window.top.postMessage
<script>
window && window.top.postMessage(parseHash(window.location.hash))
function parseHash(hash) {
var data = hash.substr(1);
var dict = data.split("&");
return dict.reduce((props, val) => {
var map = val.split('=');
props[map[0]] = map[1];
return props;
}, {})
return oidcResponse;
}
</script>
MAIN PROBLEM: If cognito asks for user credentials this strategy just doesn't work. The iframe would just be stuck at the login page of cognito or some other federated idp.
The prompt=none solves this issue. I hope this thread is still being monitored by the amplify team or the cognito team
The solution to mitigate the second point is to set the remembered devices functionality to "Always On" in Cognito. Doing this will make Cognito verify the device identifier associated with the refresh token on log in, thus preventing an attacker from signing in with a stolen refresh token on an other device. (docs)
I tried using the remembered device functionality in cognito to mitigate this problem. However, I believe that cognito only verifies the device identifier when using the refresh token to generate a new access token.
However, if I use a different device to access my resource server using an access token provided to an authenticated client on another device, it lets me through. According to my understand, this will leave the access token in the local storage vulnerable.
Is there a workaround for this problem yet?
There are 3 related problems here:
- Cognito not supporting prompt=none
- Browsers no longer supporting this flow (Safari in particular)
- SameSite=strict being considered more secure than this flow
I have an Online AWS Cloudfront Deployed SPA, which uses Cognito and which anyone on this thread can run.
The SPA is also runnable locally via this GitHub repo. This is an AWS implementation of Curity's Token Handler Pattern.
@gary-archer I wonder if it might run into the max cookie-size limit. Some of these tokens are pretty large. To get around that you could encrypt the token, store the key in the cookie, store the encrypted content the browser's storage, and pass both to the proxy.
@gary-archer: Do we need Lambda@Edge for this or could we not just use lambda behind API Gateway to do the same?
Hi @rspuler. The key point around wrapping refresh tokens in cookies is that a same domain cookie works best, since these days browsers such as Safari will aggressively drop cross site cookies - especially on POST requests. I would have used a normal API Gateway lambda otherwise, as you suggest.
You can run my AWS hosted SPA from my Quick Start Page, to see the desired end behaviour. My demo SPA stores access tokens in memory and refresh tokens in cookies. It has zero cross site cookie problems - as you can see by:
- Logging in
- Then reloading the browser (F5, Cmd+R etc)
- Opening new browser tabs
- Clicking Expire Access Token followed by Reload Data
Having Authorization servers put tokens into cookies is directly against the Oauth Spec - Tokens are returned in response bodies.
This basically re-introduces the security problem that existed before 'httponly' cookies - if someone can pull off XSS, they can steal the users credentials and act as the user without the need to persist inside their browser. This is particular problematic with Singe Page Applications as they typically have a well documented or at least easier to understand API than traditional web applications.
AWS, is there a plan to do some enhanced XSS protection here? I know cookies aren't going to fly, but I've seen some examples of folks using webworkers to hold refresh tokens such that JS code cannot read tokens.
@gary-archer I mentioned it in comments above and it's been emphasized in newly updated docs from Auth0 that all efforts should be made to avoid persisting tokens client-side in SPA applications, BUT for everyone like us who must (e.g. to maintain login across browser refresh or multiple tabs) then the best thing you can do is limit the expiration of said tokens:
To reduce security risks if your SPA is using implicit... ...or hybrid flows, you can reduce the absolute token expiration time. This reduces the impact of a reflected XSS attack (but not of a persistent one).
@mlabieniec Good news, Cognito team implemented much more flexible token expiration configuration. Thanks for relaying feedback to them.
@gary-archer Have you also considered putting session management responsibilities onto the OpenID Provider/Authorization Server(or whatever system the user uses to authenticate to them).
What I mean is if the users browser had a session cookie (samesite/secure/httponly) for the OP/AS, there would be no need to persist the refresh token to storage. Upon seeing an unauthenticated user/no tokens, the SPA would redirect to the OP/AS but the user would already have a session set to that system.
The major downside here is that there is a need to manage session state at OP/AS, but that challenge doesn't seem insurmountable.