Not satisfied? Fine, fine. I’ll write a longer answer.
Let’s talk about what we’re talking about. JWT stands for JSON Web Tokens, a reasonably well defined standard for authenticated tokens. Specifically they have a header with format information, a payload, and a signature or message authentication code. The core idea is that whoever has the corresponding verification key can verify that the payload is authentic and has not been altered. What they do with that information is up to them.
The JWT spec (RFC 7519) makes suggestions by providing a few well-known registered claim names: issuer, audience, subject, expiration time, etc. A common usage pattern is that, after verifying the authenticity against whatever trust relationship they have with the issuer, the recipient checks whether they are the intended audience (if any is specified) and the expiration time has not yet passed, and then take the subject as an authenticated identity of the bearer of the token.
It’s perfectly designed for bearer token authentication! Or is it? Let me be clear: JWT as authentication tokens are constructed for Google/Facebook scale environments, and absolutely no one who is not Google/Facebook needs to put up with the ensuing tradeoffs. If you process less than 10k requests per second, you’re not Google nor are you Facebook.
The core benefit, proponents will tell you, is that the recipient of a JWT doesn’t need to connect to the user database to verify the token authenticity and render its service. In a large installation, like Google’s, that means that the JWT issuer, the authentication service, can be a dedicated service that is managed and scaled like other services, and is the only service that needs to access the centralized user database. All other services can act on the information stored in the JWT alone, and don’t need to go through the user database, which would represent a choke point.
What about logout/session invalidation? Well, in order for this model to work, the authentication token should have a fairly short lifetime. Maybe 5 minutes, max. The client is also issued a second token, the so-called refresh token, with which it can request a new authentication token from the authentication service. This gives the authentication service a chance to consult the user database to see whether the user or a specific session has been blocked in the meantime.
Here’s the twist that is rarely, if ever, spelled out: In this setup the refresh token, not the authentication token, is the real session token. The refresh token represents the session with the authentication service (which can be revoked), while the authentication tokens are just derived credentials to be used for a few requests at most. The beauty, from Google’s point of view, is that this delegates keeping the session alive to the client, i.e. not Google’s servers. Oh and by the way, the refresh token can be, and usually is, opaque, since it’s only ever consumed by the same service that creates it. That reduces a lot of complexity, by just using an opaque identifier stored in a database.
Now, let’s assume you are not Google. Check which of these apply to you:
- You wanted to implement log-out, so now you’re keeping an allowlist of valid JWTs, or a denylist of revoked JWTs. To check this you hit the database on each request.
- You need to be able to block users entirely, so you check a “user active” flag in the database. You hit the database on each request.
- You need additional relationships between the user object and other objects in the database. You hit the database on each request.
- Your service does anything at all with data in the database. You hit the database on each request.
Congratulations, if you confirmed any of the items above, you don’t need JWTs. You’re hitting the database anyway, and I’m pretty sure that you only have one database which stores both your user profiles and your application data. By just using a “normal” opaque session token and storing it in the database, the same way Google does with the refresh token, and dropping all JWT authentication token nonsense, you stand to reap these great benefits:
- No weird workarounds (allow/denylist) for shortcomings of JWT as authentication token
- Greatly reduced complexity. No need to manage a secure JWT signing/authentication key
- You get to pass on some interesting bugs.
Just use the normal session mechanism that comes with your web framework and that you were using before someone told you that Google uses JWT. It has stood the test of time and is probably fine.
If you need something to do to make you feel like you’re running a big deployment, you can probably configure your session mechanism to use redisvalkey to store the session data. You’re still going to use the authenticated user id to query the database, but for unauthenticated requests it may be faster/use less resources. It might not be. You’ll have to tune and measure that.
I’m not disagreeing with what you wrote for using JWTs as a session token, but there are other use cases.
For example, if you have two applications where each has it’s own user and session management, but occasionally you want to send users from one to the other without them having to log in again. One (elegant, imho) solution is that application 1 issues a short lived token and redirects the user agent to application 2, which accepts it as proof that the user has been authenticated.
You’re right. What you’re describing is formalized in OpenID Connect (OIDC) and the JWT for passing user information from an Identity Provider to a Service Provider is then called the ID token, a third token type (besides access and refresh) in the underlying OAUTH2 protocol.
The oauth website has a couple of, erm, Opinions on the topic of whether you’re supposed/allowed to interpret the access token or derive user information from it: https://oauth.net/2/access-tokens/ 😉
As far as I see it you are mixing authentication, session management and user management.
For authentication purposes access/refresh token are quite nice since they are rather hard to forge.
For user management, well, you do not want to stuff all the required user claims into that poor token. If you try you will find that there is a limit that nobody specified but is there nonetheless.
For session management I agree with you. Token are no good session persistence measures, but they do work.
Anyway, there is a reason why developers offload that pesky auth/user/session stuff to an identity provider, then all those questions above are “someone else’s problem” (see Douglas Adam for further instructions).
I feel like this subject comes up every couple of months with another iteration of why people think JWTs are bad. Claiming JWTs are bad is like claiming that HTTP is bad, data packets are bad, or that currency is bad.
For sure some RFCs are bad, this is always true, there are things that are always improving. And it’s hard to not point out, that we’ve even built the successful Auth SaaS–Authress–which does provide JWTs, although we thought long about how session management work work.
I am not one for commenting on other people’s articles, but this one just does not make sense to me, and this is why I’ll leave my two cents here:
In your headline, you say that you *should* not use JWTs — at the end of the article, you say you do not *need* JWTs. Which one is it?
I think that no one ever argued that you absolutely *need* a JWT. The one and only real benefit of a JWT is standardization, and that is exactly why you really *should* use it.
Every token format that you implement on your own is — for a developer that is not yourself — always going to be inferior to the JWT if you do not document and specify it as well as the RFC does (Which is something most developers will not do). You *should* always strive to use standards the broader community agreed to, instead of reinventing the wheel because you think you know better than others.
Nowhere does the RFC for the JWT mention any type of “refresh token” or “session key” and I think you are wildly confusing JWT and OAuth 2.0 in this article and seem to think that these two things are interchangeable.
The JWT RFC only specifies the format of the Token itself, and even the RFC for OAuth 2.0 does not *mandate* the use of the JWT format either.
Which flow you eventually use a JWT token in is up to you. This is also why the JWT in and of itself will never have anything to do with the amount of database queries you will make, as these will always part of the authentication and authorization protocols and flows.
If you rewrote this article to “You most likely do not need OAuth 2.0 in your application” (the emphasis is on *need* instead of *should* because, again, you absolutely should always strive to use standards), then I would probably agree with you, though.
Most applications are designed to work around sessions. Authentication is done once per session and not on every requests. Doing OAuth2 once per session is sensible and after that, you can normally just ignore the JWT token or otherwise store it in the session table for on-demand use. This workflow is well established and common in pretty much all web application frameworks.
Using the JWT token directly as session identifer would be possible to deal with interfaces that can’t use e.g. cookies, but then you have to canonicalize the JSON to use it as identifier or do the more expensive verification dance. It’s almost always worse.
The article is more about Basic Auth vs. JWT, as I read it.
And I don’t fully get the problem of using one over the other, or what to recommend in a non Google environment, I have to say. One argument always goes, that you’re trying to avoid sending your password over the wire all the time with Basic Auth. With JWT this is gonna be less. But is that really important? I couldn’t say. I don’t know attack vectors that are working with Basic Auth, but not with JWT.
but JWT is not only that. Token claims can be used to integrate your app with other already written systems. It is client side session not just random number.
I like your thinking. Having been working with the oauth2/oidc/authentications projects with different SaaS products for the last 5+ years have made me question why the default solutions are often times quite complex.
My favourite example, and the one you already mentioned, is the logout in the SSO scenario. There are still no standard solution to get the logout working if you rely on JWT tokens that are validated on the Relying Party side. Sure there are ways to get it working in some degree, but there are always some gotchas that you need to be aware of. Like the one where you need to balance the token expiration time to make it valid long enough that you don’t worsen the UX of the application, but to make it expire as soon as possible.
This is a problem that most of these OpenID Authentication services are not telling you. They always sell the idea, that by using their service, you don’t have to implement these things by yourself. But the reality is, that a lot of work is put in the shoulders of the Relaying Parties. Like this logout case. If you would want to to immediately log the user out when the SSO session is killed, you basically have two options: either some sort of event hook in the RP side to get notified when this happens, or regularly check the validity of the token by calling the Identity Provider. Having a webhook for that can be complicated, and the latter just makes you to question why even bother with validating the tokens only in the RP side.
Sorry, I kind of side-stepped from you original point, but I feel like these paid services companies are using are many times making the problem (you wrote about) worse by not providing things that just work without added complexity.
You’re missing one of the biggest benefits of using JWT’s, interoperability. JWT’s are a known standard, and harder to screw up versus a home baked authN solution.
Secondly, very few companies should be creating home-baked auth solutions anyways. Unless that’s your core product, there’s no reason to build that kind of risk into your product when things like Auth0/Clerk/Ory exist.
I like this one:
> JWT’s are a known standard, and harder to screw up
And then I read even from bigger companies stuff like “alg: none” was accepted…
You’re missing the point.
There is dedicated software, so-called IDPs that manage users, some also do access management, with an UI.
The while Oauth2 or nowadays Openid-Connect thing uses JWT, so you can have a token to access multiple backends in a microservice environment.
In this case the user management (IDP) is a microservice, and your graphql or RESTful backend is a microservice.
The full stack SSR classic website is a confidential client in this case.
Yes, oidc means added complexity. You have to deal with X‑FRAME-OPTIONS and CSP, and CORS.
Advantage is a more secure solution that is more flexible.
What if you have a service at blog.ploetzli.ch and forum.ploetzli.ch, with oidc you can use a single IDP, aka a single JWT for both.
Each cookie is only valid in its (sub)domain and path.
No one is saying you have to use JWT as cookie values.
You don’t.
What is your solution when you have a RESTful backend and a mobile frontend, a Svelte SSR frontend and a React PWA frontend and a Qt or other desktop app?
Do you send each of them a cookie? That won’t work.
What do you send instead of a JWT? A JWT is a cryptographically signed JSON payload which has a header, body and signature.
Weakness: The usual suspects are only capable of RSA-SHA256 signing and verification, but that’s not future proof, actually not even now proof. But RS256 is the default for the most prominent IDPs, like keycloak or authentik and some proxies. Louketo for instance can’t handle HMAC-SHA512. Various middlewares also are incapable of doing more than RS256.
How do you pass information to the client when you have decoupled frontend and backend?
What is your alternative? Redis? Is the transport encrypted? The documentation says that there is no brute force protection. Also it’s an in memory database.
OIDC and JWT are standards, that wouldn’t be used if they weren’t working.
OIDC on the client (or frontend if you will) is more complicated than simple user/password, which btw is also possible with oidc’s password flow.
If feel like OIDC isn’t the is all end all, there will be another protocol that will succeed it.
When it works, it’s great, but when it doesn’t you’re in a world of pain.
Also @fefe, wenn man keine Ahnung hat, Fresse halten.
Yeah… no !
Lots of inaccuracies in this post. JWTs are great and should always be the preferred option when implementing user authentication and authorization. It is a proven technology with lots of ready-made libraries.
Sure, it was designed for scale, but it is also designed for ease of use, which is even more important when it comes to implementing security. It also, out of the box, supports multiple logged in clients for the same user. No need to keep a separate table of session tokes and have a 1‑M relationship from the user table.
If you want more security in your app you can exclude the refresh token, that is an optional part of JWTs (it’s not even mentioned in the RFC). Allow/Denylists is also optional (and not really needed unless you have a problem with tokens leaking from your clients, which would mean any token would be vulnerable).
This article read to me like the author isn’t comfortable using JWTs. I would suggest going through more tutorials on the subject.
I am usually not commenting on such articles. But the misconceptions here have to be pointed out:
1.) Argument 1: “If you have less than 10k requests per second, you are not big enough due to the ensuing tradeoffs”
Here, it is not only about performance, but also where a user directory lives. Especially in Enterprise Systems, that is different. A user management almost never lives where your application db lives.
2.) “The authentication service should be a dedicated service, and you should not have many separate issuers to have quality of service and compliance”.
That is usually a given in larger enterprises or services, that you either use AzureAD, Ping, Okta, Ory etc… or in large cases you use your main own KeyCloak.
A large security aspect here is, that neither your clients, web apps, APIs etc, will need to know your password. Which is huge from my point of view. If you have user databases that you are talking to, you are basically comparing password hashes, which means, your app will need to handle passwords…
3.) Logout / Session invalidation: How is a token related to a session or logout?
This is indeed a point. In a pure API world, that is not a real issue, as there are no sessions there. But in Web Environments, that is true. If you take the token as it is then the lifetime is basically the session time and in the worst case, you cannot invalidate it, if you don’t want to introspect the token.
I don’t see the risk that much, if you restrict the lifetime of the access token to like 15 min, or 30 min. Even having it for 1 h (which is the default use case).
And as pointed out by other commenters, you can do the initial OAuth2/OIDC authentication and then initiate your session with that and store the token, if need be. Then you will be logged out, if you logout the session.
The clients can also revoke a refresh token, which makes the refresh token not usable anymore. So the logout is possible, but it’s not an immediate one.
4.) Now the assumptions if we are not “Google”, which does not take certain architectures or providers into account and makes a lot of assumptions.
For instance in large enterprises, you only have “1” internal user database, and the creation, password management etc. is heavily restricted by compliance reasons and also defense mechanisms. There is a strong drive and need for a singular user management, authentication and authorization. You cannot just “hit the database” easily. But it has been done with LDAP in past in many enterprises.
Your main points:
— You wanted to implement log-out, so now you’re keeping an allowlist of valid JWTs, or a denylist of revoked JWTs. To check this, you hit the database on each request.
A: That is a bold statement, this can also be done via token introspection: https://www.rfc-editor.org/rfc/rfc7662. However, you could choose to live with the lifetime of the Access Token, even after session logout. Introspection can be centralized as well, even if your IdP does not support it. (EntraID for instance does not support it)
— You need to be able to block users entirely, so you check a “user active” flag in the database. You hit the database on each request.
A: Also here, usually the app owner does not manage users itself. That is basically HR, whether you even have a user to begin with. Other department, other processes and so on. Again, if you are not a valid user anymore you cannot redeem your refresh token aka. prolong your session. So the only time you have to “worry” is the lifetime of the access token, for systems using JWTs.
— You need additional relationships between the user object and other objects in the database. You hit the database on each request.
A: That is also an unreasonable conclusion, which depends on your design and used products. In a proper RBAC model, you don’t have a relationship of a user with an object. You have that with an action, tied to a role, to an object. And all users that are having this role, can do certain things. If you need a UUID of a user, you can use the SID for instance of a user, and you can use that. It is fix and will not change in the lifetime of a user. So you are not “hitting the DB” on every request.
— Your service does anything at all with data in the database. You hit the database on each request.
A: Which database? You should at least differ the “user db” and the “application db”. Of course, you often have a persistence layer in your app. But the user DB is rarely to be consulted. If designed properly, the JWT is, as explained in the standard a “self-contained” information. And let’s not forget caching etc…
5.) “user profiles and application data are all in the DB.”
Well, true, you have user profiles. But any respectable framework that supports OAuth/OIDC, does so efficiently, meaning you cannot set properties, coming from the Token, only additional information, that you might need. But your user password is basically empty.
6.) Just use “normal” session tokens and storing them in the database.
But what you are not telling, is how do you authenticate the user in the first place? How do you handle passwords? Is the cleartext password known to you? Another large point here is, that you cannot use delegation with “login via” in that argument. No Google, Facebook, Microsoft or your own OIDC login.
This is a fairytale argument. You are loosing a lot if you switch to “normal” session tokens without OAuth2/OIDC. Even if you win some benefits.