Disclaimer
Wait, wait, wait — don’t go away. Let me explain myself. JWT itself is a good mechanism, but the problem lies in the authorization flow that uses JWT.
I’m talking about clients (React, Angular, Flutter) that send a login and password to the backend and receive a JWT in response.
Today, we’ll discuss why this approach is insecure, not recommended, and what to use instead.
The problematic flow
Before proceeding we should somehow name or classify this flow. Since OAuth and OpenIdConnect protocols are the leading protocols for authorization - we are going to use them.
I have already explained all the internals of the OAuth protocol, how it works and why in one of my articles, it would be great to read it as a prerequisite.
In general OAuth has 4 different “approaches” or “implementations” for authorization called grants. They have different purposes, and different security drawbacks.
For Resource Owner authorization (the end user) we have only 3 grants: auth code, implicit, password credentials.
The “login, password” -> JWT token flow that people are implementing everywhere is actually called Resource Owner Password Credentials grant
or shortly Password Credentials grant
.
If you are interested, how this flow is implemented under hood - please have a look at this article, where I made this auth with .net backend and react as a client
Why Custom Password Credentials is bad
Now that we’ve classified the flow and know that it’s actually the Password Credentials OAuth Grant, let’s look at what’s bad about it.
It works exactly the same as people usually implement it. The Client (browser or mobile) sends login + password over the network to the authorization server. Server checks that user with that credential exists and issues access token + refresh token (possibly).
The difference is that it follows the protocol, so it adds parameters like grant_type
and scope
. Besides that OAuth MAY have client authentication on top of resource owner (user) authentication.
Why is it bad?
User’s credentials exposure
In official RFC it’s stated that the main problem is the exposure of Resource Owner’s (user’s) credentials to the client, where they can be read and used in any way. Now, the security of your credentials is tied to two systems instead of one: the client and the server.
Since OAuth initially was designed to handle “third party apps” access, it is not usually suitable for general use, when both client and server are your first party apps, basically.
Password Brute Force
Since the client fires the request (XHR), it’s easy to perform a brute-force attack. To prevent this, you need rate limiting and similar protections.
With the Authorization Code flow, for example — where the login UI is hosted on the Authorization Server — you can simply add anti-forgery tokens and that’s it, because it’s done via a <form> submission instead of an XHR request.
No External Identity Providers (Google, Github, Microsoft)
I’ve been in situations like this many times. First, the product team says, “Let’s implement email and password.” So you do it, everything works — great success! But suddenly, they want to add an external identity provider, like Google or GitHub authentication.
Now real fun starts.
The only good way to do it is on the server side. It can be done on the client as well, but then it will be hard, or sometimes impossible, to validate a third party identity token from the backend.
Now, to do it on server side - you need to implement Authorization Code grant from your Authorization Server (your backend) to Identity provider (Google). With a very tricky implementation to not lose the request:
This flow works, as it is in prod right now, but it is very insecure and looks like a big “hack” or “workaround”. It is so complicated, because the client has a different “origin” or domain where it is hosted.
Single Sign On is not possible
Nativelly, OAuth or OIDC implementations are SSO by default, cause you can connect clients in 1 hour, and you have auth with all needed providers, and features like MFA ready in production.
For the custom Password Grant type - you need to implement it for every new client (login page, token logic, etc).
Does not support Multi Factor Auth (MFA)
Here, I need to point out that this flow does not support MFA out of the box with Authorization Servers like Microsoft Entra, Google Auth, etc. However, you can implement it yourself.
The problem is that it’s not standardized, which introduces multiple security issues — you’d essentially be reinventing the wheel. Also, it involves communication between the client and the Authorization Server, whereas in a proper OAuth implementation, MFA is handled entirely on the Authorization Server’s side.
From oauth.net:
And from MSDN:
Conclusion
As you can see, the Custom Password Credentials grant type has many drawbacks, starting from security and ending with scalability.
However, it does not mean you should go and implement OAuth protocol as it will take you some time, and it is more complex. If you plan to use multiple clients (browser, mobile), and identity providers (Google, Facebook, Github, Microsoft) - then it might be a very good idea. Nowadays there are a bunch of SaaS products for that (Clerk, Duende, OpenIddict, Auth0), that let you configure OpenId Connect in a matter of hours + ready made OAuth Client libraries.