Building Secure APIs with OAuth 2.0 and JWT Authentication

Building Secure APIs with OAuth 2.0 and JWT Authentication

Executive Summary

Most API security implementations fail not because developers choose the wrong protocol, but because they misunderstand what OAuth 2.0 actually secures versus what JWTs actually validate. OAuth solves delegation, not authentication. JWTs solve stateless validation, not authorization. Conflating these responsibilities creates the vulnerabilities that breach production systems.

The Real Problem: Mixing Authorization Protocols with Authentication Tokens

The typical API security conversation starts with “should we use OAuth or JWT?” This question reveals a fundamental misunderstanding. OAuth 2.0 is an authorization framework. JWT is a token format. They solve different problems and work together, not as alternatives.

The confusion costs SaaS teams real money. We have seen production systems that implement OAuth flows correctly but store plaintext JWT secrets in application code. We have debugged APIs where teams validated JWT signatures but never checked token expiration. We have migrated systems off custom authentication because the founding engineer thought OAuth was “too complex for MVP” and built a homegrown session system that became a security liability at scale.

This article addresses the architectural decisions that determine whether your API security holds up under actual attack, not just penetration testing during QA.

Mental Model 1: The Three-Layer Security Stack

Production API security operates on three independent layers. Teams that conflate them create security gaps that are invisible until exploited.

Layer 1: Authentication – Who are you? Validates that the entity making the request is who they claim to be. Typically uses credentials (username/password), certificates, or federated identity (Google, Microsoft). OAuth handles authentication through the authorization server.

Layer 2: Authorization – What are you allowed to do? Determines which resources and operations the authenticated entity can access. OAuth’s access tokens carry authorization grants. JWTs can encode permissions but do not enforce them.

Layer 3: Resource Protection – Does your request meet resource-specific rules? Applies fine-grained access control at the resource level. A user might have authorization to read orders, but not orders belonging to other tenants. This layer is never handled by OAuth or JWT alone.

The most common production mistake we see: teams implement Layers 1 and 2 correctly but assume that validating a JWT signature means the request is authorized for any resource the token grants access to. Layer 3 requires explicit policy checks at the resolver level for every protected resource.

OAuth 2.0: What It Actually Does

OAuth 2.0 exists to solve the delegation problem: how does Application A access resources on Application B on behalf of a user, without Application A ever seeing the user’s password for Application B?

Before OAuth, users gave third-party apps their passwords. The app stored those credentials and acted as the user. No way to revoke access without changing the password. No way to limit what the app could do. OAuth eliminates both problems.

The Four OAuth Grant Types and When to Use Each

Authorization Code Grant – The only grant type that should be used for web and mobile applications accessing your API on behalf of users.

Flow:

  1. User clicks “Login with YourApp” in the client application
  2. Client redirects to your authorization server with client_id, redirect_uri, and scope
  3. User authenticates and consents to requested permissions
  4. Authorization server redirects back with an authorization code
  5. Client exchanges code for access token and refresh token via a backend server call
  6. Client uses access token to make API requests

Security property: The access token never passes through the browser or mobile app during the exchange. The authorization code is useless without the client secret, which only the backend server possesses.

Client Credentials Grant – Used for server-to-server API calls where no user is involved.

Flow:

  1. Server authenticates with client_id and client_secret
  2. Authorization server returns access token
  3. Server uses access token for API requests

Use case: A background job processing data, a webhook handler, or an internal service calling your API. No user consent required because no user is involved.

Implicit Grant – Deprecated. Do not use. Replaced by Authorization Code with PKCE.

Resource Owner Password Credentials Grant – Only use if you control both the client and authorization server and cannot implement a proper OAuth flow. Typically a sign the architecture needs rethinking.

PKCE: Why Every OAuth Flow Needs It Now

Proof Key for Code Exchange (PKCE, pronounced “pixie”) solves authorization code interception attacks. Mobile apps and single-page applications cannot securely store client secrets. PKCE eliminates the need for client secrets in authorization code flows.

Without PKCE: An attacker intercepts the authorization code during the redirect and exchanges it for an access token using a stolen or nonexistent client secret.

With PKCE: The client generates a cryptographically random code_verifier and derives a code_challenge. The authorization server stores the challenge. When exchanging the code, the client provides the verifier. The server validates that hashing the verifier produces the stored challenge. Without the original verifier, the authorization code is useless.

PKCE implementation adds minimal complexity and eliminates a significant attack vector. Always use it for mobile and browser-based applications.

JWT: What It Actually Validates

A JSON Web Token is a cryptographically signed JSON payload. The signature proves the token was issued by someone with the private key and has not been tampered with. JWTs do not inherently provide security. They provide verifiable claims.

JWT Structure Explained

A JWT consists of three base64-encoded sections separated by dots:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTEyMyIsImlhdCI6MTczNTIwMDAwMH0.signature
^header                                ^payload                            ^signature

Header: Specifies the signing algorithm (RS256, HS256, etc.) and token type. Payload: Contains claims like sub (subject/user ID), exp (expiration), iat (issued at), and custom claims. Signature: Computed by hashing the header and payload with a secret key or private key.

The Critical Validation Steps Most Teams Skip

Validating a JWT requires more than checking the signature. My team has audited production APIs where signature validation passed but the following checks were absent:

Expiration validation – Check that exp (expiration time) is in the future. Tokens should be short-lived: 15-60 minutes for access tokens. Teams that set exp to weeks or months misunderstand the entire purpose of token expiration.

Issued-at validation – Reject tokens where iat (issued at) is in the future. This catches clock skew attacks and malformed tokens.

Audience validation – Check that aud (audience) matches your API identifier. Prevents tokens issued for other services from being replayed against your API.

Issuer validation – Verify iss (issuer) matches your authorization server. Prevents tokens from other OAuth providers being accepted.

Algorithm validation – Hardcode the expected algorithm (RS256 for production). The “none” algorithm attack exploits APIs that trust the algorithm specified in the JWT header. Explicitly rejecting alg: none prevents signature bypass.

Token revocation check – JWTs are self-contained and cannot be revoked without a persistent store. Implement a revocation list (Redis set of token IDs) checked on every validation. High-security operations (password reset, permission changes) must invalidate active tokens immediately.

Mental Model 2: The Token Lifecycle Pyramid

JWT usage follows a pyramid structure where tokens exist in four states, each with different trust properties.

Tier 4 (Bottom): Issued Token – Generated by the authorization server. Not yet validated. Trust: None. Anyone can create a JWT. Validation has not occurred.

Tier 3: Validated Token – Signature verified, expiration checked, issuer and audience confirmed. Trust: Cryptographic. The token was issued by your authorization server and has not been tampered with.

Tier 2: Authorized Token – Validated token with confirmed permissions for the requested resource. Trust: Authorization. The token grants access to the specific operation being attempted.

Tier 1 (Top): Contextually Authorized Token – Authorized token with tenant, resource ownership, and fine-grained policy checks passed. Trust: Full. The request meets all security requirements for the specific resource instance.

Most security breaches occur because teams validate tokens (Tier 3) but skip Tier 2 and Tier 1 checks. A valid JWT does not mean the request is authorized. It means the JWT was issued by your system. Authorization happens at the resolver level, per resource, per request.

Production Implementation Patterns

Stateless JWT Validation Architecture

Access tokens should validate statelessly for performance. Store the public key (for RS256 JWTs) in application memory and validate signatures without database calls. This enables horizontal scaling without shared session state.

However, “stateless” does not mean “no persistent checks.” Critical operations require checking a revocation list stored in Redis or similar fast key-value store.

Refresh tokens must be stored and validated against persistent storage (database or Redis) to enable revocation. Compromise a refresh token and you have long-lived access. Revocation is non-negotiable.

Token Storage: Where to Put JWTs in Client Applications

Web Applications (Server-Side Rendered): Store tokens in HTTP-only, Secure, SameSite=Strict cookies. Never store tokens in localStorage or sessionStorage. JavaScript cannot access HTTP-only cookies, preventing XSS exfiltration.

Single-Page Applications (SPAs): Use HTTP-only cookies with a backend-for-frontend (BFF) pattern, or store tokens in memory only and re-authenticate on page refresh. LocalStorage is not secure for tokens. XSS attacks extract tokens trivially from localStorage.

Mobile Applications: Use the platform’s secure enclave: iOS Keychain with kSecAttrAccessibleWhenUnlockedThisDeviceOnly or Android Keystore with setUserAuthenticationRequired(true). Never store tokens in shared preferences or SQLite databases.

Refresh Token Rotation

Refresh tokens live longer than access tokens (days or weeks vs. minutes). Rotating refresh tokens on every use limits the blast radius of compromise.

Rotation pattern:

  1. Client uses refresh token to request new access token
  2. Authorization server issues new access token and new refresh token
  3. Authorization server invalidates old refresh token
  4. Client stores new refresh token, discards old one

If the old refresh token is used again, it indicates theft. Invalidate all refresh tokens for that user session immediately.

Common Mistakes That Break Production Systems

Mistake 1: Using HS256 in Multi-Service Architectures

HS256 (HMAC with SHA-256) is a symmetric signing algorithm. The same secret signs and validates tokens. If you have multiple services validating tokens, every service possesses the secret, and any service can forge tokens.

Use RS256 (RSA with SHA-256) or ES256 (ECDSA with SHA-256) in production. These are asymmetric: the authorization server signs with a private key, services validate with a public key. Services cannot forge tokens because they lack the private key.

The only valid use case for HS256: Single-service architectures where the authorization server and API server are the same codebase. Even then, RS256 provides better forward compatibility when you inevitably split services.

Mistake 2: Storing Sensitive Data in JWT Payloads

JWTs are base64-encoded, not encrypted. Anyone with the token can decode and read the payload. We have seen production tokens containing email addresses, phone numbers, and internal role identifiers visible to anyone intercepting the token.

Store only non-sensitive identifiers in JWTs: user ID, tenant ID, expiration time. Fetch sensitive attributes from your database during request handling.

Mistake 3: Not Validating Redirect URIs

Authorization code flows redirect users back to the client application with the authorization code. If the authorization server does not validate that the redirect_uri matches a pre-registered value, attackers redirect users to attacker-controlled domains and steal authorization codes.

Require exact string matching on redirect URIs during client registration. Do not allow wildcard matching or subdomain wildcards unless you fully understand the security implications.

Mistake 4: Setting Token Expiration to Days or Weeks

Access tokens should expire quickly: 15-60 minutes maximum. Long-lived access tokens increase the window for exploitation if compromised. Users stay logged in via refresh token rotation, not long access token lifetimes.

We have audited systems with 30-day access token expiration and no refresh token implementation. The reasoning: “Refresh tokens are complicated.” The result: Stolen tokens grant access for 30 days with no revocation mechanism.

Mistake 5: Checking Permissions Once at Login

Permissions change. Users get promoted, downgraded, or removed from teams. If you cache permissions in the JWT and only re-validate on token refresh, permission changes take effect only after token expiration.

For high-security operations (payment processing, data export, administrative actions), fetch current permissions from the database regardless of what the JWT claims. Trust the JWT for identity, verify permissions from source of truth.

When Not to Use OAuth and JWT

OAuth and JWT add complexity. Do not use them when simpler solutions suffice.

Skip OAuth when:

  • You are building an internal API with no third-party integrations. API keys with HMAC signatures are simpler and equally secure.
  • Your application has no concept of user delegation. If users never grant third-party apps access to their data, OAuth is unnecessary overhead.
  • You are building a single-tenant system with a single web client. Traditional session-based authentication with HTTP-only cookies is simpler and well-proven.

Skip JWT when:

  • You have a monolithic application with server-side sessions. Session IDs in cookies validated against a database are simpler than JWTs and enable instant revocation.
  • You operate in a regulated environment requiring audit trails for every authentication event. JWTs are self-contained and do not generate database records. Traditional sessions provide better auditing.
  • You need instant token revocation without maintaining a revocation list. Sessions stored in Redis or a database offer instant invalidation. JWT revocation requires additional infrastructure.

The decision is not about which technology is “better.” It is about architectural fit. OAuth and JWT excel in distributed systems with multiple services, third-party integrations, and mobile/SPA clients. For simpler architectures, simpler solutions work better.

Enterprise Considerations

Enterprise deployments add requirements that significantly complicate OAuth and JWT implementations.

Single Sign-On (SSO) Integration: Enterprise customers demand SSO through their identity providers (Okta, Azure AD, OneLogin). Your authorization server must support SAML or OIDC federation. OIDC (OpenID Connect) is OAuth 2.0 with standardized identity claims and works better than SAML for modern SaaS.

Implementation requires:

  • Discovery endpoint for OIDC metadata
  • JWK (JSON Web Key) endpoint exposing public keys
  • UserInfo endpoint returning profile claims
  • Support for enterprise-specific claims (groups, roles, custom attributes)

Token Exchange for Service-to-Service Calls: Enterprise architectures involve multiple internal services calling each other on behalf of a user. The Token Exchange specification (RFC 8693) defines how Service A exchanges a user’s access token for a new token scoped to Service B. This maintains audit trails showing which service acted on whose behalf.

Tenant Isolation at Token Level: Multi-tenant SaaS must encode tenant context in every token and validate it in every resolver. A user with tokens for Tenant A must never access resources belonging to Tenant B, even if permissions appear sufficient.

Tenant validation happens in middleware before any business logic executes. Extract tenant_id from the JWT, compare to the tenant scoped in the request (via subdomain, header, or path parameter), reject mismatches with a 404 (not 403, to prevent enumeration).

Compliance Logging: Regulated industries require recording who accessed what data when. OAuth flows generate authorization grants. JWT validation does not inherently log anything. Implement audit logging at the API gateway or load balancer level, recording every token validation and resource access with user ID, tenant ID, resource ID, timestamp, and outcome.

Token Lifetime Policies: Enterprises often mandate maximum token lifetimes: 30-minute access tokens, 24-hour refresh tokens, forced re-authentication every 8 hours. Configure these policies at the authorization server level and enforce them consistently across all clients.

Cost and Scalability Implications

Authorization Server Costs: Running your own OAuth authorization server requires persistent storage (PostgreSQL or similar) for clients, users, authorization codes, and refresh tokens. Add Redis for rate limiting and token revocation lists. For SaaS at scale, hosting an authorization server runs $200-500 monthly for infrastructure alone.

Managed OAuth providers (Auth0, Okta, AWS Cognito) start at $0-$100/month for small user bases and scale to thousands per month at enterprise volume. Trade-off: Lower operational burden, higher per-user cost, vendor lock-in.

JWT Validation Costs: Stateless JWT validation is computationally cheap: verifying an RS256 signature takes microseconds. However, fetching the public key on every request is expensive. Cache public keys in application memory and refresh periodically (hourly or daily). Most authorization servers expose JWK endpoints for automated key rotation.

At high request volume (10,000+ req/sec), JWT validation CPU usage becomes measurable. Profile your application under load and determine whether JWT validation is a bottleneck before over-optimizing.

Refresh Token Storage Costs: Storing refresh tokens in a database for millions of users creates query load. Every token refresh hits the database to validate the refresh token, then writes a new one. Use indexed queries on token ID and user ID. Consider moving refresh token storage to Redis for sub-millisecond lookup times at the cost of persistence risk (Redis failure loses refresh tokens, forcing re-authentication).

Revocation List Costs: Maintaining a JWT revocation list in Redis is cheap (a few MB for tens of thousands of revoked tokens) but requires TTL management. Revocation list entries should expire after their corresponding tokens would have expired naturally. Without TTL, the revocation list grows unbounded.

Positioning Authentication as Long-Term Infrastructure

Authentication is not a feature you ship once. It is infrastructure that evolves with your product, regulatory environment, and customer expectations.

The teams that build sustainable authentication systems treat it as a distinct service with its own versioning, SLAs, and maintenance schedule. The teams that treat authentication as “something we handle in middleware” end up refactoring it under time pressure when their first enterprise customer demands SSO.

My team has migrated five SaaS platforms off homegrown authentication systems onto proper OAuth and JWT architectures. Every migration took 6-12 weeks and involved coordinating with active customers to prevent service disruption. Every founder said the same thing: “We thought OAuth was overkill for MVP.”

OAuth is not overkill. It is the industry-standard solution to a problem that every SaaS product encounters: controlled delegation of access. Implementing it correctly from the start is faster than migrating later.

JWTs eliminate session state and enable stateless horizontal scaling. For SaaS products, this is not premature optimization. It is fundamental architecture that determines whether you can scale cost-effectively or need to throw hardware at session management problems.

Ready to Build Authentication That Scales With Your Business?

OAuth and JWT implementations live somewhere between “copy code from a tutorial” and “hire a security team for six months.” Most SaaS engineering teams land in the middle: they understand the concepts but need help making architecture decisions that avoid the mistakes covered in this article.

If you are building a new API and need authentication designed correctly from launch, or if you are migrating off a homegrown system and need to avoid downtime, we can help you architect it properly.

Schedule a free consultation and walk us through your architecture. We will identify the specific security and scalability risks in your current or planned implementation, recommend the OAuth grant types that fit your client types, and give you a clear path to production-ready authentication.

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top