Skip to content

OAuth2 JWT Profile — Using JSON Web Tokens as OAuth2 Access Tokens

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about OAuth2 JWT Profile. We cover key concepts, practical examples, and best practices to help you master this topic.

The OAuth2 JWT profile defines how to use JSON Web Tokens as structured access tokens, enabling self-contained authorization data, JWT-based client authentication (private_key_jwt), and JWT bearer token assertions for federated authorization.

What You'll Learn

  • JWT as structured access tokens in OAuth2
  • JWT-based client authentication (private_key_jwt)
  • JWT bearer grant for token exchange
  • JWKS integration with authorization server
  • Validating JWT access tokens in resource servers

Why It Matters

Structured JWT access tokens eliminate token introspection requests — resource servers validate tokens locally. DodaTech's API Gateway validates 10M+ tokens daily without calling the authorization server, reducing latency and eliminating a single point of failure.

Real-World Use

An OAuth2 authorization server issues JWT access tokens containing user roles, tenant ID, and permissions. Resource servers validate these tokens locally using the authorization server's JWKS endpoint, eliminating the need for token introspection on every request.

flowchart LR
    A["Client"] -->|"Authorization Request"| B["Auth Server"]
    B -->|"JWT Access Token"| A
    A -->|"Request + JWT"| C["Resource Server 1"]
    A -->|"Request + JWT"| D["Resource Server 2"]
    A -->|"Request + JWT"| E["Resource Server 3"]
    B -->|"Publishes JWKS"| F["https://auth.dodatech.com/.well-known/jwks.json"]
    C -->|"Validates locally"| F
    D -->|"Validates locally"| F
    E -->|"Validates locally"| F

Code Examples

Example 1: OAuth2 Authorization Server Issuing JWT Tokens

from flask import Flask, request, jsonify
import jwt
from datetime import datetime, timedelta, timezone

app = Flask(__name__)
PRIVATE_KEY = load_private_key()

@app.route('/token', methods=['POST'])
def token_endpoint():
    grant_type = request.form.get('grant_type')

    if grant_type == 'authorization_code':
        code = request.form.get('code')
        auth_session = validate_auth_code(code)

        access_token = create_jwt_token(auth_session)

        return jsonify({
            'access_token': access_token,
            'token_type': 'Bearer',
            'expires_in': 900,
            'scope': ' '.join(auth_session['scope'])
        })

    elif grant_type == 'client_credentials':
        client_id = request.form.get('client_id')
        client = authenticate_client(request)

        access_token = create_jwt_token({
            'sub': client_id,
            'roles': client.roles,
            'scope': request.form.get('scope', '').split()
        })

        return jsonify({'access_token': access_token, 'token_type': 'Bearer'})

def create_jwt_token(session):
    now = datetime.now(timezone.utc)
    payload = {
        'sub': session.get('sub') or session.get('client_id'),
        'iss': 'https://auth.dodatech.com',
        'aud': 'https://api.dodatech.com',
        'exp': now + timedelta(minutes=15),
        'iat': now,
        'jti': str(uuid.uuid4()),
        'client_id': session.get('client_id'),
        'scope': session.get('scope', []),
        'roles': session.get('roles', [])
    }
    return jwt.encode(payload, PRIVATE_KEY, algorithm='RS256')

Example 2: JWT Client Authentication (private_key_jwt)

import jwt
from datetime import datetime, timedelta, timezone

def create_client_assertion(client_id, private_key, token_url):
    """Create JWT client assertion for private_key_jwt auth."""
    now = datetime.now(timezone.utc)
    assertion = {
        'iss': client_id,
        'sub': client_id,
        'aud': token_url,
        'exp': now + timedelta(minutes=5),
        'iat': now,
        'jti': str(uuid.uuid4())
    }
    return jwt.encode(assertion, private_key, algorithm='RS256')

# Authenticate with authorization server
client_assertion = create_client_assertion(
    CLIENT_ID,
    CLIENT_PRIVATE_KEY,
    'https://auth.dodatech.com/token'
)

response = requests.post(
    'https://auth.dodatech.com/token',
    data={
        'grant_type': 'client_credentials',
        'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
        'client_assertion': client_assertion,
        'scope': 'read:threats write:reports'
    }
)

print(f"Access token: {response.json()['access_token'][:50]}...")
# Output: Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Example 3: Local JWT Validation in Resource Server

from jwt import PyJWKClient
import jwt

class JWTResourceServer:
    def __init__(self, jwks_url, expected_audience, expected_issuer):
        self.jwks_client = PyJWKClient(jwks_url)
        self.audience = expected_audience
        self.issuer = expected_issuer

    def validate_token(self, token):
        try:
            signing_key = self.jwks_client.get_signing_key_from_jwt(token)
            payload = jwt.decode(
                token,
                signing_key.key,
                algorithms=['RS256'],
                audience=self.audience,
                issuer=self.issuer,
                options={
                    'require': ['sub', 'iss', 'aud', 'exp', 'iat', 'jti', 'scope'],
                    'verify_exp': True
                }
            )
            return payload
        except jwt.ExpiredSignatureError:
            print("Token expired — request new from auth server")
        except jwt.InvalidAudienceError:
            print("Token not intended for this resource server")
        except Exception as e:
            print(f"Validation failed: {e}")
        return None

# Usage
validator = JWTResourceServer(
    'https://auth.dodatech.com/.well-known/jwks.json',
    'https://api.dodatech.com',
    'https://auth.dodatech.com'
)
payload = validator.validate_token(access_token)
if payload:
    print(f"Authenticated: {payload['sub']}, Scopes: {payload['scope']}")

Common Mistakes

1. Not Caching JWKS Responses

Fetching JWKS on every request adds latency. Cache with a reasonable TTL (e.g., 1 hour).

2. Using HS256 in OAuth2 Contexts

HS256 requires shared secrets across all resource servers. Use RS256 or ES256 so each resource server only needs the public key.

3. Ignoring aud Claim in JWT Tokens

A JWT issued for one resource server must not be usable at another. Validate the aud claim.

4. Not Handling Key Rotation

JWKS keys change. Handle kid not found by fetching the JWKS fresh and retrying validation.

5. Mixing JWT and Opaque Tokens

Decide on one token format per authorization server. Supporting both adds complexity.

Practice Questions

  1. What advantage do JWT access tokens have over opaque tokens?
  2. How does private_key_jwt authentication work?
  3. What is the JWKS endpoint used for?
  4. How do you handle token revocation with JWT access tokens?
  5. What claims should a JWT access token contain?

Answers:

  1. JWTs are self-contained — resource servers validate them locally without calling the authorization server.
  2. The client creates a signed JWT assertion with its client_id and sends it instead of a client_secret.
  3. The JWKS endpoint publishes public keys that resource servers use to verify JWT signatures.
  4. JWTs remain valid until expiry. Use short TTLs or a token blacklist for revocation.
  5. sub, iss, aud, exp, iat, jti, scope, and optionally client_id and roles.

Challenge: Build an OAuth2 authorization server that issues JWT access tokens, supports private_key_jwt client authentication, and publishes a JWKS endpoint. Build a resource server that validates tokens locally.

FAQ

Can I use both JWT and opaque tokens from the same auth server?

: Yes, but it adds complexity. The token endpoint can return a token_type that indicates the format.

How long should JWKS be cached?

: 1 hour is typical. The max-age directive in the JWKS response headers can guide caching.

What happens when a signing key is compromised?

: Generate a new key pair, update the JWKS, and revoke all tokens signed with the old key.

Is `private_key_jwt` more secure than `client_secret_basic`?

: Yes. Private key authentication avoids transmitting shared secrets and enables key rotation.

Can a resource server validate JWTs without network access?

: Yes, if it has a cached JWKS. This enables offline validation, useful for edge deployments.

What's Next

Explore {{< ilink "OAuth" "OAuth2 Token Exchange" }} for JWT-to-JWT translation, or build a {{< ilink "OAuth" "OAuth2 Resource Server" }} with local JWT validation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro