JWT was meant to be stateless, if it's not, then it's just a layer of unnecessary complexity with potential security and implementation flaws.
It's possible to add additional information in a JWT. And of course it's complexity that adds additional attack surface, but at least there is some kind of standardization around it.
Basically, Clients all used NTLM to talk to the main site, but the main site would use JWT to pass the authenticated user info to the other services being called. The signature ensured that you couldn't spoof, short of being an authorized user that could get an impersonate token for calling the APIs.
But the nice thing was it meant we didn't really have to hit the DB at all in any of this, and it was way cheaper to implement than an API gateway.
If you're talking about something bespoke then it probably doesn't.
Isn't that JWT?
The web server gets a token from the API server, then prepares a few JSON messages that the web client will send asynchronously with JS. Since each message content is signed, the web client can't tamper with what is sent to the API. JWT was perfect for this 3-tiers messaging.
If you work mainly in Java for example, you'll more often see JSESSIONID which are random string identifiers, referring to a database containing active tokens and user profiles.
However if you work in Python, you'll more often see objects. Typically something like a user identifier + creation date + random bits, that is encrypted with a symmetric key. It's usually encrypted, not signed, so yet another thing than signed tokens and random cookies.
Basically a bunch of endpoints can be put up, and if they use JWTs, it is easy to hit those endpoints from any type of app.
Cookies can of course be used, but that requires pulling cookie jars into native code. Perfectly do-able, but also super awkward and potentially error prone. e.g. I remember using apps on Windows that required me to clear my Internet Explorer cookies if the native app's auth got into a broken state!
(Things aren't that bad anymore)
JWTs are also nice because I can easily write services that authenticate to each other. I can have a service running on my backend that authenticates its limited access service account, gets a JWT, and goes and talks to another service. Could I pass around cookies? Sure, but it'd be more work and more complicated than "attach this JSON blob".
Cookies are nice if everything is browser based, but I'd argue that isn't the best way to build services.
(And finally, the amount of time I've spent debugging JWT issues < the amount of time I've spent debugging cookie issues!)
At that point why not just use JWT?
If my auth service provider already uses JWT (which it does), and all the platforms I am writing on have a provided library that consumes JWT (which they do), then why would I go with a custom header?
Also having uniformity of code patterns is nice.
My web service uses the same authentication scheme as my native apps. Heck my backend DB knows how to look JWT tokens and apply permissions correctly.
Reference:
[1]: https://zaiste.net/creating-secure-rest-api-nodejs-without-j...
Our API knows that that organization's auth server is allowed to sign tokens, the third party frontends can obtain those tokens and send them to our API, and it works (or so I hope, I'm in the reading up on all this stuff phase). Sessions using regular cookies just don't.
It was easily implmeneted, easy to understand, secure by design and not open to any of those security issues because there was no magic lib which would have allowed for some downgrade attack.
And what did it actually solve? Session stickyness. Simple and easy.
But this video says all I have to say (2018):
https://www.youtube.com/watch?v=JdGOb7AxUo0
1 sec takeaway (More in the video):
https://i.imgur.com/vUYTYfS.png
That said, JWT's are great for stuff like 2-Factor via email link or redirecting from one domain to another. Single use, which it was built for.