How to Use JWTs for Authorization: Best Practices and Common Mistakes
- Share:
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.
What Is JWT Authorization?
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.
- The header defines what kind of token it is and what algorithm was used to sign it.
- The payload contains the actual data—things like the user ID, email, roles, or any other identity-related claims.
- The signature makes sure that none of it was tampered with.
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.
The Right Way to Use JWTs for Authorization
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.
Why JWTs Alone Aren’t Enough for Authorization
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.
What Not to Put in a JWT
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:
- Lists of resource IDs that the user can access
- Folder names, document paths, or URL-to-permission mappings
- Billing status, subscription tier, or quota usage
- Real-time context like location, time of day, or session state
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.
TL;DR JWT Authorization - What NOT to do
Having covered the main issues, here are some of the main mistakes developers make when using JWTs for authorization, and how to avoid them.
- Don’t treat JWTs as a complete access control system
JWTs are not a one-stop-shop for access. Treating them as such breaks down when you need anything more than basic, hardcoded rules. JWTs don’t know your current app state, they don’t talk to your database, and they don’t understand relationships between users and resources. - Don’t overload the token with roles and permissions
We talked about this above, so no need to repeat ourselves. - Don’t use long-lived tokens without rotation
This one’s a big security red flag. If your tokens don’t expire quickly—and you’re not rotating or refreshing them—you’re creating a long window of vulnerability. If a token is compromised, an attacker can use it until it finally expires, which could be hours or even days. - Don’t rely on token contents instead of real-time checks
You might think storing the user's roles, scopes, or even resource access in the JWT saves a few database calls. And technically, it does. But you’re trading speed for accuracy—and that’s not a great trade-off when it comes to security. - Don’t mix up authentication and authorization responsibilities
Just because the token proves who someone is doesn’t mean it should decide what they’re allowed to do. That’s a different layer of logic, and you have to keep the two separate.
How to Combine JWTs with a Real Authorization System
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:
- Change access rules without redeploying your app
- Keep tokens lightweight and short-lived
- Audit and log decisions in one place
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.
TL;DR: Best Practices for JWT-Based Authorization
We covered most of these points already, but let’s just do one last quick rundown on how to get things right:
- Keep JWTs short-lived
Tokens should expire quickly—ideally in a few minutes. Use refresh tokens or re-authentication flows to keep things smooth for the user while still protecting your system. Long-lived tokens are just a security risk waiting to happen. - Only store identity-bound data in the token
Stick to claims likeuser_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. - Never include permissions or resources
Don’t embed which folders a user can access or which buttons they can click. That logic belongs in your authorization system, not inside the token. - Always use HTTPS
This one should be obvious, but it's worth repeating: tokens are sensitive. If you're sending them over HTTP, you're asking for trouble. HTTPS is non-negotiable. - Sign your tokens securely—and verify them properly
Use strong algorithms (like RS256 or ES256), rotate your signing keys periodically, and validate every token that comes in. If your token can’t be verified, it shouldn’t be trusted—period. - Treat the token as a starting point, not the full answer
Use the identity inside the JWT to query your actual access control system. Keep tokens small, consistent, and easy to refresh. Let your authorization logic evolve independently.
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.
When (and When Not) to Use JWTs for Authorization
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.
Written by
Daniel Bass
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.
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker