A Guide to Bearer Tokens: JWT vs. Opaque Tokens

- Share:





2938 Members
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.
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.
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.
Bearer tokens are a cornerstone of stateless, scalable authentication. They enable:
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.
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.
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.
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.
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
}
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.
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).
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:
sub)iss)exp)But all of that is hidden from clients and APIs unless they explicitly call the introspection endpoint.
Opaque tokens trade off performance and convenience for security and control. Since all validation and data lives on the server, the issuer can:
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.
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:
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 must be validated through introspection, which means every request needs a call to the authorization server (or a fast local cache). This introduces:
That said, opaque tokens shine in environments where security and real-time access control outweigh raw performance:
To mitigate performance costs, many systems cache introspection results or use hybrid models (JWT for access tokens and opaque for refresh tokens).
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:
Do you need revocation?
Is token visibility a concern?
Are you in a microservice environment?
Do you expect frequent permission changes?
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
This model is widely used in OAuth 2.0 flows and supported by most modern identity providers. It gives you:
JWTs for external/public APIs + Opaque for internal services
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.
Bearer tokens — whether JWTs or opaque — are issued after a user is authenticated. These tokens serve as proof of identity and are included in subsequent requests to help determine what the user is allowed to do. But holding a token isn't the same as having permission.
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.
OAuth scopes are a start, but they're often coarse-grained — useful for general access like read:documents, but not for answering precise questions like:
• "Can Bob edit document 123?"
• "Does this user own the resource they're trying to access?"
• "Should access be allowed based on department, region, or relationship?"
Tokens may contain the user's identity, roles, or scopes — but they don't enforce access. Authorization happens after the token is parsed, using external context, business logic, and often dynamic attributes.
To go beyond basic scopes and static roles, you need fine-grained, contextual authorization — ideally enforced by a centralized policy engine or authorization service.

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:
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:
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:
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.
Tokens confirm identity. Authorization decides access — and that logic lives beyond the token.
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.

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