Skip to content
Jarviix

Tech · 7 min read

OAuth 2.0 and JWT: Authentication Patterns Every Backend Engineer Should Know

OAuth 2.0 flows, JWT structure, when to use each, and the security pitfalls that compromise most implementations.

By Jarviix Engineering · Apr 19, 2026

Authentication security lock
Photo via Unsplash

Authentication and authorization are the foundation of every secure application, and OAuth 2.0 + JWT have become the dominant standards. They're also two of the most commonly misunderstood and misimplemented technologies in modern backends. Subtle errors create serious security vulnerabilities.

This post covers what OAuth 2.0 actually does, the major flows and when to use each, JWT structure and validation, and the implementation pitfalls that compromise security in practice.

The basics

Authentication vs Authorization

  • Authentication (AuthN): who are you?
  • Authorization (AuthZ): what are you allowed to do?

OAuth 2.0 is fundamentally an authorization framework. It says "user X has authorized app Y to access resource Z." It says nothing about who user X is.

OpenID Connect (OIDC) extends OAuth 2.0 to handle authentication ("here's an ID token proving who the user is").

JWT (JSON Web Token) is a token format. Both OAuth and OIDC use JWTs, but JWTs are independent of either standard.

OAuth 2.0 actors

  • Resource Owner: the user who owns data
  • Client: the application requesting access
  • Resource Server: the API serving the data
  • Authorization Server: issues tokens after user consent

Example: User (resource owner) clicks "Sign in with Google" on Spotify (client). Google's auth server (authorization server) shows consent screen. After approval, Spotify gets a token to access Google's user profile API (resource server).

OAuth 2.0 flows (grant types)

Authorization Code Flow (most common)

For server-side web apps and mobile apps with a backend.

  1. Client redirects user to authorization server with client_id, redirect_uri, scope
  2. User authenticates with auth server
  3. User consents to requested permissions
  4. Auth server redirects back to redirect_uri with an authorization code
  5. Client exchanges code for access token (server-to-server, includes client_secret)
  6. Client uses access token to call resource server

Why it's secure: Access token never passes through user's browser. The code-for-token exchange happens server-side with secret authentication.

Use for: Server-side web apps, mobile apps with backend API.

Authorization Code Flow with PKCE

PKCE (Proof Key for Code Exchange) adds protection against code interception attacks. Now the standard for mobile and SPA clients.

  1. Client generates a random code_verifier
  2. Client computes code_challenge = SHA256(code_verifier)
  3. Client sends code_challenge with auth request
  4. Auth server returns authorization code
  5. Client exchanges code + code_verifier for token
  6. Auth server verifies code_challenge matches code_verifier

Use for: Mobile apps, single-page apps (SPAs), CLI tools.

Client Credentials Flow

For server-to-server (no user). Client authenticates with client_id + client_secret to get token.

  1. Client sends credentials to auth server
  2. Auth server returns access token

Use for: Backend services calling other backend services.

Implicit Flow (deprecated)

Used to be standard for SPAs. Now deprecated in favor of Authorization Code with PKCE.

Don't use it for new code.

Resource Owner Password Credentials (deprecated)

Client takes username/password directly and exchanges for token. Removes user consent step.

Don't use it. It defeats most of OAuth's security model.

Device Authorization Flow

For devices with limited input (TVs, printers, IoT). User completes auth on phone/computer; device polls for token.

Examples: TV apps showing "go to https://device.example.com and enter code XYZ123."

Access tokens

OAuth 2.0 doesn't specify token format. Two common approaches:

Opaque tokens

Random strings. Resource server validates by calling auth server. Pros: easy to revoke. Cons: every API call needs validation round-trip.

JWTs (self-contained)

Signed JWTs containing user info and permissions. Resource server validates locally. Pros: no validation round-trip. Cons: can't easily revoke.

Most modern systems use JWTs for performance, accepting the revocation trade-off (or implementing short-lived tokens + refresh tokens).

Refresh tokens

Access tokens expire (usually 15-60 minutes). Refresh tokens (longer-lived, often days/weeks) allow getting new access tokens without re-prompting user.

Pattern:

  1. User authenticates → gets access_token (15 min) + refresh_token (30 days)
  2. Client uses access_token until it expires
  3. Client uses refresh_token to get new access_token
  4. When refresh_token expires, user must re-authenticate

Refresh tokens must be stored securely — they're more sensitive than access tokens.

JWT structure

JWT has three parts separated by dots:

HEADER.PAYLOAD.SIGNATURE

Each part is base64url-encoded.

{
  "alg": "RS256",
  "typ": "JWT"
}

Specifies signing algorithm and token type.

Payload (claims)

{
  "sub": "user-12345",
  "name": "Alice",
  "iat": 1716000000,
  "exp": 1716003600,
  "aud": "my-api",
  "iss": "https://auth.example.com",
  "scope": "read:profile write:posts"
}

Standard claims (sub, iat, exp, aud, iss) plus custom claims.

Signature

Cryptographic signature of header + payload using secret (HMAC) or private key (RSA/ECDSA).

Validating JWTs

Always perform these checks:

  1. Signature valid: token wasn't tampered with
  2. Algorithm matches expected: prevent algorithm confusion attacks
  3. exp not in past: token not expired
  4. nbf not in future: token already valid
  5. iss matches expected issuer: came from trusted source
  6. aud matches your service: token was intended for you
  7. scope includes required permissions: user has authorization

Skipping any of these creates vulnerabilities.

Common JWT vulnerabilities

Algorithm confusion

Application accepts both RS256 and HS256. Attacker submits token signed with HS256 using the public key as secret. Validator doesn't realize and accepts.

Prevention: Hardcode expected algorithm. Reject tokens with different algorithms.

"none" algorithm

Some libraries accept tokens with alg: none (no signature). Trivially forgeable.

Prevention: Reject "none" algorithm explicitly.

Weak HMAC secrets

Short or guessable HMAC secrets can be brute-forced.

Prevention: Use 256-bit random secrets minimum.

Leaked signing keys

JWT signing keys in source code, environment variables checked into git, etc.

Prevention: Secrets management (AWS Secrets Manager, HashiCorp Vault).

No expiration enforcement

Server doesn't check exp claim. Tokens never expire functionally.

Prevention: Always validate exp and reject expired tokens.

Sensitive data in payload

JWT payload is base64-encoded, NOT encrypted. Anyone with the token can read the payload.

Prevention: Don't put PII, passwords, or secrets in JWT payload. Use opaque references that require additional API calls to resolve.

No revocation

Once issued, JWTs can't be invalidated until expiry. If a user logs out or token is compromised, it remains valid.

Mitigation: Short-lived tokens (15 min); maintain a revocation list (defeats statelessness for those checks).

Practical implementation guide

For a typical web app

  1. Use a battle-tested OAuth library (Spring Security OAuth, Passport.js, django-oauth-toolkit)
  2. Use Authorization Code Flow with PKCE
  3. Tokens stored in HTTP-only, Secure, SameSite cookies
  4. Short-lived access tokens (15 min); refresh tokens stored securely
  5. Validate all incoming tokens — signature, expiry, issuer, audience

For an API

  1. Accept JWTs in Authorization: Bearer <token> header
  2. Validate signature against issuer's public key (fetched from JWKS endpoint)
  3. Cache JWKS for performance
  4. Check aud claim matches your service
  5. Check scopes/claims for authorization

For SSO across services

  1. Single auth server issues tokens
  2. Each service validates tokens locally (using JWT)
  3. User signs in once, accesses all services
  4. Use OIDC for the auth flow; JWTs for service-to-service

Common mistakes

  • Implementing OAuth from scratch: use established libraries; OAuth has many subtle requirements
  • Not validating audience: allowing tokens issued for other services
  • Storing JWTs in localStorage: accessible by XSS attacks; use HTTP-only cookies
  • Long-lived access tokens (>1 hour): increases damage from leaked tokens
  • No refresh token rotation: if refresh token leaks, attacker has long-term access
  • Mixing authorization and authentication concepts: confusing what each technology actually does
  • Putting database IDs in JWT payload: enables enumeration attacks; use opaque IDs
  • Single signing key forever: rotate keys periodically

OAuth 2.0 and JWT are powerful when used correctly and dangerous when misimplemented. Don't try to master every nuance from scratch — use proven libraries, follow standard patterns (Authorization Code with PKCE, short-lived tokens, JWT validation per spec), and stay aware of the common pitfalls. Most authentication breaches don't come from breaking the underlying crypto; they come from implementation shortcuts.

Frequently asked questions

What's the difference between OAuth 2.0 and OpenID Connect?

OAuth 2.0 is an authorization framework — it handles 'this user has permitted my app to access their resource.' OpenID Connect (OIDC) is an authentication layer built on top of OAuth 2.0 — it handles 'this is who the user is.' OIDC adds standardized identity claims and an ID token (JWT format) on top of OAuth's access tokens. Most modern 'sign in with Google/GitHub/Microsoft' flows use OIDC, not pure OAuth 2.0.

Should I use JWT for session tokens?

Sometimes. JWTs are useful for stateless authentication where the token contains all needed info — common in microservice and API architectures. The downsides: tokens cannot be invalidated before expiry without server-side state, payload can become large with claims, and JWT crypto mistakes are common security vulnerabilities. For traditional web apps with sessions, server-side session IDs (UUIDs in cookies) backed by Redis are simpler and safer. For APIs and microservices, JWT is the standard.

Are JWTs secure if I sign them?

Signing prevents tampering but not other attacks. Common JWT vulnerabilities: using the 'none' algorithm to bypass signing, accepting RS256 tokens with the public key as HMAC secret (algorithm confusion), insufficient key strength, leaked secrets, no expiration enforcement, and using sensitive data in payload (JWT payload is base64-encoded, not encrypted — anyone can read it). Use battle-tested libraries (jjwt, jsonwebtoken, PyJWT), enforce specific algorithms, and never put secrets in JWT payloads.

Related Jarviix tools

Read paired with the calculator that does the math.

Read next