2300 words
12 minutes
How to Build a Secure Login System with Flask and JWT Authentication

Building a Secure Login System with Flask and JWT Authentication#

A fundamental requirement for many web applications is a robust and secure login system. Traditional session-based authentication relies on server-side state, which can become complex to manage in distributed or microservice architectures. JSON Web Tokens (JWT) offer a stateless approach to authentication, providing scalability and flexibility. Combining the lightweight nature of the Flask web framework with JWT offers an efficient method for implementing secure user authentication.

Flask is a microframework for Python, known for its simplicity and extensibility. It provides the core functionalities needed to build web applications, leaving many decisions about databases, template engines, and authentication to the developer or extensions. This flexibility makes Flask suitable for building APIs and smaller applications where a full-stack framework might be overkill.

JWT (JSON Web Token) is an open standard (RFC 7519) for transmitting information securely between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs consist of three parts separated by dots: Header, Payload, and Signature. The Header typically contains the token type (JWT) and the signing algorithm (e.g., HMAC SHA256, RSA). The Payload contains claims, which are statements about an entity (typically the user) and additional data. Common claims include user ID, username, roles, and expiration time. The Signature is created by taking the encoded Header, the encoded Payload, a secret key, and the algorithm specified in the Header, and signing it. This signature is used to verify that the sender of the JWT is who it says it is and that the message hasn’t been tampered with.

Using JWT for authentication allows the server to issue a token upon successful login. This token is then sent by the client with subsequent requests to access protected resources. The server can verify the token’s validity using the signature and the shared secret key or public key, without needing to query a database for session information on every request.

Essential Concepts for Flask JWT Authentication#

Understanding the core components and processes involved is crucial for building a secure Flask JWT system.

Flask Web Framework Basics#

  • Routing: Flask uses decorators (@app.route('/path')) to map URLs to Python functions. These functions handle incoming requests and return responses.
  • Request Object: Flask provides a global request object that contains incoming request data, such as form data, JSON payloads, query parameters, and headers.
  • Configuration: Application settings, like secret keys or database URIs, are stored in the app.config dictionary.

JSON Web Tokens (JWT) Explained#

  • Structure: Header (metadata), Payload (claims), Signature (verification).
  • Claims: Standard claims (like exp for expiration, sub for subject/user ID) and custom claims (application-specific data).
  • Statelessness: The server does not need to store session state for authenticated users. The token itself contains the necessary information. This is beneficial for scaling horizontally.
  • Verification: The server uses the secret key (for symmetric encryption like HS256) or public key (for asymmetric encryption like RS256) to verify the token’s signature. A valid signature ensures the token hasn’t been altered since it was issued.

The Authentication Flow with JWT#

  1. User Login: The user submits credentials (username, password) to the server.
  2. Server Verification: The server verifies the credentials against stored user data (e.g., checking the hashed password).
  3. Token Issuance: If credentials are valid, the server generates a JWT (typically an access token and often a refresh token) containing user information (like user ID) in the payload.
  4. Token Transmission: The server sends the JWTs back to the client.
  5. Client Storage: The client stores the tokens (e.g., in local storage, session storage, or HTTP-only cookies).
  6. Accessing Protected Resources: For subsequent requests to protected endpoints, the client includes the access token, usually in the Authorization header as a Bearer token (Authorization: Bearer <token>).
  7. Server Validation: The server receives the request, extracts the token, and verifies its signature and expiration time.
  8. Resource Access: If the token is valid, the server processes the request and returns the requested data. If invalid, it rejects the request (e.g., returns 401 Unauthorized).
  9. Token Refresh: When an access token expires, the client uses a refresh token (if issued) to request a new access token from a dedicated refresh endpoint. Refresh tokens typically have a longer lifespan than access tokens.

Security Considerations with JWT#

While statelessness is a benefit, JWTs require careful handling to ensure security.

  • Secret Key Management: The secret key used to sign tokens must be kept confidential on the server. Compromising this key allows attackers to forge tokens.
  • HTTPS/SSL/TLS: Tokens should always be transmitted over encrypted connections (HTTPS) to prevent interception.
  • Token Storage on Client: Storing tokens in browser local storage is susceptible to Cross-Site Scripting (XSS) attacks. HTTP-only cookies offer better protection against XSS but require CSRF protection. Storing tokens securely on mobile applications also requires specific considerations.
  • Expiration Times: Tokens should have appropriate expiration times. Short-lived access tokens reduce the window of opportunity if a token is compromised.
  • Token Revocation (Blacklisting): Although JWTs are stateless by design, implementing a mechanism to invalidate tokens before their natural expiration (e.g., upon logout or security breach) is crucial. This is often done by maintaining a blacklist of revoked tokens on the server side.
  • Payload Data: Avoid putting sensitive information directly into the token payload, as it is only encoded, not encrypted. Information suitable for the payload includes non-sensitive user identifiers or roles needed for authorization decisions.

Step-by-Step: Building the Flask JWT System#

This section outlines the process of setting up a basic Flask application with JWT authentication using the Flask-JWT-Extended extension.

1. Project Setup and Dependencies#

Begin by creating a project directory and installing Flask and Flask-JWT-Extended.

Terminal window
mkdir flask_jwt_auth
cd flask_jwt_auth
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
pip install Flask Flask-JWT-Extended werkzeug
  • Flask: The web framework.
  • Flask-JWT-Extended: An extension that simplifies working with JWTs in Flask.
  • werkzeug: Used here specifically for secure password hashing.

2. Basic Flask App Structure#

Create a file (e.g., app.py) and set up a minimal Flask application.

from flask import Flask, request, jsonify
from flask_jwt_extended import create_access_token, jwt_required, JWTManager, get_jwt_identity, create_refresh_token, jwt_refresh_token_required, get_raw_jwt
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
# --- Configuration ---
# Change this in a real app!
app.config["JWT_SECRET_KEY"] = "your-super-secret-key-change-me"
# Configure JWT expiration times
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = 3600 # 1 hour
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = 2592000 # 30 days
jwt = JWTManager(app)
# --- User Data (In-Memory for Demonstration) ---
# In a real app, this would be a database
users = {}
# A simple blacklist set for token revocation
blacklist = set()
@jwt.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
jti = decrypted_token['jti']
return jti in blacklist
# --- Routes ---
if __name__ == "__main__":
app.run(debug=True)
  • Initializes the Flask app.
  • Configures the JWT_SECRET_KEY. This must be a strong, randomly generated key in a production environment.
  • Sets expiration times for access and refresh tokens.
  • Initializes JWTManager.
  • Includes a basic in-memory structure for user data and a set for token blacklisting.
  • The @jwt.token_in_blacklist_loader function is registered with Flask-JWT-Extended to check if a token’s unique identifier (jti) is in the blacklist.

3. User Model/Data and Password Hashing#

For a secure system, passwords must never be stored in plain text. Use a strong hashing algorithm. werkzeug.security provides functions for this.

Modify the users structure and add functions for hashing and checking passwords.

# ... (previous code)
# In-memory user storage: username -> { 'password': hashed_password, 'id': user_id }
users = {} # Example: {'testuser': {'password': 'hashed_password_string', 'id': 1}}
user_id_counter = 1 # Simple ID generator for demonstration
def add_user(username, password):
global user_id_counter
if username in users:
return False # User already exists
hashed_password = generate_password_hash(password)
users[username] = {'password': hashed_password, 'id': user_id_counter}
user_id_counter += 1
return True
def authenticate_user(username, password):
user_data = users.get(username)
if user_data and check_password_hash(user_data['password'], password):
return user_data # Return user data including ID
return None # Authentication failed
# --- Blacklist for token revocation ---
# (Already included in step 2)
blacklist = set()
@jwt.token_in_blacklist_loader
def check_if_token_in_blacklist(decrypted_token):
jti = decrypted_token['jti']
return jti in blacklist
# --- Routes ---
# ... (add routes below)
  • generate_password_hash(): Creates a salted hash of the password.
  • check_password_hash(): Verifies a password against a hash.

4. Registration Endpoint (/register)#

This endpoint handles creating new users.

# ... (previous code)
@app.route('/register', methods=['POST'])
def register():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({"msg": "Missing username or password"}), 400
if add_user(username, password):
return jsonify({"msg": "User registered successfully"}), 201
else:
return jsonify({"msg": "Username already exists"}), 409
  • Accepts POST requests with JSON data containing username and password.
  • Checks for missing data.
  • Uses the add_user function to create the user with a hashed password.
  • Returns appropriate success or error responses.

5. Login Endpoint (/login)#

This endpoint authenticates the user and issues JWTs.

# ... (previous code)
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({"msg": "Missing username or password"}), 400
user_data = authenticate_user(username, password)
if user_data:
# Create the tokens. We pass the user's ID as the subject.
# The identity is later retrieved by get_jwt_identity()
access_token = create_access_token(identity=user_data['id'])
refresh_token = create_refresh_token(identity=user_data['id'])
return jsonify(access_token=access_token, refresh_token=refresh_token)
else:
return jsonify({"msg": "Bad username or password"}), 401
  • Accepts POST requests with JSON credentials.
  • Uses authenticate_user to verify credentials.
  • If successful, create_access_token() and create_refresh_token() are used to generate tokens. The identity parameter is used to store information (here, the user ID) that can be retrieved later from the token.
  • Returns the access and refresh tokens in the response body.

6. Protected Endpoint (/profile)#

This endpoint requires a valid access token to access.

# ... (previous code)
@app.route('/profile')
@jwt_required() # Decorator to protect the route
def profile():
# Access the identity of the current user with get_jwt_identity
current_user_id = get_jwt_identity()
# In a real app, you would fetch user details from the database using this ID
# For this example, we just return the ID
return jsonify({"message": f"Welcome user with ID: {current_user_id}"})
  • The @jwt_required() decorator ensures that a valid access token must be present in the request’s Authorization: Bearer <token> header.
  • get_jwt_identity() retrieves the identity (user ID in this case) that was stored in the token payload during creation.

7. Token Refresh Endpoint (/refresh)#

This endpoint allows exchanging a valid refresh token for a new access token.

# ... (previous code)
@app.route('/refresh', methods=['POST'])
@jwt_refresh_token_required() # Requires a valid refresh token
def refresh():
current_user_id = get_jwt_identity()
new_access_token = create_access_token(identity=current_user_id)
return jsonify(access_token=new_access_token)
  • The @jwt_refresh_token_required() decorator ensures a valid refresh token is provided.
  • get_jwt_identity() retrieves the user ID from the refresh token.
  • A new access token is created with the same identity.

8. Logout/Token Revocation (/logout)#

This endpoint demonstrates how to invalidate a token. Flask-JWT-Extended supports blacklisting based on the token’s unique identifier (jti).

# ... (previous code)
@app.route('/logout', methods=['POST'])
@jwt_required() # Requires a valid access token to initiate logout
def logout():
jti = get_raw_jwt()['jti'] # Get the unique identifier of the current token
blacklist.add(jti) # Add the jti to the blacklist
return jsonify({"msg": "Successfully logged out"}), 200
@app.route('/logout2', methods=['POST'])
@jwt_refresh_token_required() # Also allow revoking the refresh token separately
def logout2():
jti = get_raw_jwt()['jti']
blacklist.add(jti)
return jsonify({"msg": "Refresh token revoked"}), 200
  • get_raw_jwt() retrieves the full decoded token payload.
  • The jti (JWT ID) claim is extracted. This is a unique identifier automatically added by Flask-JWT-Extended when JWT_BLACKLIST_ENABLED is set to True (which is the default when using token_in_blacklist_loader).
  • The jti is added to the blacklist set.
  • The @jwt.token_in_blacklist_loader function (defined earlier) is automatically called by Flask-JWT-Extended whenever a token is received. If the token’s jti is found in the blacklist, the request is rejected.
  • Note: In a production environment, the blacklist should persist (e.g., using a database or cache like Redis) rather than being an in-memory set that resets when the server restarts.

Real-World Application: Single Page Applications and Mobile Apps#

The Flask JWT authentication pattern is particularly well-suited for modern application architectures where the frontend (a single-page application built with frameworks like React, Vue, or Angular, or a mobile application) is separate from the backend API.

In such a scenario:

  1. The frontend handles user interaction and sends authentication requests (login, register) to the Flask backend API.
  2. Upon successful login, the Flask API returns JWTs (access and refresh tokens) to the frontend.
  3. The frontend stores these tokens. For example, a SPA might store them in localStorage or sessionStorage, while a mobile app uses secure storage mechanisms provided by the platform.
  4. For subsequent API calls to protected resources, the frontend includes the access token in the Authorization: Bearer header of the HTTP request.
  5. The Flask backend verifies the token using Flask-JWT-Extended. If valid, it processes the request.
  6. When the access token expires, the frontend detects this (e.g., by receiving a 401 Unauthorized response from a protected endpoint) and uses the refresh token to request a new access token from the /refresh endpoint.
  7. User logout involves the frontend deleting the locally stored tokens and sending a request to a logout endpoint on the backend to blacklist the tokens.

This architecture benefits from JWT’s statelessness on the server side, allowing the backend API to scale horizontally without worrying about shared session state.

Key Takeaways and Actionable Insights#

Building a secure login system with Flask and JWT requires attention to detail and adherence to security best practices.

  • Choose Flask-JWT-Extended: This library significantly simplifies JWT integration with Flask by handling token creation, validation, and common workflows like refreshing and blacklisting.
  • Protect the Secret Key: The JWT_SECRET_KEY is paramount. Use a strong, random value and never expose it. Store it securely (e.g., using environment variables).
  • Hash Passwords: Always hash passwords using a strong, modern algorithm like bcrypt or scrypt (provided by werkzeug.security). Never store plain text passwords.
  • Use HTTPS: All communication involving tokens must occur over HTTPS to prevent man-in-the-middle attacks.
  • Set Appropriate Expiration: Configure sensible lifetimes for access tokens (short, e.g., 15 mins to a few hours) and refresh tokens (longer, e.g., days or weeks).
  • Implement Token Revocation: Use a blacklist mechanism (as provided by Flask-JWT-Extended) to invalidate tokens upon logout or security events. Ensure the blacklist is persistent.
  • Secure Client-Side Storage: Be mindful of where tokens are stored on the client side. HTTP-only cookies for access tokens can mitigate XSS risks, but introduce CSRF considerations. Secure local storage options exist for mobile apps.
  • Validate Input: Sanitize and validate all user inputs (usernames, passwords, etc.) to prevent injection attacks.
  • Consider Rate Limiting: Implement rate limiting on login attempts to prevent brute-force attacks.
  • Error Handling: Provide clear but non-verbose error messages to the client (e.g., “Bad username or password” instead of specifying if the username or password was incorrect).

Implementing these steps provides a solid foundation for a secure, scalable authentication system using Flask and JWT.

How to Build a Secure Login System with Flask and JWT Authentication
https://dev-resources.site/posts/how-to-build-a-secure-login-system-with-flask-and-jwt-authentication/
Author
Dev-Resources
Published at
2025-06-26
License
CC BY-NC-SA 4.0