JJWT Decoder
12 min read

OAuth 2.0 and JWT: How They Work Together

Understand how OAuth 2.0 and JWT work together for secure authorization. Learn about access tokens, ID tokens, and OpenID Connect.

In This Article

OAuth 2.0 and JWT: Two Standards That Work Together

OAuth 2.0 and JWT are often mentioned together, but they serve different purposes. OAuth 2.0 is an authorization framework that defines how applications can obtain access to resources on behalf of a user. JWT is a token format — a compact, self-contained way to encode claims. Understanding how they work together is essential for building modern authentication systems.

If you're new to JWT, start with our What is JWT guide. For a comparison with traditional sessions, see our JWT vs Session Authentication article.

OAuth 2.0 in a Nutshell

OAuth 2.0 (RFC 6749) defines four roles:

  • Resource Owner: The user who authorizes access
  • Client: The application requesting access
  • Authorization Server: Issues tokens after user consent
  • Resource Server: Hosts protected resources and validates tokens

OAuth 2.0 defines several grant types (authorization flows), but the most relevant for web and mobile apps are:

  1. Authorization Code Flow — the most secure, recommended for server-side apps
  2. Authorization Code + PKCE — for mobile apps and SPAs
  3. Client Credentials — for machine-to-machine communication

Where JWT Fits in OAuth 2.0

OAuth 2.0 doesn't mandate a specific token format. It defines the flows and interactions, but the actual access token can be opaque (a random string) or structured (a JWT). When JWT is used as the access token format in OAuth 2.0, you get several benefits:

  • Self-contained: The resource server can verify the token without calling the authorization server
  • Rich claims: The token carries user identity, scopes, and metadata
  • Standardized structure: Header, payload, and signature follow RFC 7519

Access Token vs ID Token

A common source of confusion is the difference between access tokens and ID tokens:

| Property | Access Token | ID Token |

|----------|-------------|----------|

| Purpose | Authorize API access | Prove user identity |

| Audience | Resource server | Client application |

| Read by | API / Resource server | Client (frontend) only |

| Contains | Scopes, permissions | User profile claims |

| Sent to | API endpoints | Never sent to APIs |

The access token is meant for APIs — it tells the resource server what the user is allowed to do. The ID token is meant for the client — it proves who the user is and comes from OpenID Connect.

Access Token Example

{
  "sub": "user123",
  "scope": "read:profile write:orders",
  "iss": "https://auth.example.com",
  "aud": "https://api.example.com",
  "exp": 1720630800
}

ID Token Example

{
  "sub": "user123",
  "name": "John Doe",
  "email": "john@example.com",
  "iss": "https://auth.example.com",
  "aud": "my-client-app-id",
  "exp": 1720630800,
  "nonce": "abc123"
}

OpenID Connect: Adding Identity to OAuth 2.0

OAuth 2.0 handles authorization, but it doesn't define authentication. That's where OpenID Connect (OIDC) comes in. OIDC is a thin layer on top of OAuth 2.0 that adds:

  • An ID Token (JWT) that proves user identity
  • A UserInfo endpoint that returns user profile data
  • Discovery endpoints for automatic configuration

When you "Sign in with Google" or "Login with GitHub", you're using OpenID Connect on top of OAuth 2.0.

Complete OAuth 2.0 + JWT Flow

Here's a complete implementation of the Authorization Code flow with JWT tokens:

const express = require('express');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

const app = express();

const AUTH_SERVER_URL = 'https://auth.example.com';
const CLIENT_ID = process.env.CLIENT_ID;
const CLIENT_SECRET = process.env.CLIENT_SECRET;
const REDIRECT_URI = 'https://myapp.com/callback';

// Step 1: Redirect user to authorization server
app.get('/login', (req, res) => {
  const state = crypto.randomUUID();
  req.session.oauthState = state;

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'openid profile email',
    state: state
  });

  res.redirect(`${AUTH_SERVER_URL}/authorize?${params}`);
});

// Step 2: Handle callback with authorization code
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state parameter (CSRF protection)
  if (state !== req.session.oauthState) {
    return res.status(403).json({ error: 'Invalid state' });
  }

  // Step 3: Exchange code for tokens
  const tokenResponse = await fetch(`${AUTH_SERVER_URL}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      grant_type: 'authorization_code',
      code,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    })
  });

  const tokens = await tokenResponse.json();

  // tokens contains:
  // {
  //   access_token: "eyJ...",     (JWT for API access)
  //   id_token: "eyJ...",        (JWT with user identity)
  //   refresh_token: "rt_...",   (opaque token for refresh)
  //   token_type: "Bearer",
  //   expires_in: 3600
  // }

  // Decode ID token for user info (DO NOT trust without verification)
  const idToken = jwt.decode(tokens.id_token);

  // Store tokens securely
  req.session.accessToken = tokens.access_token;
  res.cookie('refreshToken', tokens.refresh_token, {
    httpOnly: true, secure: true, sameSite: 'strict'
  });

  res.redirect('/dashboard');
});

// Step 4: Use access token to call API
app.get('/api/profile', async (req, res) => {
  const response = await fetch('https://api.example.com/userinfo', {
    headers: { 'Authorization': `Bearer ${req.session.accessToken}` }
  });
  const profile = await response.json();
  res.json(profile);
});

Authorization Server: Issuing JWT Tokens

On the authorization server side, here's how JWT tokens are generated:

function issueOAuthTokens(user, scope) {
  const now = Math.floor(Date.now() / 1000);

  // Access token — for API access
  const accessToken = jwt.sign(
    {
      sub: user.id,
      scope: scope,
      iss: 'https://auth.example.com',
      aud: 'https://api.example.com',
      iat: now,
      exp: now + 3600,
      jti: crypto.randomUUID()
    },
    privateKey,
    { algorithm: 'RS256', keyid: 'key-1' }
  );

  // ID token — for client authentication
  const idToken = jwt.sign(
    {
      sub: user.id,
      name: user.name,
      email: user.email,
      iss: 'https://auth.example.com',
      aud: CLIENT_ID,
      iat: now,
      exp: now + 3600,
      nonce: user.nonce
    },
    privateKey,
    { algorithm: 'RS256', keyid: 'key-1' }
  );

  // Refresh token — opaque, stored server-side
  const refreshToken = crypto.randomUUID();
  storeRefreshToken(refreshToken, { userId: user.id, scope, createdAt: now });

  return { access_token: accessToken, id_token: idToken, refresh_token: refreshToken, token_type: 'Bearer', expires_in: 3600 };
}

Common Misunderstandings

"OAuth 2.0 and JWT are the same thing"

They're not. OAuth 2.0 is a framework for authorization flows. JWT is a token format. You can use OAuth 2.0 without JWT (opaque tokens) and JWT without OAuth 2.0 (custom authentication).

"ID tokens should be sent to APIs"

Never send ID tokens to APIs. ID tokens are for the client application only. APIs should receive access tokens, which contain authorization scopes, not personal identity claims.

"JWT tokens in OAuth must be encrypted"

JWT tokens in OAuth 2.0 are typically signed but not encrypted. The claims are readable but tamper-proof. If you need to hide claims, use JWE (JSON Web Encryption) — but this is rarely needed for access tokens.

"The client should validate the access token"

The client should treat the access token as opaque. Only the resource server validates it. The client should only validate the ID token.

Conclusion

OAuth 2.0 and JWT are complementary standards. OAuth 2.0 defines how to obtain tokens, and JWT defines the token format. Together with OpenID Connect, they form the foundation of modern authentication. Use our JWT decoder to inspect your OAuth tokens and understand what claims they contain. For securing your JWT implementation, refer to our JWT security best practices.

Need to decode a JWT token? Try our free JWT decoder tool — no sign-up required, runs entirely in your browser.