I've been trying to implement a reliable authentication flow for a Next.js project but I'm completely lost now. I've already seen the examples repo of Next.js. But I have a lot of questions for a complete solution.
I have a express.js API and a separate Next.js frontend project. All the data and the authentication is handled by the API. Frontend just renders the pages with SSR. If I would just create a monolith project, where rendering the pages and all the data is handled by a single server (with a custom server option for Next.js I mean), I would just use express-session and csurf. It would be a traditional way to manage sessions and create security against CSRF.
Express.js API is not a requirement. It is just an example. It could be a Django API, or a .Net Core API. The main point is, it is a separate server and a separate project.
How can I have a simple, yet reliable structure? I've examined some of my favorite websites (netlify, zeit.co, heroku, spectrum.chat etc). Some of them use localstorage to store access and refresh tokens (XSS vulnerable). Some of them use cookies and they are not even HTTPOnly (both XSS and CSRF vulnerable). And examples like spectrum.chat use the way I mentioned above (cookie-session + preventing csrf).
I know there is the giant hype around the JWT tokens. But I find them too complex. Most of the tutorials just skips all the expiration, token refreshing, token revocation, blacklisting, whitelisting etc.
And many of the session cookie examples for Next.js almost never mention CSRF. Honestly, authentication is always a big problem for me. One day I read that HTTPOnly cookies should be used, next day I see a giant popular site not even using them. Or they say "never store your tokens to localStorage", and boom some giant project just uses this method.
Can anyone show me some direction for this situation?
Disclaimer: I am a maintainer of the free open source package below, but I think it's appropriate here as it's a common question there isn't a great answer for, as many of the popular solutions have the specific security flaws raised in the question (such as not using CSRF where appropriate and exposing Session Tokens or web tokens to client side JavaScript).
The package NextAuth.js attempts to address the issues raised above, with free open source software.
It uses httpOnly cookies with secure.
It has CSRF protection (double submit cookie method, with signed cookies).
Cookies are prefixed as appropriate (e.g. __HOST- or __Secure).
It supports email/passwordless signin and OAuth providers (with many included).
It supports both JSON Web Tokens (signed + encrypted) and Session Databases.
You can use it without a database (e.g. any ANSI SQL, MongoDB).
Has a live demo (view source).
It is 100% FOSS, it is not commercial software or a SaaS solution (is not selling anything).
Example API Route
e.g. page/api/auth/[...nextauth.js]
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
providers: [
// OAuth authentication providers
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET
}),
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
}),
// Sign in with email (passwordless)
Providers.Email({
server: process.env.MAIL_SERVER,
from: '<no-reply#example.com>'
}),
],
// MySQL, Postgres or MongoDB database (or leave empty)
database: process.env.DATABASE_URL
}
export default (req, res) => NextAuth(req, res, options)
Example React Component
e.g. pages/index.js
import React from 'react'
import {
useSession,
signin,
signout
} from 'next-auth/client'
export default () => {
const [ session, loading ] = useSession()
return <p>
{!session && <>
Not signed in <br/>
<button onClick={signin}>Sign in</button>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={signout}>Sign out</button>
</>}
</p>
}
Even if you don't choose to use it, you may find the code useful as a reference (e.g. how JSON Web Tokens are handled and how they are rotated in sessions.
I've had to think about this as well for my current project. I use the same technologies: an ExpressJS API and a NextJS server-side-rendered front-end.
What I chose to do is use passport.js in the ExpressJS API. TheNetNinja on YouTube has a really good playlist of this with 21 episodes. He shows you how to implement Google OAuth 2.0 in your API, but this logic transfers to any other strategy (JWT, Email + Password, Facebook authentication etc.).
In the front-end, I would literally redirect the user to a url in the Express API. This url would show the user the Google OAuth screen, the user clicks on "Allow", the API does some more stuff, makes a cookie for the specific user and then redirects back to a url in the front end. Now, the user is authenticated.
About HTTPOnly cookies: I chose to turn off this feature, because I was storing information in the cookie that I needed in the front-end. If you have this feature enabled, then the front-end (javascript) doesn't have access to those cookies, because they are HTTPOnly.
Here's the link to the playlist I was talking about:
https://www.youtube.com/watch?v=sakQbeRjgwg&list=PL4cUxeGkcC9jdm7QX143aMLAqyM-jTZ2x
Hope I've given you a direction you can take.
EDIT:
I haven't answered your question about CSURF, but that's because I'm not familiar with it.
I've finally found a solution!
Now I'm using csrf npm package, not csurf. csurf is just turns csrf into an express middleware.
So, I create a csrfSecret in the getInitialProps of _app. It creates the secret, sets it as a httpOnly cookie. Later, it creates a csrfToken and returns it with pageProps. So, I can access it with window.NEXT_DATA.props.csrfToken. If user refreshes the page, csrfSecret remains the same, but csrfToken gets renewed.
When I make a request to the proxied "/api/graphql API route, it first gets the csrf token from x-xsrf-token header and verifies it with the csrfSecret cookie value.
After that, it extracts the value of authToken cookie and passes it to the actual GraphQL API.
API is all token based. It only needs a non-expiring access token. (BTW, It doesn't need to be JWT. Any cryptographically strong, random token can be used. Which means a reference/opaque token.)
CSRF check is not needed for the actual API, because it doesn't rely on cookies for authentication. It only checks authorization header.
Both authToken and csrfSecret is httpOnly cookies. And I never even store them in client-side memory.
I think this is as secure as I could get. Now I'm happy with this solution.
Related
I'm working on a project where I've got a central API server and then multiple microservices for it including a website. The website uses OpenID to handle authentication. To allow for server-side rendering on the website yet have it remain stateless, I'm storing the access token in a cookie which is being used on the server each time the user requests a page via retrieving the access token from the cookie and appending it as an authorization header. Is there an exploits that could happen from this? As far as I'm aware I shouldn't have any problems with CSRF or any other exploit like it, however I haven't seen this way of handling authentication before.
Short answer: Yes
Long answer
The definition of CSRF is that the authentication cookie is automatically attached when any request from anywhere to your website is made. You will always need to implement xsrf counter measures + frontend.
Implementation
On each webrequest the webbrowser makes to the server, the server attaches a non-httponly cookie to the response, containing a CSRF-token which identifies the user currently signed on (NuGet).
public async Task Invoke(HttpContext httpContext)
{
httpContext.Response.OnStarting((state) =>
{
var context = (HttpContext)state;
//if (string.Equals(httpContext.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase))
//{
var tokens = antiforgery.GetAndStoreTokens(httpContext);
httpContext.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { Path = "/", HttpOnly = false });
//}
return Task.CompletedTask;
}, httpContext);
await next(httpContext);
}
Your frontend must be configured to read this cookie (this is why it's a non-httponly cookie) and pass the csrf-token in the X-XSRF-TOKEN header on each request:
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN'
}),
Then you need to add and configure the Antiforgery services to the ASP.NET Core application:
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
Now you can decorate your controller methods with the ValidateAntiforgeryAttribute.
I'm using angular, and angular does not send a X-XSRF-TOKEN header when the URL starts with https:. This could perhaps also be the case for React, if they provide an embedded solution.
Now if you combine this with the cookie authentication provided by ASP.NET Core Identity (SignInManager.SignInAsync), you should be clear to go.
Appendix
Note that all of the above is useless if you have an XSS vulnerability somewhere in your website. If you're not sanitizing (htmlspecialchars) your user-input before rendering it in HTML, an attacker can manage to inject a script into your HTML:
<div class="recipe">
<div class="title">{!! Model.UnsanitizedTitleFromUser !!}</div>
<div class="instructions">{!! Model.UnsanitizedInstructionsFromUser !!}</div>
</div>
The result could possibly be the following:
<div class="recipe">
<div class="title">Pancakes</div>
<div class="instructions">
<script>
// Read the value of the specific cookie
const csrfToken = document.cookie.split(' ').map(function(item) { return item.trim(';'); }).filter(function (item) { return item.startsWith('XSRF-TOKEN'); })[0].split('=')[1];
$.delete('/posts/25', { headers: { 'X-XSRF-TOKEN': csrfToken } });
</script>
</div>
</div>
The injected script runs in the website context, so is able to access the csrf-cookie. The authentication cookie is attached to any webrequest to your website. Result: the webrequest will not be blocked.
Important links
ASP.NET Core docs
For react I cannot find documentation on CSRF, but the idea is explained in the answer
More info
A hacker could try and send you an email with a link to a Facebook URL. You click this link, the webbrowser opens up, the authentication cookie for facebook.com is automatically attached. If this GET-request consequently deletes posts from your timeline, then the hacker made you do something without you realizing.
Rule of thumb: Never change state (database, login, session, ...) on a GET-request.
A second way a hacker could try and trick you is by hosting a website with the following html:
<form action="https://facebook.com/posts" method="POST">
<input type="hidden" name="title" value="This account was hacked">
<input type="hidden" name="content" value="Hi, I'm a hacker">
<input type="submit" value="Click here and earn 5000 dollars">
</form>
You only see some button on a random website with an appealing message, you decide to click it, but instead of receiving 5000 dollars, you're actually placing some posts on your facebook timeline.
As you can see, this is totally unrelated with whether you're hosting a single-page or MVC application.
Defense
MVC applications
In MVC websites, the usual practise is to add an input containing a CSRF token. When visiting the page, ASP.NET Core generates a CSRF token which represents your session (so if you're signed in, that's you). When submitting the form, the CSRF token in the POST body must contain the same identity as the one in the Cookie.
A hacker cannot generate this token from his website, his server since he isn't signed in with your identity.
(However, I think that a hacker would be perfectly capable of sending an AJAX GET request from his website with you visiting, then try to extract the token returned from your website and append it to the form). This could then again be prevented by excluding the GET-requests which return a form containing a CSRF-token from CORS (so basically don't have a Access-Control-Allow-Origin: * on any url returning some CSRF-token))
Single-page applications
This is explained on top. In each webrequest made to the server, the server attaches a non-httponly cookie to the response containing the CSRF-token for the current user session.
The SPA is configured to read this XSRF-TOKEN cookie and send the token as X-XSRF-TOKEN header. AFAIK, the cookie can only be read by scripts from the same website. So other websites cannot host a form containing this token field for someone's identity.
Although the XSRF-TOKEN cookie is also sent along to the server, the server doesn't process it. The cookie value is not being read by ASP.NET Core for anything. So when the header containing a correct token is present on the request, the backend can be sure that the webrequest was sent by your react (or in my case angular) app.
Spoiler
In ASP.NET Core, during a webrequest, the Identity does not change. So when you call your Login endpoint, the middleware provided in this answer will return a csrf token for the not-signed-in user. The same counts for when you logout. This response will contain a cookie with a csrf-token as if you're still signed in. You can solve this by creating an endpoint that does absolutely nothing, and call it each time after a sign in/out is performed. Explained here
Edit
I did a little test, and this image basically summarises everything from the test:
From the image you can read the following:
When visiting the index page from app4, a cookie is returned (non HttpOnly, SameSite.Strict)
app5 hosts a javascript file which can do anything this website owner wants
app4 references this script hosted by app5
The script is able to access the non HttpOnly cookie and do whatever it wants (send an ajax call to its server, or something rogue like that)
So storing the token in a non-httponly cookie is only fine if the scripts you include (jquery, angularjs, reactjs, vue, knockout, youtube iframe api, ...) will not read this cookie (but they can, even when the script is included with the <script> tag) AND you are certain that your website is fully protected against XSS. If an attacker would be somehow able to inject a script (which he hosts himself) in your website, he's able to read all non-httponly cookies of the visitors.
We are in the process of building customer app for an organisation , with technical stack
react js for portal and react-native for the mobile.
During the review, it was suggested to go for session id based user session instead of JWT Token based.
We also want to use OIDC FLow for Authentication and OAUTH2.0 for Authorization of API's
As there are suggestion for both JWT and Session ID which each of them has their own pros and cons.
Security team is insisting on the Session ID's with reasons below
JWT is Bulky , not revokable
JWTs are dangerous JWT vsSession
What should be our choice for security architecture
Do we need still Session Id to track
The Token used for the OIDC cannot be sent to UI,as it has privacy and security information
should we have to go for JWE?
Can we have hash of the token or correlation id corresponding to the OIDC TOkens and lookup on each call from client to backend , so that browser or client app need not know OIDC TOkens?
Do we really need bespoke auth service, Can API Gw itself along with any OIDC will be enough to implement end2end security without any need for writing spring base or any Authentication service
[![FLow][4]][4]
CONFIDENTIALITY
You can satisfy these concerns in a standard way via a couple of techniques, though they require a more complex flow:
Use opaque tokens that are not readable by internet clients and do not reveal any sensitive information - see The Phantom Token Pattern for how this works.
In the browser use only secure cookies (SameSite=strict, HTTP Only, AES256 encrypted), which can contain opaque tokens. See the Token Handler Pattern where there is a React SPA you can run and a Node token handler API you can plug in.
Generally these internet credentials behave like session IDs, which are also opaque, but you are using standard OAuth and your APIs still end up authorizing via digitally verifiable JWT access tokens.
TOKEN HANDLER PATTERN
For SPAs the idea is to enable a setup like this, where you plug in (and perhaps adapt) open source token handler components, rather than needing to develop them yourself. This pattern should work with any Authorization Server:
Key benefits are as follows - see these Curity resources for further details:
SPA uses only the strongest SameSite=strict cookies, with no tokens in the browser
SPA can be deployed to many global locations via a Content Delivery Network
By default, cookies are only used on Ajax requests to APIs, which gives the SPA best control over usability aspects
API FLOWS
When calling APIs the flows then work like this, and typically involve a reverse proxy placed in front of APIs, so that API code remains simple:
Web clients send a secure cookie, and a cookie decryption gets the opaque token. A second plugin then gets a JWT from the opaque token and forwards it to APIs.
Mobile clients send an opaque token to the same API paths, in which case the cookie decryption plugin does nothing. The second plugin then gets a JWT from the opaque token and forwards it to APIs.
Note that the client can still receive an expires_in field if it wants to perform optimizations to check the lifetime of access tokens, but I have always advised against this, and to instead just handle 401s reliably in clients, like this:
private async fetch(method: string, path: string): Promise<any> {
try {
// Try the API call
return await this.fetchImpl(method, path);
} catch (e) {
if (!this.isApi401Error(e)) {
throw ErrorHandler.handleFetchError(e);
}
await this.oauthClient.refresh();
try {
// Retry the API call
return await this.fetchImpl(method, path);
} catch (e) {
throw ErrorHandler.handleFetchError(e);
}
}
}
I have a web application, where the frontend is built with react.js and typescript and the backend is build with ASP.NET Core 3.1 and connected to a SQL Server. The SQL Server is used for saving data, which I can enter in the frontend.
Now I want to implement a custom authentication system, where I can protect different endpoints with an authorization. I know there are several ways to go for, but I don't want to use a service / library, where a user could login with his google account for example.
Jwt seems in my Opinion a good way to go here, but I really don't understand the whole system.
This article helped me already a lot: Implementing authentication and authorization with React hooks, .NET Core Web API, and SQL Server .After reading this, I don't understand the relationship between logging in and how my backend knows, that a user is logged in (for protecting my endpoints).
Of course I already read many articles about authentication and authorization for ASP.NET Core. I've read about different services like:
Auth0 (seems not a good idea, because you can login with google etc.)
IdentityUI (seems good, I've seen a few youtube tutorials but they are all using another project structure, where the frontend and backend isn't separated. So they are using the razor pages like Login.cshtml, but I don't want to render any pages in the backend, only in the frontend)
For authorization for my controller in the backend, I planned to use the following:
[Authorize] // to protect endpoint
[HttpGet]
public async Task<IEnumerable<>> GetData()
{
// some code
}
But as I already said: I don't know / understand how my backend knows if a user is logged in (and how to check it).
Could somebody provide me an appropriate tutorial or an article, where is explained, how to manage authorization and authentication for frontend and backend? Or maybe somebody knows, how to use the IdentityUI with a frontend build with react + typescript and a backend, which shouldn't render any pages.
Thanks for your attention :)
Well... for detailed flow how they work, here is RFC 6749, this is a pretty comprehensive collection of knowledge related to the topic and the easiest approach would be wiki page in general.
But to simplify the process, just get to know these things:
What is JWT
Jwt just a string that was generated by some algorithm from a public/private key pair(don't even care how it works, just give it a key pair, some lib on every language would do the rest). This JWT often contain all the information that needed to specify who the user is(big word right ? but actually, userId is enough for simple case).
It contain 3 different parts Header, Payload and Signature, and the string assured it cannot be interrupted(or just modify it as we wish, and the validation process would failed).
Further detail here.
What happen on server side?
The most basically flow was send user, password to server for validate who we are and our account exists. Then, server side would take some necessary info out of it to generate the JWT token. The server which generate JWT token was normally refer to Identity Provider.
Then that token was send back to client, and client save it somewhere for later use.
Client side use token to request other resource
Normally, Identity Provider would provide a .wellknow endpoint that contain all necessary info for other resources to gather for validation process.
Now, client side send a http request to these resources with the JWT token. They use info gathered from .wellknow endpoint to validate was the JWT valid. If it is, we are who we claim we are, authentication process success.
The end.
Over-simplyfied flow imagination in your specific case
React Client => Request login => Identity Provider => Jwt Token send back => React client save it somewhere.
React Client => Request some resource over http with the JWT token => Resource server validate it (through info from .wellknow endpoint) => Authentication success or fail => normal process => send response back to client.
I was trying set up google authentication with react frontend and django rest framework backend. I set up both the frontend and backend using this two part tutorial, PART1 & PART2. When I try to login with google in the frontend I get POST http://127.0.0.1:8000/google-login/ 400 (Bad Request) I think it's because my google api needs an access token and an authorization code to be passed. After debugging the react js, I noticed the response I get from google doesn't have an authorization code. I suspect because responseType is permission(by default), Source:React login props , instead of code. I was wondering how would you change the response type in react? (I'm not even sure if this alone is the issue)
Here's my backend code
In my views.py file
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
callback_url = "http://localhost:3000"
client_class = OAuth2Client
in my urls.py
path('google-login/', GoogleLogin.as_view(), name='google-login'),
for my front end
/Components/login.js
const googleLogin = async (accesstoken,code) => {
console.log(accesstoken)
let res = await cacaDB.post(
`google-login/`,
{
access_token: accesstoken,
code: code
}
);
console.log(res);
return await res.status;
};
const responseGoogle = (response) => {
console.log(response.code);
googleLogin(response.accessToken, response.code);
}
return(
<div className="App">
<h1>LOGIN WITH GOOGLE</h1>
<GoogleLogin
clientId="client_id"
buttonText="LOGIN WITH GOOGLE"
onSuccess={responseGoogle}
onFailure={responseGoogle}
/>
</div>
)
I want to save the user in the database and have them stay logged in, in the front end.
This Post explains the login flow behind the scene. Here's Login flow image I'm basically stuck on returning code and accesstoken(I can return this successfully) step.
Here's my list of questions,
How do I return code from google?
I have knox token set up, can I
use it instead of the JWT tokens?
Does the class GoogleLogin(SocialLoginView), take care of the steps of validating the access token and code with google and creating the user with that email in database?
Would really appreciate your inputs.
After investigating a bit on my end, I think I might have a solution that works for you.
I've messed with OAuth before, and it's quite tricky sometimes because it has to be robust. So a bunch of security policies usually get in the way.
I'll provide my full step-by-step, since I was able to get it working, trying my best to match what you posted.
Firstly, to have a clean slate, I went off the example code linked in the tutorials. I cloned and built the project, and did the following:
Creating a new project on GCP
Configured the OAuth consent screen
I set the User type to "internal". This options may not be available if you're not using an account under GSuite (which I am). "External" should be fine though, just that "internal" is the easiest to test.
Created a OAuth 2.0 Client
Added http://localhost:3000 to the "Authorized JavaScript origins" and "Authorized redirect URIs" sections
Register a Django superuser
Registered a Site, with value of localhost:8000 for both fields.
Went into the admin panel, and added a Social Application with Client ID and Secret Key as the "Client ID" and "Client Secret" from GCP, respectively. I also picked the localhost site that we added earlier and added it to the right hand box. (I left Key blank)
Example of my Application Page
Filled in the clientId field in App.js, in the params of the GoogleLogin component.
Here's where I ran into a bit of trouble, but this is good news as I was able to reproduce your error! Looking at the request in the network inspector, I see that for me, no body was passed, which is clearly the direct cause of the error. But looking at App#responseGoogle(response), it clearly should pass a token of some sort, because we see the line googleLogin(response.accessToken).
So what is happening is that accounts.google.com is NOT returning a proper response, so something is happening on their end, and we get an invalid response, but we fail silently because javascript is javascript.
After examining the response that Google gave back, I found this related SO post that allowed me to fix the issue, and interestingly, the solution to it was quite simple: Clear your cache. I'll be honest, I'm not exactly sure why this works, but I suspect it has something to do with the fact that development is on your local machine (localhost/127.0.0.1 difference, perhaps?).
You can also try to access your site via incognito mode, or another browser, which also worked for me.
I have knox token set up, can I use it instead of the JWT tokens?
I don't think I have enough knowledge to properly answer this, but my preliminary research suggests no. AFAIK, you should just store the token that Google gives you, as the token itself is what you'll use to authenticate. It seems that Knox replaces Django's TokenAuthentication, which means that Knox is in charge of generating the token. If you're offloading the login work to Google, I don't see how you could leverage something like Knox. However, I could be very wrong.
Does the class GoogleLogin(SocialLoginView), take care of the steps of validating the access token and code with google and creating the user with that email in database?
I believe so. After successfully authenticating with Google (and it calls the backend endpoint correctly), it seems to create a "Social Account" model. An example of what it created for me is below. It retrieved all this information (like my name) from Google.
Example of my "Social Accounts" page
As for how to retrieve the login from the browser's local storage, I have no idea. I see no evidence of a cookie, so it must be storing it somewhere else, or you might have to set that up yourself (with React Providers, Services, or even Redux.
I'm developing simple CRUD app in react and express. Now, When user SignIn, I store the userId(from database) and JWT(web token) in localstorage and I use this userId and token to check isSignin() and isAuthenticated() for future API calls. Now, If I deploy or make production build of this application, the user info will be clearly visible in my localstorage and it can be threat to my security. Can anyone please tell me how to hide this information and implement these mathods in production ready app? I want deploy it on AWS. Because, I've seen on so many website, we cannot see our own userId other credentials in our own localstorage.
here my methods. The req are coming from front end in react and as soon as I am getting response to front-end my react code is storing that in localstorage.
exports.isSignedIn = expressJwt({
secret: process.env.SECRET,
userProperty: "auth"
});
exports.isAuthenticated = (req, res, next) => {
let checker = req.profile && req.auth && req.profile._id == req.auth._id;
if (!checker) {
return res.status(403).json({
error: "ACCESS DENIED"
});
}
next();
};
Randall Degges gave a very detailed explanation about why you shouldn't use localstorage to store sensitive data. I will point out the main part.
If you need to store sensitive data, you should always use a
server-side session. Sensitive data includes:
User IDs, Session IDs, JWTs, Personal information, Credit card information,
API keys, And anything else you wouldn't want to publicly share on
Facebook If you need to store sensitive data, here's how to do it:
When a user logs into your website, create a session identifier for
them and store it in a cryptographically signed cookie. If you're
using a web framework, look up “how to create a user session using
cookies” and follow that guide.
Make sure that whatever cookie library your web framework uses is
setting the httpOnly cookie flag. This flag makes it impossible for a
browser to read any cookies, which is required in order to safely use
server-side sessions with cookies. Read Jeff Atwood's article for more
information. He's the man.
Make sure that your cookie library also sets the SameSite=strict
cookie flag (to prevent CSRF attacks), as well as the secure=true flag
(to ensure cookies can only be set over an encrypted connection).
Each time a user makes a request to your site, use their session ID
(extracted from the cookie they send to you) to retrieve their account
details from either a database or a cache (depending on how large your
website is)
Once you have the user's account info pulled up and verified, feel
free to pull any associated sensitive data along with it
This pattern is simple, straightforward, and most importantly: secure.
And yes, you can most definitely scale up a large website using this
pattern. Don't tell me that JWTs are “stateless” and “fast” and you
have to use local storage to store them: you're wrong!
Source/Read more: Please Stop Using Local Storage