A Guide to Bearer Tokens: JWT vs. Opaque Tokens
- Share:
Bearer tokens play an important role in securing APIs and managing user sessions. Whether you're building a single-page app, a backend-for-frontend API, or a network of microservices, bearer tokens act as the key that grants access to protected resources—without needing to re-authenticate the user on every request.
But not all bearer tokens are the same.
The two most common types you'll encounter are JSON Web Tokens (JWTs) and opaque tokens. Both serve the purpose of proving identity and access rights, but they differ significantly in structure, security posture, performance, and validation methods.
Choosing the right one for your application can have a major impact on everything from scalability and latency to token revocation and data exposure.
This guide will explain the key differences between JWT and opaque tokens, help you decide when to use each, and explain how each works.
TL;DR: JWT vs. Opaque Tokens
JWTs are self-contained bearer tokens that include all necessary user and access data, allowing for fast, stateless validation — ideal for APIs and microservices. However, they can’t be revoked easily and may expose sensitive data if not encrypted.
Use JWTs for high-performance, stateless APIs.
Opaque tokens, on the other hand, are simple reference strings that require server-side validation. They offer better security and revocation control but come with extra overhead and reduced scalability.
Use opaque tokens when you need fine-grained control, real-time revocation, or private token contents.
What Is a Bearer Token?
A bearer token is a type of access token that acts like a digital key. If you have one, you can use it to access protected resources without requiring a username or password. It’s called a "bearer" token because anyone holding the token can use it, like cash or a concert ticket.
In most applications today, bearer tokens are issued after successful authentication (e.g., logging in via OAuth2 or OpenID Connect). Once issued, they’re sent by the client on each request—typically in the Authorization
header like this:
Authorization: Bearer <token>
This allows servers and APIs to verify the user’s identity and permissions without requiring repeated logins.
Why Are Bearer Tokens Important?
Bearer tokens are a cornerstone of stateless, scalable authentication. They enable:
- Session management without cookies or server state
- Token-based access to APIs across microservices, devices, and platforms
- Fine-grained authorization when paired with systems like OAuth scopes or policy engines
But not all bearer tokens are the same. The two most common formats—JWTs (JSON Web Tokens) and opaque tokens—differ in how they store data, how they're validated, and what they reveal (or don't reveal) about the user. Understanding those differences is critical to properly securing your applications.
The Bearer Token Lifecycle: Before and After Login
Initial State – User not logged in yet
The user exists in the system but hasn’t initiated the login flow.
User initiates login
The user takes action to begin authentication (e.g., submits credentials).
Login request sent to Authentication Server
The app forwards login details to an identity provider (e.g., Auth0, Firebase, Okta).
Token Issued
The authentication server verifies credentials and issues a bearer token.
User logs in with token
The token is returned to the client and stored (typically in local storage, memory, or a secure HTTP-only cookie) for use in future API requests — depending on your app architecture.
Application stores token and proceeds
From this point forward, the app includes the token with every request to prove the user's identity.
JWT vs. Opaque Tokens: Key Differences at a Glance
Before diving deeper into how each token type works, here’s a quick overview of how JWTs compare to opaque tokens across the most important dimensions:
Both token types are bearer tokens, and both can be used for access control—but the trade-offs between speed and control, visibility and confidentiality, make a big difference depending on your architecture and security requirements.
Next, let’s break down JWTs and opaque tokens in detail so you can better understand when to use each.
JSON Web Tokens (JWT) Explained
A JSON Web Token (JWT) is a compact, self-contained bearer token that includes all the information needed to identify a user and authorize access — right inside the token itself. It’s an open standard (RFC 7519) and is widely adopted in web and API development.
JWT Structure
A JWT has three parts, separated by dots:
<Header>.<Payload>.<Signature>
Each part is base64url-encoded JSON:
The Header defines the token type (JWT) and the algorithm used for signing (e.g., HS256 or RS256).
The Payload contains the claims: data like user ID, roles, scopes, expiration time (exp
), issuer (iss
), and audience (aud
).
The Signature cryptographically signs the token to ensure it hasn’t been tampered with.
Example decoded JWT payload:
{
"sub": "1234567890",
"name": "Jane Doe",
"role": "admin",
"exp": 1712240000
}
Why JWTs Are Popular
- Stateless: All the data is inside the token. Servers don’t need to store any session state.
- Fast to validate: Just verify the signature using a shared secret or public key — no database lookup required.
- Interoperable: JWTs are supported across platforms and languages.
- Flexible: You can encode any claims you want, and clients can read them too (for example, a frontend app might display a user’s role or name from the token).
Use Cases for JWTs
- APIs and microservices that need fast, local authentication
- Single Page Applications (SPAs) and mobile apps using token-based login
- Single Sign-On (SSO) implementations
- Systems with minimal server state or session storage
Limitations and Risks
- Revocation is hard: Once a JWT is issued, it’s valid until it expires. There’s no built-in way to revoke it early.
- Security concerns: The payload is readable by anyone with the token — unless you encrypt it. Never store sensitive data in the payload unless it's encrypted or you're absolutely sure it's safe.
- Token bloat: More claims = bigger tokens, which can increase request size and cause issues (especially in HTTP headers).
Misuse for Authorization:
Many developers mistakenly treat JWTs as a complete authorization solution just because they carry user claims like roles or scopes.
JWTs are commonly used for both authentication (via ID tokens) and authorization (via access tokens), but shouldn’t be relied on as the sole source of truth for access decisions.
Embedding access control logic directly into JWTs (e.g., if role == "admin"
) tightly couples permissions to authentication, making them static, hard to manage, and insecure in complex applications.
JWTs should be used to identify the user, and authorization decisions should be made separately (Using a policy engine or external access control service).
JWTs are powerful and performant — but they come with responsibility. You must manage key rotation, expiration, and careful token design to avoid exposing data or granting access longer than intended.
Opaque Tokens Explained
An opaque token is a bearer token that carries no readable information for the client or resource server. It's just a random string — a reference or identifier — that maps to actual user and session data stored securely on the server.
Unlike JWTs, opaque tokens don’t expose any data inside the token itself. If you try to decode one, you’ll get nothing meaningful — which is the point. The only way to validate or use an opaque token is to send it back to the authorization server, usually via a token introspection endpoint, which looks up the token and returns its metadata (if it's valid).
What’s Inside an Opaque Token?
Technically, nothing meaningful to the outside world. A typical opaque token might look like this:
2YotnFZFEjr1zCsicMWpAA
Behind the scenes, this token is tied to a data record on the auth server that includes:
- The user identity (
sub
) - Issuer (
iss
) - Expiration time (
exp
) - Scopes or roles
- Client information
- Any additional metadata
But all of that is hidden from clients and APIs unless they explicitly call the introspection endpoint.
Why Opaque Tokens Matter
Opaque tokens trade off performance and convenience for security and control. Since all validation and data lives on the server, the issuer can:
- Revoke tokens instantly by deleting or disabling them
- Update claims or permissions in real-time
- Keep token contents private, preventing metadata leakage or misuse
Use Cases for Opaque Tokens
- OAuth 2.0 flows where the client only needs to present the token (not inspect it)
- Refresh tokens that must be revocable and long-lived
- Enterprise and internal applications with higher security requirements
- Systems with dynamic access control, where roles or entitlements can change often
Limitations of Opaque Tokens
- Require a lookup: Every token must be validated via a network call or database hit, which adds latency and infrastructure complexity.
- Less scalable: Relying on a centralized introspection endpoint can become a bottleneck in high-throughput systems or microservice architectures.
- Not self-descriptive: APIs and clients can’t make quick decisions based on the token alone — they must always check with the source.
Performance and Scalability
When choosing between JWTs and opaque tokens, the decision usually comes down to a trade-off between security control and performance efficiency. Each token type has strengths and weaknesses across these dimensions.
JWTs: High-Speed, Low-Overhead
One of the biggest advantages of JWTs is that they can be validated locally by any resource server—no network call required. The signature is checked with a secret or public key, and the claims are verified in-memory.
This makes JWTs really fast and highly scalable — perfect for:
- High-throughput APIs
- Distributed systems (e.g., microservices)
- Serverless functions
However, this speed comes at the cost of control. You’re trusting a token for its entire lifetime, and revocation is inherently clunky.
Opaque Tokens: Centralized, Slower
Opaque tokens must be validated through introspection, which means every request needs a call to the authorization server (or a fast local cache). This introduces:
- Additional latency (typically 10–50ms or more per request)
- A central dependency — the auth server must be available and fast
- Scalability challenges in distributed systems
That said, opaque tokens shine in environments where security and real-time access control outweigh raw performance:
- Enterprise internal services
- Systems with strict compliance
- Long-lived sessions or refresh tokens
To mitigate performance costs, many systems cache introspection results or use hybrid models (JWT for access tokens and opaque for refresh tokens).
When to Use Each Token Type
Choosing between JWTs and opaque tokens isn’t about which one is “better” — it’s about which one fits your application’s architecture, security model, and operational needs.
Here’s a breakdown to help guide your decision:
Decision-Making Criteria
Do you need revocation?
- Yes → Use opaque tokens. These can be revoked instantly by the server, making them ideal for long-lived sessions, refresh tokens, or high-security environments.
- No (short-lived tokens are fine) → JWTs work well, especially when paired with short expiration times to minimize risk.
Is token visibility a concern?
- Yes → Use opaque tokens. They don’t expose any user data or claims, making them more secure by default — especially in zero-trust or regulated environments.
- No → JWTs are fine if you're okay with exposing some metadata (e.g., roles, usernames) and avoid putting sensitive data in the token.
Are you in a microservice environment?
- Yes → Use JWTs. Their stateless nature and local validation make them ideal for distributed systems where speed and scalability are critical.
- No (centralized apps or monoliths) → Opaque tokens are often easier to manage and provide better control for traditional applications.
Do you expect frequent permission changes?
- Yes → Use opaque tokens. Since they’re validated server-side, any updates to user roles or access policies take effect immediately.
- No → JWTs can still work, but be cautious of long expiration times that may preserve outdated permissions.
Hybrid Approaches (The Best of Both Worlds)
In practice, many production systems use a combination of both JWTs and opaque tokens to balance speed, security, and revocation:
JWT for Access + Opaque for Refresh
- Access Tokens: Use short-lived JWTs for fast, stateless access to APIs.
- Refresh Tokens: Use opaque tokens for secure, revocable session extension.
This model is widely used in OAuth 2.0 flows and supported by most modern identity providers. It gives you:
- The speed and scalability of JWTs for most requests
- The control and revocability of opaque tokens when refreshing sessions
JWTs for external/public APIs + Opaque for internal services
- JWTs are easy to validate across teams or third-party clients.
- Opaque tokens offer tighter control within your trusted infrastructure.
If you're using a system like Permit.io, you can extract the user identity from either token type and apply fine-grained authorization policies on top — whether you're using RBAC, ABAC, ReBAC, or a combination of all three.
Authentication → Authorization
Whether you're using JWTs, opaque tokens, or a hybrid of both, those tokens primarily handle authentication — confirming who the user is. But that’s only the first half of what you need to ensure proper access control.
Once the user is authenticated, you still need to answer the harder question:
“What is this user allowed to do?”
That’s the job of authorization.
Where Authentication and Authorization Connect -
A common mistake in modern app development is assuming that claims inside a token (like role:admin
) are enough to determine access. This approach works for simple apps, but quickly breaks down as complexity grows:
- What if user permissions change after the token is issued?
- What if access depends on dynamic attributes (like department, location, or resource ownership)?
- What about access to nested resources (e.g., documents inside folders inside teams)?
- How do you audit who accessed what — and why?
Hardcoding access checks like if (user.role === 'admin')
in your app logic or trying to infer permissions from a JWT creates tight coupling, poor visibility, and a security risk.
Externalizing Authorization with Policy as Code
In modern application design, it's a best practice to decouple authorization from application code — moving access control decisions out of hardcoded logic and into policy-driven systems. This approach is known as externalized authorization, and it's often implemented using policy as code: structured, declarative rules that define who can access what, and under which conditions.
Instead of relying on static checks inside JWTs or imperative if
statements scattered across your codebase, you define your access rules in one central place — just like you would with infrastructure-as-code tools. This allows for:
- Centralized control over access rules
- Dynamic updates to permissions (without redeploying your app)
- Easier audits and compliance
- Support for advanced models like RBAC, ABAC, ReBAC, and PBAC
Implementing Authorization (Without Building it from Scratch)
Permit.io is an authorization as a service provider that allows you to implement these models without building them from scratch. It integrates with your authentication system, pulls identity data from bearer tokens (whether JWT or opaque), and evaluates access requests against policies you've defined. It also provides a UI and API to manage roles, attributes, relationships, and workflows in one place.
Externalized authorization with policy as code is a scalable and secure way to manage permissions — and tools like Permit.io help you get there faster. Here’s how it works:
- Extract identity from the token (JWT or opaque via introspection).
- Evaluate access based on pre-defined policies you configure in Permit.io
- Enforce decisions consistently across frontend, backend, and microservices.
Permit.io gives you a centralized, dynamic, and secure way to control access — using the identity already provided by your bearer tokens. This means you can:
- Work with both JWTs and opaque tokens
- Decouple your auth logic from your application code
- Support real-time updates to permissions (no need to reissue tokens)
- Integrate authorization with your existing auth provider (e.g., Auth0, Firebase, Okta)
- Use a built-in UI for policy generation, management, auditing, etc.
Conclusion
Bearer tokens are the backbone of API authentication — but choosing between JWTs and opaque tokens isn’t just a technical question. It’s a design decision that impacts your app’s security, performance, and operational complexity.
JWTs offer speed, scalability, and local validation, making them ideal for distributed systems and high-performance APIs. However, they come with risks—like limited revocation and potential data exposure—and are often misused as an all-in-one authorization solution.
Opaque tokens provide stronger security and real-time control. They’re a great fit for long-lived sessions, dynamic permissions, and high-trust environments — though they do introduce infrastructure overhead due to introspection.
The bottom line: Use JWTs when you need speed and statelessness. Use opaque tokens when you need control and revocability.
Remember that tokens authenticate the user, but authorization is a separate concern.
Rather than hardcoding access logic into your app or relying on token claims alone, externalize your authorization using policy as code. Tools like Permit.io can help you implement fine-grained access control that's dynamic, scalable, and secure — no matter which token format you use.
Want to learn more about access control? Join our Slack community, where thousands of developers discuss, build, and implement access control for their applications.
Written by
Gabriel L. Manor
Full-Stack Software Technical Leader | Security, JavaScript, DevRel, OPA | Writer and Public Speaker