OAuth2 JWT Profile — Using JSON Web Tokens as OAuth2 Access Tokens
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
- What advantage do JWT access tokens have over opaque tokens?
- How does private_key_jwt authentication work?
- What is the JWKS endpoint used for?
- How do you handle token revocation with JWT access tokens?
- What claims should a JWT access token contain?
Answers:
- JWTs are self-contained — resource servers validate them locally without calling the authorization server.
- The client creates a signed JWT assertion with its client_id and sends it instead of a client_secret.
- The JWKS endpoint publishes public keys that resource servers use to verify JWT signatures.
- JWTs remain valid until expiry. Use short TTLs or a token blacklist for revocation.
sub,iss,aud,exp,iat,jti,scope, and optionallyclient_idandroles.
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
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