OAuth 2.0 vs JWT: Authentication Patterns

Felipe RodriguesFelipe Rodrigues
15 min read

I still remember the meeting vividly. We were a small, sharp team at a fintech startup, racing to get our first API to market. The whiteboard was a chaotic mess of boxes and arrows, but the energy was electric. Then came the inevitable question from a bright, eager engineer: "For authentication, why don't we just use JWTs? They're stateless, fast, and simple. We can sign them on the server, send them to the client, and we're done."

Heads nodded around the room. It sounded so clean, so modern. It was the path of least resistance, a clean break from the cumbersome session-based systems of the past. We agreed, coded it up in a week, and patted ourselves on the backs. Our API was secure. Or so we thought.

The first crack appeared three months later when we wanted to build a "Connect your bank" feature. We needed to let our application access a user's data on a third-party banking API, but only with their explicit permission and only for specific actions. Our simple JWT system had no vocabulary for this. It was a binary credential: you were either logged in with full access, or you weren't. The second, more painful crack came when a customer's account was compromised. We needed to immediately revoke their access. But our JWTs were stateless, designed to be valid until their expiration time. We had no mechanism to kill a token mid-flight without building a complex, stateful revocation list, ironically defeating the entire purpose of our "stateless" architecture.

This experience taught me a hard lesson, one that has been reinforced over a decade of building and breaking systems. My thesis is this: the most persistent and costly mistake I see teams make is framing the discussion as "OAuth 2.0 versus JWT". This is a fundamental category error, a false dichotomy that leads to brittle, insecure, and unscalable architectures. It's like asking whether you should use a key or a key-making machine. The real question is: are you building a simple door lock or a full-fledged hotel security system? One is a token format, the other is an authorization framework. Understanding the difference is not just academic; it's the bedrock of modern, secure system design.

Unpacking the Hidden Complexity

The allure of the "JWT-as-auth" model is its apparent simplicity. A client logs in with a username and password, the server generates a signed JSON Web Token (JWT) containing a user ID and an expiration date, and the client includes this token in the Authorization header for all subsequent requests. The server, upon receiving a request, simply verifies the token's signature and checks its expiry. No database lookups, no session state. It seems perfect.

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#fffde7", "primaryBorderColor": "#f57f17", "lineColor": "#424242"}}}%%
flowchart TD
    classDef client fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    classDef server fill:#fce4ec,stroke:#ad1457,stroke-width:2px

    User[User Enters Credentials]
    subgraph ClientApp [Client Application]
        direction LR
        A[1 - POST to /login]
    end

    subgraph BackendAPI [Backend API]
        direction LR
        B[2 - Validate Credentials] --> C[3 - Generate Signed JWT]
    end

    D[4 - Store JWT in Client]
    E[5 - Include JWT in future API calls]
    F[6 - Server Validates JWT Signature and Expiry]

    User --> A
    A --> B
    C --> D
    D --> E
    E --> F

    class User,A,D,E client
    class B,C,F server

Figure 1: The Naive "JWT-Only" Authentication Flow. This diagram illustrates the deceptively simple flow where a JWT is the beginning and end of the security story. The client logs in, receives a JWT, and uses it for all future requests. The server's only job is to validate this token. While this works for a single, simple service, it hides significant architectural limitations.

This simple model works beautifully until it doesn't. The moment your system requirements evolve beyond a single client talking to a single monolithic backend, this architecture begins to crumble under the weight of its own limitations. The "simplicity" was an illusion, masking a host of complex problems you now have to solve yourself.

The Second-Order Effects of a Flawed Model

  1. The Revocation Nightmare: As in my story, what happens when a token needs to be invalidated before its expiration? A user logs out from one device but should remain logged in on others. An administrator detects a threat and needs to kill a session immediately. A purely stateless JWT is a signed promise that is valid until its exp claim says it is. To break that promise, you must introduce state. You end up building a token denylist in a database like Redis. Every single API request now requires not just cryptographic verification but also a database lookup to check against this list. You've just reinvented stateful sessions, but with more complexity and a fancy new name.

  2. The Delegation Dead End: This is the true killer of the JWT-only approach. Modern software ecosystems are built on integration and delegated authority. A user should be able to grant Application A permission to read their calendar from Service B, without giving Application A their password for Service B. This concept is called delegation, and it's the absolute core of what OAuth 2.0 was designed to solve. A simple JWT bearer token has no standard, secure mechanism for this. How does Service B know that the user authorized Application A? How does it know what specific permissions (scopes) were granted? You would have to invent your own proprietary system of claims and trust relationships, a task far more complex and perilous than it sounds.

  3. The Scope-Creep Catastrophe: At first, your JWT might just contain sub (subject/user ID) and exp (expiration). Then, a new requirement comes in: admins should have more power than regular users. So you add a role: 'admin' claim. Then, you need to differentiate between read and write access, so you add permissions: ['read', 'write']. Before you know it, your JWT is a bloated container of business logic. This creates tight coupling. If you change a permission structure, you have to coordinate a rollout across every service that issues and consumes these tokens. It also makes the token large, increasing request overhead. The token's job is to prove who the caller is and what they are authorized to access, not to be a portable user profile.

To better frame this, let's use an analogy.

The Mental Model: The Hotel Key Card vs. The Front Desk

  • A JWT is like a modern hotel key card. It's a self-contained object. You can look at it and know certain things. It’s encoded with the room number it can open (aud or audience claim), an expiration date (exp claim), and who issued it (iss or issuer claim). When you tap it on a door, the lock (your API) can validate these details locally without calling the front desk. It's efficient and self-sufficient.

  • OAuth 2.0 is the entire hotel front desk process. It's the standardized protocol for how a guest (the User) proves their identity, requests a key for a specific duration, and is granted a key card (the Access Token) by a trusted authority (the Authorization Server). It handles checking you in, verifying your identity, taking your payment, and defining what your key can do (e.g., access room 301 and the gym, but not the penthouse). The key card it issues might be a JWT, but it could also be a simple, opaque string of characters. The key itself is just an artifact of the robust, secure process.

Arguing for "JWT vs. OAuth 2.0" is like a hotel architect arguing whether to focus on the plastic material for the key cards or the design of the front desk and its security procedures. It's a nonsensical comparison. You need both a secure process and a functional artifact.

Let's break down the comparison with a clearer focus on their roles.

FeatureJWT (The Token Format)OAuth 2.0 (The Authorization Framework)
Primary RoleInformation Assertion. A compact, URL-safe means of representing claims to be transferred between two parties. It's a signed statement.Delegated Authorization. A protocol that allows a user to grant a third-party application limited access to their resources, without exposing their credentials.
Core ConceptA signed, self-contained JSON object for securely transmitting information.A framework with defined roles (Resource Owner, Client, Auth Server, Resource Server) and flows (grant types) for obtaining access tokens.
StatefulnessThe token itself is stateless. The system using it may need to be stateful to handle revocation.The Authorization Server is inherently stateful (it manages grants, clients, and refresh tokens). The resulting Access Tokens can be stateless (JWTs) or stateful (opaque tokens).
Key Question it Answers"I have this token. What information can I trust is contained within it, who sent it, and has it been tampered with?""How can User A securely authorize App B to access their data on Server C, for a limited purpose and duration?"
Common PitfallUsing it as a session token without a plan for revocation, or stuffing it with excessive user profile data.Implementing insecure flows (e.g., Implicit Grant, now deprecated) or underestimating the complexity of running a secure Authorization Server.
RelationshipJWT is a common format for access tokens and ID tokens issued within an OAuth 2.0 flow. It's a tool used by the framework.OAuth 2.0 is the protocol that orchestrates the issuance and use of access tokens. It can use JWTs as its token format.

The Pragmatic Solution: Using Them Together

The robust, scalable, and secure solution is not to choose between them, but to use them in their designated roles. You use the OAuth 2.0 framework to govern the authorization process, and you use JWTs as the format for the access and ID tokens that the framework produces.

The gold standard for this today is the Authorization Code Flow with Proof Key for Code Exchange (PKCE). It's designed to be highly secure, especially for public clients like Single Page Applications (SPAs) and mobile apps that cannot securely store a client secret.

Let's walk through this flow.

sequenceDiagram
    actor User
    participant ClientApp as Client App (SPA/Mobile)
    participant AuthServer as Authorization Server
    participant ResourceServer as Resource Server (Your API)

    User->>ClientApp: Clicks "Login"
    ClientApp->>User: Redirects to Auth Server with PKCE challenge
    User->>AuthServer: Logs in and grants consent
    AuthServer->>User: Redirects back to Client App with Authorization Code
    User->>ClientApp: Delivers Authorization Code via redirect
    ClientApp->>AuthServer: Exchanges Code + PKCE verifier for tokens
    Note over AuthServer: Validates code and PKCE verifier
    AuthServer-->>ClientApp: Returns Access Token (JWT) and ID Token (JWT)
    ClientApp->>ResourceServer: Makes API call with Access Token in header
    Note over ResourceServer: Validates JWT (signature, exp, aud, iss)
    ResourceServer-->>ClientApp: Returns protected data

Figure 2: The OAuth 2.0 Authorization Code Flow with PKCE. This sequence diagram details the modern, secure way to handle authentication and authorization. It involves a "dance" between the client, the user, and a dedicated Authorization Server. The final artifact, the Access Token, is typically a JWT, but it's the result of this secure protocol, not a standalone solution.

This flow elegantly solves the problems of our initial naive approach:

  1. Delegation is Built-In: The entire flow is about the user (Resource Owner) delegating access to the Client App. The consent screen is a core part of the process.

  2. Revocation is Manageable: The Authorization Server is a stateful component. It tracks issued grants and refresh tokens. To revoke access, you invalidate the refresh token and any active sessions at the Authorization Server. Since access tokens are deliberately short-lived (e.g., 15 minutes), the compromised token quickly becomes useless. The client must use its valid refresh token to get a new access token, but since the refresh token has been revoked, this will fail.

  3. Clear Separation of Concerns:

    • The Authorization Server's only job is to authenticate users and issue tokens. It is the single source of truth for identity.

    • The Resource Server (your API) only needs to know how to validate a JWT from a trusted issuer. It doesn't care how the user logged in.

    • The Client App only needs to know how to orchestrate the OAuth 2.0 flow and handle tokens.

A Mini-Case Study: ConnectSphere Re-architected

Let's revisit our fintech startup, ConnectSphere. After their initial stumbles, they adopt this modern architecture.

  1. They Adopt an Authorization Server: Instead of building their own, they choose a dedicated, certified solution like Auth0 (for speed) or deploy an open-source one like Keycloak (for control). This immediately saves them thousands of hours of development and security auditing.

  2. They Define an API Gateway: All incoming requests to their microservices must first pass through an API Gateway. This gateway is configured to do one thing very well: inspect the Authorization header, validate the JWT access token against the Authorization Server's public keys, and check its claims (aud, iss, exp). If valid, it forwards the request, perhaps injecting user information into a header for downstream services.

  3. Their Services are Simplified: The individual backend services (e.g., Accounts Service, Transactions Service) no longer contain any complex authentication logic. They simply trust that if a request has reached them from the gateway, it is authenticated. They only need to check for authorization (e.g., does the scope claim in the token include transactions:read?).

%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#e8f5e9", "primaryBorderColor": "#2e7d32", "lineColor": "#333", "secondaryColor": "#f3e5f5"}}}%%
flowchart TD
    subgraph UserInteraction
        direction LR
        User[User] <--> Client[Client App SPA or Mobile]
    end

    subgraph AuthSystem
        direction TB
        AuthServer[Authorization Server e.g. Keycloak Auth0]
    end

    subgraph APILayer
        direction TB
        Gateway[API Gateway] --> S1[Accounts Service]
        Gateway --> S2[Transactions Service]
        Gateway --> S3[Notifications Service]
    end

    Client -- 1 - Redirect for Login --> AuthServer
    AuthServer -- 2 - Returns Auth Code --> Client
    Client -- 3 - Exchange Code for JWT --> AuthServer
    AuthServer -- 4 - Returns JWT Access Token --> Client
    Client -- 5 - API Request with JWT --> Gateway
    Gateway -- 6 - Validate JWT --> AuthServer
    Gateway -- 7 - Forward Request --> S1

Figure 3: A Robust and Scalable Architecture. This diagram shows the components working in harmony. The client interacts with a dedicated Authorization Server to get tokens. All API calls go through a Gateway which validates the token. The backend services are protected and simplified, focusing on business logic instead of complex auth concerns. This pattern is scalable, secure, and flexible.

Now, when ConnectSphere needs to integrate with the third-party banking API, it's a standard OAuth 2.0 flow. When they need to revoke a user's access, they do it in their Authorization Server's dashboard with a single click. Their architecture is no longer brittle; it's built on standards and prepared for future complexity.

Traps the Hype Cycle Sets for You

The tech industry is driven by trends, and it's easy to fall for appealing but flawed ideas. Here are the most common traps related to this topic.

  • Trap 1: "Stateless is always better." The siren song of statelessness is powerful for engineers who value scalability. But for authentication and authorization, pure statelessness is a myth. You will always need revocation. The pragmatic solution is not to chase a purely stateless dream but to embrace short-lived state. Access tokens (JWTs) are stateless and short-lived (5-15 minutes). Refresh tokens are stateful, long-lived, and revocable. This hybrid model gives you performance for the 99% of requests using the access token, and security and control via the stateful refresh token.

  • Trap 2: "We're smart, we can build our own auth server." Don't. Just don't. The OAuth 2.0 and OpenID Connect (OIDC) specifications are hundreds of pages long for a reason. They are filled with subtleties and security considerations that are incredibly easy to get wrong. Cross-site request forgery, code interception attacks, token leakage, improper signature validation—the list of potential vulnerabilities is endless. Your company's core business is likely not identity management. Offload that critical, non-differentiating work to experts, whether by using a managed service or a well-vetted open-source project.

  • Trap 3: "The Access Token is a bag for all user data." It's tempting to cram user roles, permissions, profile information, and preferences into the JWT access token to avoid database lookups. This is an anti-pattern. It bloats the token, increases request overhead, and couples your API's authorization logic to a specific data structure. Keep the access token lean. Its job is to authorize access. It should contain a subject ID, an audience, an issuer, an expiration, and scopes. If the client needs user profile data, it should use the OIDC id_token. If a backend service needs user data, it should use the subject ID from the access token to query a dedicated User Service.

Architecting for the Future

The debate is over. It was never OAuth 2.0 versus JWT. The modern, secure, and scalable architecture uses OAuth 2.0 with JWT. The framework provides the protocol, the roles, and the flows for secure delegated authorization. The token format provides a verifiable, self-contained artifact for that authorization.

By adopting this model, you are not just solving today's login problem. You are building a foundation for the future. When your company wants to launch a partner API, enable third-party integrations, or adopt a zero-trust security model inside your own network, you will have the architectural primitives already in place. You will be speaking the lingua franca of modern digital identity.

Your First Move on Monday Morning:

  1. Audit Your Flow: Look at your system's authentication and authorization flow. Are you using JWTs as a simple replacement for session IDs? Draw the flow on a whiteboard. If it looks like Figure 1 and not like Figure 2, you have technical debt. Identify where you have had to reinvent scope management, revocation, or delegation.

  2. Isolate Your Auth Server: Identify the piece of your code that mints tokens. Is it buried inside your main monolith? Your first step is to architecturally isolate it. Even if it's still your own code, treat it as a separate Authorization Server. Define a clear boundary. This is the first step toward eventually replacing it with a dedicated, robust solution.

  3. Standardize on the Right Flow: For any new client application (SPA, mobile, or third-party), mandate the use of the Authorization Code Flow with PKCE. Create documentation and starter kits for your developers. Stop the spread of legacy or insecure patterns.

I'll leave you with one final question to ponder. As we move toward a future of ephemeral infrastructure, service meshes, and increasingly complex supply chains, security can no longer be a simple perimeter wall. How will your identity and access strategy evolve beyond simple API keys and self-rolled JWTs to provide verifiable, fine-grained, and revocable access for every single interaction between every single service?


TL;DR

  • Stop comparing OAuth 2.0 and JWT. It's a false dichotomy. OAuth 2.0 is an authorization framework (a protocol), while JWT is a token format.

  • The correct pattern is to use them together: Use the OAuth 2.0 protocol (specifically, the Authorization Code Flow with PKCE) to orchestrate the secure issuance of tokens, and use JWTs as the format for those tokens.

  • A "JWT-only" approach is brittle. It fails when you need to revoke tokens, delegate access to third parties, or manage granular permissions (scopes) in a standardized way.

  • Use a dedicated Authorization Server. Don't build your own. Use a managed service (Auth0, Okta) or a robust open-source project (Keycloak, Ory). This is critical for security and lets your team focus on your core product.

  • Keep Access Tokens lean. They are for authorization, not for user profile data. Use them to convey who the user is (sub) and what they can do (scope), not to carry their entire life story.

0
Subscribe to my newsletter

Read articles from Felipe Rodrigues directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Felipe Rodrigues
Felipe Rodrigues