我一直在研究如何最好地在单页应用程序(SPA)中存储身份验证令牌。 SO 上关于这个主题存在一些争论,但据我所知,没有一个提供具体的解决方案。
昨天和今天花了很多时间在互联网上寻找答案,我发现了以下内容:
-
本地存储 API。我发现一些基本指南建议使用 localStorage
(尽管许多人正确地建议反对)。我不喜欢这种方法,因为存储在 localStorage
中的数据在发生 XSS 攻击时可能会被访问。
-
Web Workers。如果令牌存储在 Web Workers 中,则打开新选项卡时用户将不会登录。这会造成不合格且令人困惑的用户体验。
-
关闭。与 Web Workers 相同 - 没有持久性。
-
HttpOnly Cookies。 一方面,我了解到这可以防止 XSS。然而,另一方面,这是否意味着我们现在必须处理CSRF?那么这就完全是一场新的争论:如何使用 SPA + REST API 实现 CSRF 令牌?
其他人都是怎么做的?
I have been researching how best to store authentication tokens in a Single Page Application (SPA). There is some existing debate about this topic on SO but as far as I can see, none offer concrete solutions.
Having spent much of yesterday and today trawling the internet for answers, I came across the following:
-
Local Storage API. I found that some basic guides suggest the use of localStorage
(though many rightfully advise against it). I do not like this approach because data stored in localStorage
could be accessed in the event of an XSS attack.
-
Web Workers. If the token is stored in a web worker, the user will not be logged in if a new tab is opened. This makes for a substandard and confusing user experience.
-
Closures. Same as Web Workers - there is no persistence.
-
HttpOnly Cookies. On the one hand, I read that this can protect from XSS. However, on the other hand, wouldn't this mean that we now have to deal with CSRF? Then it's a new debate altogether: how does one implement CSRF tokens with an SPA + REST API?
How is everyone else doing it?
发布评论
评论(3)
我很高兴你问这个问题。前端经常出现关于 oauth2 的模因,这确实污染了辩论,并且很难找到事实信息。
首先,关于我建议重新考虑的一些排除选项:如果您需要在多个选项卡上进行相同的身份验证,您仍然可以使用任何将令牌存储在窗口范围内的选项,但单独管理令牌并获取页面刷新时出现新的(静默刷新,因此标准提示=无流程)。这打开了一些选项:服务工作者、网络工作者、闭包……确实,其中一些最初并不是为了这个目的,但它很好地解决了问题。这还解决了一系列有关刷新令牌的竞争条件(它们只能使用一次,因此每个选项卡都有一个可以解决一堆问题)。
话虽如此,以下是选项:
本地存储:如果 XSS 攻击成功,令牌可能会被盗。 XSS = 无论如何游戏结束(在这种情况下,没有黑客会关心你的令牌,这是不需要的)。与 cookie 的典型小时/天有效性相比,还可以通过使用短期令牌来缓解这种情况。无论如何,建议使用短期令牌。
现在,XSS 导致的令牌被盗对于某些人来说似乎是一个重要问题,所以无论如何让我们看看其他选项。
会话存储:与本地存储相同的缺点(XSS 可能导致会话泄漏),引入了自己的 CSRF 问题,但也解决了其他一些问题(刷新...)。
网络工作者:这实际上是一个很好的解决方案。如果应用程序的随机部分成功发生 XSS,它将无法窃取令牌。理论上,如果可以注入一些在身份验证(身份验证代码或令牌交换)时运行的脚本,它也可能被利用......但这对于所有流都是如此,包括 cookie/会话。
闭包:与网络工作者相同。不太孤立(更容易被窃取您代币的人替换)。
Service Worker:我认为理想的解决方案。易于实现(您只需拦截获取请求并在几行代码中添加您的令牌)。无法被XSS注入。您甚至可以说它实际上是针对特定用例的。它还解决了页面上多个应用程序的情况(它们共享 1 个服务工作线程,在需要时添加令牌),而其他选项都不能很好地解决这个问题。唯一的缺点:浏览器可以终止它,您需要实现一些东西来延长它的生命周期(但有一个记录在案的标准方法)。
HttpOnly Cookies:简而言之,您将过渡到传统的服务器端 Web 应用程序(至少对于某些部分),它不再是具有标准 oidc 或 aouth2 的独立 SPA。这是一个选择(现在已经不是我的选择了),但它不应该是由代币存储驱动的,因为有一些选择甚至更安全并且可以说更好。
结论:我的建议是仅使用本地/会话存储。成功的 XSS 可能会让您失去工作或失去客户(提示:当他们可以调用
pay(5000000, lulzsecAccount)
API 时,没有人会对您的令牌感兴趣)。如果您对令牌存储很挑剔,那么 Service Worker 是我认为的最佳选择。
I'm happy that you're asking this question. There are recurring memes regarding oauth2 on the frontend that are really polluting the debate, and finding factual information is difficult.
First, regarding some excluded options which I suggest reconsidering: if you need the same authentication on multiple tabs, you can still use any option that would store tokens in a window scope, but individually manage tokens and get a new one on page refresh (silent refresh, thus standard prompt=none flow). This opens some options: service worker, web workers, closures... True, some of this isn't meant for that originally, but it solves the problem nicely. This also solves a bunch of race conditions about refresh tokens (they can only be used once, so having one for each tab solves a bunch of problems).
That being said, here are the options:
Local storage: in case of successful XSS attacks, tokens can be stolen. XSS=game over anyway IMO (no hacker will care about your token in such a case, it's not needed). It can also be mitigated by having short-lived tokens in comparison with the typicial hours/days validity of cookies. In any case, short-lived tokens are recommended.
Now, stolen tokens in case of XSS seem to be an important issue for some people, so let's look at the other options anyway.
Session storage: same downsides as local storage (XSS can lead to session leakage), introduces its own CSRF issues, but also solves some others (refresh...).
Web workers: this is actually a nice solution. In case of successful XSS in a random part of the application, it won't be able to steal tokens. In theory, if one could inject some script that would run at authentication (auth code or token exchange), it could be exploited too... but that's true for all flows, including cookies/sessions.
Closures: same as web workers. Less isolated (easier to replace by one that would steal your token).
Service worker: ideal solution in my opinion. Easy to implement (you can just intercept fetch requests and add your token in a few lines code). Can't be injected by XSS. You could even kind of argue that it's actually meant for that exact use case. It also solves the case of multiple applications on a page (they share 1 service worker which adds token when required), which none of the other options solves nicely. Only downside: browser can terminate it, you need to implement something to extend it's lifetime (but there is a documented, standard way).
HttpOnly Cookies: in short, you're then transitioning to a traditional server-side web application (at least for some parts), it's not an independent SPA with standard oidc or aouth2 anymore. It's a choice (it's not been mine for some years now), but it shouldn't be motivated by token storage, as there are options for that which are even secure and arguably better.
Conclusion: my recommendation is to just use local/session storage. Successful XSS will probably cost you your job or your customer anyway (hint: nobody is interested in your tokens when they can call the
pay(5000000, lulzsecAccount)
API).If you're picky about token storage, service worker is the best choice IMO.
解决方案是a httponly samesite cookie。
在问题中,您正确地注意到
httponly
保护XSS。Samesite
反过来保护CSRF。这两种选项均得到所有现代浏览器的支持。这比其他解决方案要简单,更安全。很容易在API上设置并完全透明与水疗中心。安全是内置的。
混凝土解决方案:
实际身份验证可以由您的API或将重定向到API的外部提供商来完成。然后,当登录时,您的API会创建JWT令牌并将其存储在
httponly samesite
cookie中。您可以在 nestjs-starter 中所解释的:。一个限制是API和SPA必须在同一域上。
为了存储令牌客户端,此另一个 a>非常全面。
The solution is a HttpOnly SameSite Cookie.
In the question, you correctly note that
HttpOnly
protects from XSS.SameSite
in turn protects from CSRF. Both options are supported by all modern browsers.This is orders of magnitude simpler and safer than other solutions. It's easy to set up on the API and completely transparent to the SPA. Security is built-in.
Concrete solution:
The actual authentication can be done by your API or by an external provider that redirects to your API. Then, when logged-in, your API creates a JWT token and stores it in a
HttpOnly SameSite
Cookie. You can see this at work with NestJS at nestjs-starter as explained in: OAuth2 in NestJS for Social Login (Google, Facebook, Twitter, etc).One limitation is that API and SPA have to be on the same domains.
For rather storing the token client-side, this other article is very comprehensive.
您的比较有点令人困惑...当您谈论水疗身份验证时,您需要一个解决方案来存储身份验证令牌。网络工作人员和关闭的主要目的是运行代码,而不是存储身份验证令牌。您可以将其写入硬编码变量,但这不是其目的,这就是为什么新标签无法识别它的原因。
因此,我们留下了本地存储和会话cookie。
在水疗和客户端渲染之前,我们过去只有服务器端渲染和cookie。这是发明httponly的时候,很难窃取会话ID和用户的身份。当发明客户端渲染时,发明了无状态的API。这些API不使用会话ID,而是使用令牌,例如JWT。这意味着服务器不会在会话ID标识的会话中保存有关用户的任何信息。相反,JWT令牌包含令牌本身中的签名信息,服务器不记得任何内容,并在每个请求中重新验证用户。还有一种混合方法的代币方法可以保存在服务器端(例如Redis)中的NOSQL DB中,以更快地使身份验证过程更快。
但最重要的是,httponly cookie与使用会话的状态API一起使用,LocalStorage与使用代币的无状态API一起使用。您也可以以相反的方式使用它(具有令牌的cookie,localstorage和会话),但“自然不太自然”。
至于XSS,cookie中的httponly机制会使攻击者的生活变得更加艰难,但是即使使用了攻击者,攻击者仍然可以通过网络钓鱼造成很大的伤害。因此,它并不关键,因为这只是一个补偿控制,而不是任何方式的主要缓解措施,因此您可以安全地使用localstorage。此外,cookie容易受到LocalStorage不存在的CSRF攻击。
因此,这两个选项都是有效的。
您可以在此处阅读更多信息:
localstorage,sessionstorage,session,session和cookies有什么区别?
Your comparison is a bit confusing... When you talk about SPA authentication, you need a solution that alows you to store authentication tokens. The main purpose of Web Workers and Closures is to run code, not for to store authentication tokens. You can write it down as a hard coded variable but it's not its purpose and this is why a new tab won't identify it.
So we're left with Local Storage and Session Cookies.
Before SPA and client side rendering, we used to only have server side rendering and cookies. This is when HTTPOnly was invented to make it harder to steal session IDs and users' identities. When client side rendering was invented, stateless APIs were invented. These APIs do not use session IDs but Tokens instead, such as JWT. It means that the server do not save any information about the user in a session that is identified by a session ID. Instead, JWT tokens contains the signed information within the token itself, and the server do not remember anything and reauthenticate the user in each request. There is also a hybrid approach of tokens that are saved in NoSQL DBs in the server side such as Redis, to make the authentication process faster.
But the bottom line is that HTTPOnly cookies are used with stateful APIs that use sessions, and LocalStorage are used with stateless APIs that use tokens. You can also use it the other way around (cookies with tokens, LocalStorage with sessions) but its "less natural".
As for XSS, HttpOnly mechanism in cookies makes attackers' life a bit harder, but even if it is used, attackers can still do a lot of damage with phishing for example. So its not critical as this is just a compensating control and not a main mitigation in any way, so you can safely use LocalStorage. Furthermore, cookies are prone to CSRF attacks where LocalStorage does not.
So both options are valid.
You can read about it some more in here:
What is the difference between localStorage, sessionStorage, session and cookies?