Understanding the Structure of a JWT Token
Every JSON Web Token follows a simple three-part structure:
HEADER.PAYLOAD.SIGNATURE
A typical JWT token looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJhZG1pbiIsImV4cCI6MTcwMDAwMDAwMH0.SflKxwRJSMeKKF2QT4fwpMeJ36POk6yJV_adQssw5c
These three sections are separated using dots and each component serves a specific purpose.
| Component | Contains | Example |
|---|
| Header | Algorithm and token type | {“alg”:”HS256″,”typ”:”JWT”} |
| Payload | Claims such as user information and expiry | {“user_id”:123,”role”:”admin”,”exp”:1700000000} |
| Signature | Cryptographic signature | Prevents token tampering |
The header specifies:
- Which signing algorithm is being used.
- The type of token being generated.
{
"alg":"HS256",
"typ":"JWT"
}Payload
The payload stores claims or information associated with the user.
Common examples include:
{
"user_id":123,
"role":"admin",
"exp":1700000000
}Payload data may contain:
- User IDs
- Roles and permissions
- Token expiry time
- Issuer information
- Audience information
It’s important to understand that JWT payloads are encoded but not encrypted. Anyone possessing the token can decode its contents.
For this reason, you should never store:
- Passwords
- Credit card information
- Banking details
- Sensitive personally identifiable information
JWT payloads should only contain information that can safely be viewed if the token becomes exposed.
Common JWT Claims Used in Python Applications
JWT provides several standardized claims that simplify Python JWT authentication systems.
| Claim | Meaning |
|---|
| sub | Subject (User ID) |
| exp | Expiration time |
| iat | Issued At |
| nbf | Not Before |
| iss | Issuer |
| aud | Audience |
Let’s briefly understand them.
sub (Subject)
Identifies the user associated with the token.
"sub":"42"
exp (Expiration)
Defines when the token becomes invalid.
"exp":1700000000
Short-lived access tokens significantly improve application security.
iat (Issued At)
Indicates when the JWT token was generated.
"iat":1700000000
nbf (Not Before)
Specifies the earliest time at which a token becomes valid.
iss (Issuer)
Defines the entity responsible for issuing the token.
"iss":"api.example.com"
aud (Audience)
Indicates which application or service should accept the token.
These standardized claims make token-based authentication in Python highly interoperable across different services and frameworks.
How JWT Authentication Works in Python
The workflow of JWT authentication is straightforward once you understand the sequence of events.
Step 1 – User Login
The user submits credentials such as username and password to the application’s login endpoint.
Step 2 – Credential Verification
The server validates:
- Username
- Password
- Account status
- Access permissions
against the application’s user database.
Step 3 – Generate JWT Token in Python
After successful authentication, the server creates a signed JWT containing information such as User ID, Role, Expiration Time, and Issued Time.
This signed token is then returned to the client application.
Step 4 – Client Stores the Token
The client stores the JWT securely using:
- Browser storage mechanisms
- Secure mobile storage
- HTTP-only cookies
- In-memory storage for enhanced security
Step 5 – Sending Authenticated Requests
Whenever protected resources are requested, the token is sent using the Authorization header.
Authorization: Bearer
This mechanism makes JWT one of the most commonly used solutions for secure API authentication in Python applications.
Step 6 – Token Verification
Upon receiving the request, the server performs several checks:
- Decodes the token.
- Verifies the cryptographic signature.
- Checks token expiration.
- Validates the signing algorithm.
- Confirms issuer and audience information if required.
Only valid tokens are granted access to protected endpoints.
The entire process eliminates the need for maintaining server-side session state, making JWT particularly suitable for scalable REST APIs and microservices architectures.
Setting Up PyJWT in Python
After understanding how JWT authentication works, the next step is implementing it in Python. The most widely used library for working with JSON Web Tokens is PyJWT. It provides a simple and secure API for generating, verifying, and decoding JWT tokens while supporting multiple cryptographic algorithms such as HS256, RS256, and ES256.
PyJWT integrates seamlessly with popular Python frameworks including:
- Flask
- FastAPI
- Django
- Tornado
- Microservices-based applications
If you’re learning Python JWT authentication for REST APIs or secure web applications, PyJWT is generally the recommended choice because of its active maintenance and straightforward implementation.
Installing PyJWT
Installing PyJWT requires only a single command.
pip install PyJWT
If your application uses RSA or Elliptic Curve algorithms such as RS256 or ES256, install the cryptography package as well.
pip install PyJWT[cryptography]
Once installed successfully, verify the installation using Python.
import jwt
print(jwt.__version__)
A successful installation should display version information similar to:
2.x.x
PyJWT supports both symmetric and asymmetric cryptographic algorithms, making it suitable for everything from small projects to enterprise-level authentication systems.
PyJWT vs python-jose
While learning JWT in Python, you may come across another library called python-jose. Both libraries support JSON Web Tokens, but there are some practical differences.
| Feature | PyJWT | python-jose |
|---|
| Ease of Use | Excellent | Good |
| Documentation | Excellent | Good |
| FastAPI Support | Recommended | Supported |
| Maintenance | Active | Active |
| Beginner Friendly | Yes | Moderate |
| Common Usage | High | Moderate |
PyJWT is frequently recommended because it offers:
- Cleaner APIs
- Better documentation
- Excellent framework compatibility
- Easy JWT token generation and verification
Unless your project specifically requires JOSE-related features, PyJWT is usually the preferred choice for implementing token-based authentication in Python applications.
Generating a JWT Token in Python
Creating a JWT token with PyJWT is remarkably simple. You provide:
- A payload dictionary
- A secret key
- A signing algorithm
PyJWT handles the remaining processes, including:
- Encoding
- Signing
- Expiration conversion
- Token generation
Python Example – Generate a Signed JWT Token
import jwt
import datetime
SECRET_KEY = "your-super-secret-key-keep-this-safe"
ALGORITHM = "HS256"
def create_token(user_id: int, role: str) -> str:
payload = {
"sub": str(user_id),
"role": role,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(
payload,
SECRET_KEY,
algorithm=ALGORITHM
)
return token
# Usage
token = create_token(
user_id=42,
role="admin"
)
print(token)
# Example output:
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.....This implementation creates a signed JWT containing:
- Subject (sub)
- User role
- Issued time (iat)
- Expiration time (exp)
These are among the most commonly used claims for Python JWT authentication systems.
Understanding JWT Claims Used During Token Generation
Let’s examine the payload more closely.
payload = {
"sub": str(user_id),
"role": role,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}Each field serves a specific purpose.
Subject (sub)
Identifies the user associated with the token.
"sub":"42"
User Role
Stores role-based authorization information.
"role":"admin"
This allows applications to implement:
- Role-based access control
- Permission management
- API authorization
Issued At (iat)
Defines when the token was created.
"iat": datetime.datetime.utcnow()
Expiration (exp)
Determines how long the token remains valid.
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
PyJWT automatically converts datetime objects into Unix timestamps internally, simplifying token management considerably.
Never Hardcode Your Secret Keys
One of the most common security mistakes beginners make is storing secret keys directly within source code.
Avoid this approach:
SECRET_KEY = "my-secret-password"
Instead, use environment variables.
import os
SECRET_KEY = os.environ.get("JWT_SECRET")Using environment variables provides several advantages:
- Improved security
- Easier deployment
- Better secret management
- Reduced risk of accidental exposure
A compromised secret key allows attackers to generate valid tokens for any user in your system. Protecting it should always be a top priority when implementing secure API authentication in Python.
Verifying and Decoding JWT Tokens in Python
Every protected API endpoint must verify incoming tokens before granting access.
Fortunately, PyJWT performs several operations automatically when decoding a token.
It will:
- Decode Base64URL data.
- Verify the token signature.
- Validate expiration time.
- Verify allowed algorithms.
- Raise exceptions when validation fails.
Python Example – Verify a JWT Token
import jwt
from jwt.exceptions import (
ExpiredSignatureError,
InvalidTokenError
)
SECRET_KEY = "your-super-secret-key-keep-this-safe"
ALGORITHM = "HS256"
def verify_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM]
)
return payload
except ExpiredSignatureError:
raise ValueError(
"Token has expired. Please log in again."
)
except InvalidTokenError:
raise ValueError(
"Invalid token. Authentication failed."
)
# Usage
payload = verify_token(token)
print(payload["sub"])
print(payload["role"])
# Example output:
# 42
# adminThis single function performs multiple security checks automatically and is one of the reasons PyJWT is widely adopted for secure token-based authentication systems.
Why Is algorithms=[ALGORITHM] Written as a List?
Many beginners wonder why PyJWT requires the algorithm parameter to be provided as a list.
algorithms=["HS256"]
This is a deliberate security feature.
PyJWT forces developers to explicitly specify which algorithms are acceptable during token verification. This prevents attackers from modifying token headers to bypass signature verification.
Never allow token headers to determine which algorithm should be trusted automatically.
Always whitelist accepted algorithms such as:
algorithms=["HS256"]
or
algorithms=["RS256"]
Explicit algorithm validation significantly improves the security of Python JWT authentication implementations.

Handling JWT Expiry and Common Errors in Python
JWT token verification involves more than simply checking whether a token exists. Production-ready applications must handle various failure scenarios gracefully, including expired tokens, malformed token structures, invalid signatures, and incorrect audiences.
Fortunately, PyJWT provides built-in exceptions that simplify comprehensive JWT validation in Python applications. Proper exception handling significantly improves both security and user experience by allowing developers to respond appropriately whenever token validation fails.
Common situations that must be handled include:
- Expired JWT tokens
- Invalid cryptographic signatures
- Corrupted token formats
- Incorrect audiences
- Failed token verification
- Unauthorized API access attempts
Implementing robust error handling is considered a fundamental JWT security best practice for production environments.
Comprehensive JWT Error Handling in Python
PyJWT offers multiple exceptions that make token validation straightforward and secure.
Python Example – Comprehensive Token Validation
import jwt
from jwt.exceptions import (
ExpiredSignatureError,
InvalidSignatureError,
DecodeError,
InvalidTokenError,
)
def decode_token(token: str):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM]
)
return payload
except ExpiredSignatureError:
print(
"Token expired — ask client to refresh or re-login"
)
return None
except InvalidSignatureError:
print(
"Signature mismatch — token may have been tampered with"
)
return None
except DecodeError:
print(
"Malformed token — couldn't parse the structure"
)
return None
except InvalidTokenError as e:
print(
f"Token error: {e}"
)
return NoneThis implementation covers most authentication failures encountered in real-world Python applications.
Understanding Common JWT Exceptions
| Exception | When It Occurs | Typical Cause |
|---|
| ExpiredSignatureError | Token expiration time has passed | Access token has expired |
| InvalidSignatureError | Signature verification fails | Wrong secret key or modified token |
| DecodeError | Token format is invalid | Corrupted or malformed JWT |
| InvalidAudienceError | Audience claim validation fails | Token intended for another service |
| InvalidTokenError | General token verification failure | Invalid JWT token |
These exceptions allow applications to provide meaningful responses instead of generic authentication failures.
Why Proper Expiry Handling Matters
One of the biggest advantages of JWT authentication in Python is automatic expiration validation.
For example, consider the following token:
{
"sub":"42",
"exp":1700000000
}If the expiration timestamp has already passed, PyJWT immediately raises an exception without requiring developers to implement custom date comparisons.
Avoid approaches such as:
if token_expiry < current_time:
print("Token expired")Instead, allow PyJWT to perform expiration checks automatically.
jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"]
)This approach reduces:
- Timezone-related issues
- Custom validation errors
- Security vulnerabilities
- Incorrect expiration handling
Using built-in validation mechanisms is one of the recommended JWT security best practices for Python developers.
Building JWT Authentication in a Flask API
JWT tokens are extensively used for protecting REST APIs developed using Flask.
A typical Flask JWT authentication system contains:
- Login endpoints
- Token generation mechanisms
- Protected API routes
- Token validation decorators
- Authorization middleware
Let’s build a complete example.
Import Required Libraries
from flask import (
Flask,
request,
jsonify
)
import jwt
import datetime
import osNext, configure the Flask application.
app = Flask(__name__)
SECRET_KEY = os.environ.get(
"JWT_SECRET",
"dev-secret-change-in-prod"
)
ALGORITHM = "HS256"Using environment variables is strongly recommended when deploying production applications.
Creating a Simulated User Store
For demonstration purposes, we’ll create a simple user store.
USERS = {
"naveen@iies.in": {
"password":"hashed_pw_here",
"role":"instructor"
}
}Production applications should replace this implementation with:
- MySQL
- PostgreSQL
- MongoDB
- Cloud-based authentication services
Never store plain-text passwords inside production databases.
Creating JWT Tokens for Users
The login endpoint will generate signed tokens after successful authentication.
def create_token(email, role):
payload = {
"sub": email,
"role": role,
"iat":
datetime.datetime.utcnow(),
"exp":
datetime.datetime.utcnow()
+ datetime.timedelta(hours=2)
}
return jwt.encode(
payload,
SECRET_KEY,
algorithm=ALGORITHM
)This implementation provides:
- User identification
- Role-based authorization
- Expiration management
- Cryptographic token signing
JWT tokens generated using HS256 are ideal for small to medium-sized applications that use shared secret keys securely.
Protecting API Routes Using Decorators
One of Flask’s most powerful features is the ability to use decorators for authentication.
The following decorator validates every incoming JWT token before granting access.
def token_required(f):
from functools import wraps
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get(
"Authorization",
""
)
if not auth.startswith(
"Bearer "
):
return jsonify(
{
"error":
"Missing or invalid token"
}
), 401
token = auth.split(" ")[1]
try:
request.current_user = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM]
)
except jwt.ExpiredSignatureError:
return jsonify(
{
"error":"Token expired"
}
), 401
except jwt.InvalidTokenError:
return jsonify(
{
"error":"Invalid token"
}
), 401
return f(*args, **kwargs)
return decoratedThe decorator performs multiple responsibilities automatically:
- Retrieves Authorization headers.
- Extracts Bearer tokens.
- Verifies JWT signatures.
- Validates token expiration.
- Rejects invalid requests.
- Stores authenticated user information.
This design pattern is commonly used in secure Flask JWT authentication systems.
Creating the Login Endpoint
Users authenticate through the login route.
@app.route(
"/login",
methods=["POST"]
)
def login():
data = request.get_json()
email = data.get("email")
pw = data.get("password")
user = USERS.get(email)
if not user or user["password"] != pw:
return jsonify(
{
"error":
"Invalid credentials"
}
), 401
token = create_token(
email,
user["role"]
)
return jsonify(
{
"token":token
}
)After successful authentication, the API returns a signed JWT token that can be used for future requests.
Creating Protected Routes
Protected endpoints simply apply the authentication decorator.
@app.route(
"/profile",
methods=["GET"]
)
@token_required
def profile():
u = request.current_user
return jsonify(
{
"email":u["sub"],
"role":u["role"],
"message":
"You are authenticated!"
}
)Only authenticated users possessing valid JWT tokens can successfully access this route.
Finally, start the Flask application.
if __name__ == "__main__":
app.run(debug=True)Developers can test the implementation by:
- Sending credentials to /login.
- Receiving the generated JWT token.
- Passing the token inside the Authorization header.
- Accessing protected endpoints securely.
This pattern forms the foundation of token-based authentication systems used extensively in Flask APIs and modern microservices architectures.

JWT Security Pitfalls Every Python Developer Should Know
JWT authentication is simple to implement, but many real-world security vulnerabilities occur because of improper token handling. Understanding these issues is just as important as learning how to generate and verify JWT tokens in Python.
Whether you’re building Flask APIs, FastAPI applications, or microservices, following JWT security best practices will significantly improve the overall security of your application.
Some of the most common JWT-related mistakes include:
- Allowing insecure algorithms.
- Using long-lived access tokens.
- Storing tokens insecurely.
- Using weak secret keys.
- Skipping built-in expiration validation.
- Including sensitive information inside token payloads.
- Failing to revoke compromised tokens.
Let’s look at each of these in detail.
The alg:none Attack
One of the earliest JWT vulnerabilities involved the alg:none algorithm.
Attackers could modify the token header as follows:
{
"alg":"none",
"typ":"JWT"
}If applications blindly trusted the token header, signature verification could be bypassed entirely.
Fortunately, PyJWT prevents this issue by forcing developers to explicitly whitelist acceptable algorithms.
Always use:
jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"]
)
Avoid implementations that automatically trust algorithms specified inside JWT headers.
Secure implementations should explicitly define acceptable algorithms such as:
algorithms=["HS256"]
or
algorithms=["RS256"]
Never allow users or clients to determine which algorithms should be accepted.
Access Tokens and Refresh Tokens
Many beginners create JWT tokens that remain valid for several days or even weeks.
This is strongly discouraged.
A recommended strategy is:
| Token Type | Recommended Lifetime |
|---|
| Access Token | 15 minutes – 2 hours |
| Refresh Token | Several days or weeks |
| Password Reset Token | Few minutes |
Access tokens should remain short-lived. If an attacker compromises a token, its usefulness becomes limited because it expires quickly.
A typical authentication workflow looks like this:
User Login
↓
Generate Access Token
↓
Generate Refresh Token
↓
Access Token Expires
↓
Use Refresh Token
↓
Issue New Access TokenUsing refresh tokens improves both:
This approach is widely adopted by modern API authentication systems.
Avoid Storing Tokens in Local Storage
Many JavaScript applications store JWT tokens inside:
- Local Storage
- Session Storage
Although convenient, both approaches become vulnerable if your application suffers from Cross-Site Scripting (XSS) attacks.
Attackers can potentially access:
localStorage.getItem("token");and steal authentication tokens.
For web applications, consider using:
- HTTP-only cookies
- Secure cookies
- SameSite cookie policies
Mobile applications should use:
- Secure device storage
- Operating system keychains
- Encrypted storage mechanisms
Secure token storage is an essential part of implementing JWT authentication securely in production environments.
Token Revocation Challenges
JWT is stateless by design.
This means servers typically do not maintain information regarding issued tokens.
Consider the following situation:
User Logs In
↓
JWT Issued
↓
User Changes Password
↓
Token Still Valid
↓
Access Continues Until ExpiryUnless token revocation mechanisms exist, compromised tokens remain usable until expiration.
Common solutions include:
- Redis blocklists
- Database-backed token revocation lists
- Refresh token rotation
- Logout token invalidation
Many production systems maintain revoked token identifiers (jti) inside Redis for efficient token invalidation.
Although JWT is stateless, token revocation mechanisms can provide additional security whenever immediate access termination becomes necessary.
Always Use Strong Secret Keys
Weak secret keys remain one of the most dangerous JWT implementation mistakes.
Avoid secrets such as:
SECRET_KEY = "password123"
or
SECRET_KEY = "my-secret"
Instead, generate sufficiently random keys.
python -c "import secrets; print(secrets.token_hex(32))"
A randomly generated 32-character secret substantially improves protection against brute-force attacks.
Additionally:
- Store secrets using environment variables.
- Never commit secrets to Git repositories.
- Rotate secrets periodically.
- Restrict production environment access.
Proper secret management forms the foundation of secure JWT authentication in Python applications.
Never Store Sensitive Information Inside JWT Payloads
JWT payloads are:
- Encoded
- Signed
- Easily readable
They are not encrypted by default.
Anyone possessing the token can decode its contents.
Bad examples include:
{
"password":"mypassword",
"credit_card":"1234-5678",
"otp":"456123"
}Better alternatives include:
{
"sub":"42",
"role":"admin"
}Safe information commonly stored inside JWT payloads includes:
- User IDs
- Email addresses
- Roles
- Token expiration times
- Issuer information
Sensitive information should always remain inside secured databases rather than authentication tokens.
Quick Reference Cheat Sheet
| Task | Implementation |
|---|
| Install PyJWT | pip install PyJWT |
| Generate Token | jwt.encode() |
| Verify Token | jwt.decode() |
| Set Expiration | datetime.timedelta(hours=1) |
| Handle Expired Tokens | ExpiredSignatureError |
| Handle Invalid Tokens | InvalidTokenError |
| Algorithm Validation | algorithms=[“HS256”] |
| Authorization Header | Bearer |
JWT tokens follow the structure:
HEADER.PAYLOAD.SIGNATURE
Some important points to remember are:
- JWT payloads are readable.
- Never expose secret keys publicly.
- Use short-lived access tokens.
- Implement refresh token mechanisms whenever required.
- Always validate token expiration.
- Protect API endpoints using token verification.
- Explicitly whitelist acceptable algorithms.
- Store tokens securely in production environments.
Conclusion
JWT in Python offers a secure, scalable, and efficient solution for implementing token-based authentication in modern applications. From Flask APIs and microservices to IoT systems and cloud-based architectures, JSON Web Tokens simplify authentication while eliminating the need for maintaining server-side sessions.
Throughout this tutorial, we explored how JWT tokens are structured, how PyJWT generates and verifies tokens, how Flask JWT authentication works, and which security best practices should be followed in production environments.
When implemented correctly, JWT authentication provides:
- Secure API access control.
- Stateless authentication mechanisms.
- Improved scalability.
- Better developer flexibility.
- Seamless integration with Python frameworks.
The most important takeaway is that security should always remain a priority. Short-lived access tokens, strong secret keys, proper token storage, and comprehensive validation mechanisms collectively contribute to building robust authentication systems.
The best way to strengthen your understanding is through hands-on practice. Build the Flask example discussed earlier, inspect generated JWT tokens, experiment with expiration handling, and observe how PyJWT responds to modified or invalid signatures. Practical experimentation remains one of the most effective ways to master JWT authentication in Python.
