CSRF is not as big of an issue as it used to be, and when it is an issue it can be solved more easily and comprehensively than XSS:
1. The default value for SameSite attribute is now "Lax" in most browsers. This means that unless you explicitly set your authentication cookies to SameSite=None (and why would you?), you are generally not vulnerable to cookie-based CSRF (other forms of CSRF are still possible, but not relevant to the issue of storing tokens in local storage or cookies).
2. Most modern SSR and hybrid frameworks have built-in CSRF protection for forms and you have to explicitly disable that protection in order to be vulnerable to CSRF.
3. APIs which support cookie authentication for SPAs can be deployed on another domain and use CORS headers to prevent CSRF, even with SameSite=None cookies.
On the other hand, there are no mechanisms which offer comprehensive protection from XSS. It's enough for a single JavaScript dependency that you use to have a bug and it's game over.
For this reason, OAuth 2.0 for Browser-Based Applications (draft)[1] strongly recommends using a HttpOnly cookie to store the access token:
"This architecture (using a BFF with HttpOnly cookies) is strongly recommended for business applications, sensitive applications, and applications that handle personal data."
With regards to storing access tokens and refresh tokens on local storage without any protection it says:
"To summarize, the architecture of a browser-based OAuth client application is straightforward, but results in a significant increase in the attack surface of the application. The attacker is not only able to hijack the client, but also to extract a full-featured set of tokens from the browser-based application.This architecture is not recommended for business applications, sensitive applications, and applications that handle personal data."
And this is what it has to say about storing the refresh token in a cookie, while keeping the access token accessible to JavaScript:
"When considering a token-mediating backend architecture (= storing only access token in local storage), it is strongly recommended to evaluate if adopting a full BFF (storing all tokens in a cookie) as discussed in Section 6.1 is a viable alternative. Only when the use cases or system requirements would prevent the use of a proxying BFF should the token-mediating backend be considered over a full BFF."
In short, the official OAuth WG stance is very clear:
1. HttpOnly cookies ARE better in terms of security.
2. Storing Refresh Tokens in local storage is only recommended for low-security use cases (no personal data, no enterprise compliance requirements).
3. Storing short-lived Access Tokens in local storage should only be considered if there are technical complexities that prevent you from using only cookies.
1. The default value for SameSite attribute is now "Lax" in most browsers. This means that unless you explicitly set your authentication cookies to SameSite=None (and why would you?), you are generally not vulnerable to cookie-based CSRF (other forms of CSRF are still possible, but not relevant to the issue of storing tokens in local storage or cookies).
2. Most modern SSR and hybrid frameworks have built-in CSRF protection for forms and you have to explicitly disable that protection in order to be vulnerable to CSRF.
3. APIs which support cookie authentication for SPAs can be deployed on another domain and use CORS headers to prevent CSRF, even with SameSite=None cookies.
On the other hand, there are no mechanisms which offer comprehensive protection from XSS. It's enough for a single JavaScript dependency that you use to have a bug and it's game over.
For this reason, OAuth 2.0 for Browser-Based Applications (draft)[1] strongly recommends using a HttpOnly cookie to store the access token:
"This architecture (using a BFF with HttpOnly cookies) is strongly recommended for business applications, sensitive applications, and applications that handle personal data."
With regards to storing access tokens and refresh tokens on local storage without any protection it says:
"To summarize, the architecture of a browser-based OAuth client application is straightforward, but results in a significant increase in the attack surface of the application. The attacker is not only able to hijack the client, but also to extract a full-featured set of tokens from the browser-based application.This architecture is not recommended for business applications, sensitive applications, and applications that handle personal data."
And this is what it has to say about storing the refresh token in a cookie, while keeping the access token accessible to JavaScript:
"When considering a token-mediating backend architecture (= storing only access token in local storage), it is strongly recommended to evaluate if adopting a full BFF (storing all tokens in a cookie) as discussed in Section 6.1 is a viable alternative. Only when the use cases or system requirements would prevent the use of a proxying BFF should the token-mediating backend be considered over a full BFF."
In short, the official OAuth WG stance is very clear:
1. HttpOnly cookies ARE better in terms of security. 2. Storing Refresh Tokens in local storage is only recommended for low-security use cases (no personal data, no enterprise compliance requirements). 3. Storing short-lived Access Tokens in local storage should only be considered if there are technical complexities that prevent you from using only cookies.
[1] https://datatracker.ietf.org/doc/html/draft-ietf-oauth-brows...