How to Use JWTs for Authorization: Best Practices and Common Mistakes

- Share:





2938 Members
JSON Web Tokens—better known as JWTs—are one of the backbones of application security. They’re compact, self-contained, and incredibly powerful when used correctly.
Most authentication solutions we’ve been using over the past years rely on them directly - they’re the reason you can log into an app and start making authenticated requests to protected routes.
But as popular as JWTs are, they’re also one of the most misunderstood pieces of application security, especially when it comes to authorization.
I’ve seen this confusion over and over again, working with countless teams: you start using JWTs for login, and before you know it, you’re stuffing your entire permission model into a token. Roles, URLs, folder IDs—you name it. It seems like a great shortcut, and then everything breaks.
In this article, I’m going to walk through what JWT authorization really means, how to use JWTs properly as part of an access control flow, and where the line should be drawn between identity and permissions.
Let’s get into it.
At its core, the JWT is a compact, URL-safe token that carries information between two parties. It’s made up of three parts: a header, a payload, and a signature.
JWTs are most commonly used for authentication. A user logs in, the server generates a token with their identity inside, and the client stores it—usually in local storage or a cookie. From that point on, every request includes the token, typically in the Authorization header as a Bearer token, like this:
Authorization: Bearer <your-jwt-token>
But here’s where things start to blur. Because JWTs carry claims, and those claims can include things like roles or scopes, many developers assume that JWTs can—or should—handle authorization directly. It feels simple: just check the user's role inside the token and decide what they can access, right?
JWTs do play a role in authorization, but it’s a very specific role. They’re there to communicate who the user is and what they’re allowed to claim about themselves. That identity can then be passed into your authorization logic or system, where the real decision-making happens.
When we talk about "JWT authorization," we usually refer to using identity information inside a JWT to support access control decisions. But that doesn’t mean the JWT is doing the authorization; it helps bootstrap it.
JWTs are best at communicating identity, not permissions. That distinction is everything.
Think of the JWT as your way of saying: “Here’s who the user is, and here are some basic facts about them that don’t change too often.”
That might include things like their user ID, email, maybe their organization, or even a high-level role like admin or editor.
All of this is identity-bound—it doesn’t shift from request to request, and it’s safe to sign and send as part of a token.
And once you’ve got that token in your application, that’s your starting point. You now know who the user is, and you can start making access decisions by plugging that identity into your actual authorization layer.
Let’s say your app has folders, and each folder has editors, viewers, and owners. You don’t want to list every folder and its permissions inside the JWT. That’s dynamic, contextual data that changes all the time.
What you can do is include that the user has the role VP of Marketing, and then your policy engine can say: “Cool, VPs of Marketing get editor access to these types of folders.”
The magic happens after the token is verified, not inside it.
This is how you decouple authentication and authorization in a clean, maintainable way. JWTs verify identity. Your authorization logic—whether it’s in code or powered by a policy engine—decides what that identity is allowed to do.
The most common use of JWTs in authorization is OAuth 2.0. When the JWT contains all the information necessary to implement OAuth 2.0, then you're cooking with coarse-grained authorization. Of course, you can also ship your own JWT-based authorization implementation, but why do you do this when OAuth 2.0 exists? The thing is, this coarse-grained solution doesn't support anything close to fine-grained authorization requirements. We’ll talk about why these are important next.
So yes, JWTs can support authorization, but only when you treat them as identity carriers, not as the access control system itself.
Now, let’s talk about why relying solely on JWTs for authorization is going to be a problem (and probably sooner than you think).
JWTs are static. Once they’re issued, they’re basically frozen in time. Whatever roles, claims, or metadata you stuffed into the payload—that’s what you’re stuck with until the token expires. If anything changes after that—like the user’s role being downgraded, their team being restructured, or their account being suspended—your app won’t know about it.
Since JWTs are stateless, you can't just “revoke” them unless you're tracking them externally, which kind of defeats the purpose. You’re left with stale tokens that still grant access to things the user maybe shouldn’t see anymore.
JWTs are coarse-grained. You might include something like "role": "admin" or "scopes": ["read", "write"], but that doesn’t help when you need to know if a user can access a specific resource—say, one document out of a thousand. That level of granularity just doesn’t fit inside a token.
I’ve seen teams try to work around this by cramming more and more data into the JWT: permissions per folder, lists of accessible resource IDs, even full URL-to-permission mappings. Every time, it ends in one of two ways: giant tokens that hit size limits, or completely unmanageable code that breaks the moment something changes.
JWTs just weren’t built to handle real-time, context-aware, or fine-grained authorization. They don’t know about time-sensitive approvals, external conditions, or user activity. They don’t scale with your access logic. And they don’t update when your data changes.
They’re great for saying who someone is. But not for deciding what they can do, at least not on their own.
Not everything belongs in a JWT. Just because you can put something in the payload doesn’t mean you should.
Here’s a quick rule of thumb: if the data changes frequently, depends on external systems, or isn’t part of the user’s core identity, it doesn’t belong in the token.
Don’t put:
The whole point of using JWTs is to keep things clean, fast, and secure. So use them for what they’re good at: stable, identity-bound claims. The stuff that doesn’t change often and that you can safely trust for a few minutes.
Everything else belongs in your authorization layer, not your token.
Having covered the main issues, here are some of the main mistakes developers make when using JWTs for authorization, and how to avoid them.
So if JWTs aren’t meant to do all your authorization work, what’s the right way to use them? Simple: use the token to identify the user, and leave the access decisions to your authorization layer.
The JWT tells you who the user is. That’s its job. From there, your app (or better yet, your policy engine) can figure out what that user is allowed to do—based on real-time data, current system state, relationships, and dynamic rules.
This is where policy engines like OPA, Cedar, or external authorization platforms like Permit.io come in. These tools take the identity claims from your JWT and use them to evaluate access logic separately, without bloating the token or hardcoding permission checks into your app.
Let’s take an example:
Your JWT says the user is user_id: 123, and maybe it includes a claim like role: marketing_vp. Your authorization system can then say:
“Okay, marketing VPs get editor access to department folders—but only if they’re active and haven’t hit their quota.”
You can pull in contextual data, time-based rules, external APIs—whatever your policy requires, and none of that needs to live in the token. You get dynamic decisions, fast evaluations, and clean separation between identity and policy. This also means you can:
Combining JWTs with a real authorization system is the best of both worlds - The identity stays portable, authorization is flexible, and your app stays maintainable.
We covered most of these points already, but let’s just do one last quick rundown on how to get things right:
user_id, email, org_id, or maybe a high-level role if it’s fairly stable. If the data changes often or depends on application state, leave it out.JWTs can absolutely support authorization workflows—but only if you use them wisely. Keep them focused on identity and delegate access decisions to the layer built for them.
JWTs are a powerful tool—but only when used in the right way. They’re great for communicating who the user is. They’re fast, portable, and verifiable without needing a centralized session store. That’s why they’ve become the default method for authentication in most apps.
But here’s the key takeaway: JWTs are not your authorization system.
They’re the bridge between your authentication provider and your app. They carry identity—not permissions. Trying to use them as your full access control layer is going to lead to brittle code, stale access, bloated tokens, and ultimately, security risks.
If you need dynamic, fine-grained, or policy-based access control, that’s a job for a real authorization layer—one that can evolve with your app and reflect live system state.
So keep your JWTs clean and focused on identity. Let your authorization logic do the rest.
Want to learn more about Authorization? Join our Slack community, where there are hundreds of devs building and implementing authorization.

Application authorization enthusiast with years of experience as a customer engineer, technical writing, and open-source community advocacy. Comunity Manager, Dev. Convention Extrovert and Meme Enthusiast.

Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker