Flask API Authentication
Authentication is a critical aspect of any API. In this tutorial, you'll learn how to secure your Flask API endpoints, ensuring only authorized users can access protected resources.
Introduction to API Authentication
API authentication is the process of verifying the identity of clients attempting to access your API. Without proper authentication, your API would be open to anyone, potentially exposing sensitive data or allowing malicious users to manipulate your application.
There are several authentication methods commonly used in Flask APIs:
- Basic Authentication: Username/password credentials sent in HTTP headers
- Token-Based Authentication: Using tokens (like JWT) for stateless authentication
- API Keys: Simple keys included in requests to identify clients
- OAuth: An authorization framework that provides secure delegated access
In this guide, we'll explore these methods with practical examples in Flask.
Basic Authentication
Basic authentication is one of the simplest forms of API authentication, where clients send their username and password with each request.
How Basic Authentication Works
- The client encodes the username and password using Base64
- The encoded string is sent with the
Authorization
header - The server decodes and verifies the credentials
Implementing Basic Authentication in Flask
Let's create a simple Flask API with basic authentication:
from flask import Flask, request, jsonify
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
auth = HTTPBasicAuth()
# Sample user database
users = {
"john": generate_password_hash("hello"),
"susan": generate_password_hash("bye")
}
@auth.verify_password
def verify_password(username, password):
if username in users and check_password_hash(users[username], password):
return username
return None
@app.route('/api/resource')
@auth.login_required
def get_resource():
return jsonify({
'message': f'Hello, {auth.current_user()}! You accessed a protected resource'
})
if __name__ == '__main__':
app.run(debug=True)
Testing Basic Authentication
When making requests to the protected endpoint, you need to include the authentication header:
curl -u john:hello http://localhost:5000/api/resource
Expected output:
{
"message": "Hello, john! You accessed a protected resource"
}
Without valid credentials, you'll receive a 401 Unauthorized error.
Limitations of Basic Authentication
While easy to implement, basic authentication has several drawbacks:
- Credentials are sent with every request
- Base64 encoding is easily decoded (not encrypted)
- No built-in expiration mechanism
- Requires secure HTTPS connections
Token-Based Authentication with JWT
JSON Web Tokens (JWT) provide a more secure and flexible way to handle authentication. Instead of sending credentials with each request, the client obtains a token during login which is then used for subsequent requests.
How JWT Authentication Works
- User logs in with credentials
- Server validates credentials and generates a JWT
- Client stores the JWT and sends it with subsequent requests
- Server verifies the JWT signature and extracts user information
Let's implement JWT authentication using the flask-jwt-extended
package:
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'super-secret-key' # Change this in production!
jwt = JWTManager(app)
# Sample user database
users = {
"john": generate_password_hash("hello"),
"susan": generate_password_hash("bye")
}
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username', None)
password = request.json.get('password', None)
if not username or not password:
return jsonify({"msg": "Missing username or password"}), 400
if username not in users or not check_password_hash(users[username], password):
return jsonify({"msg": "Bad username or password"}), 401
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
@app.route('/api/protected', methods=['GET'])
@jwt_required()
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
if __name__ == '__main__':
app.run(debug=True)
Testing JWT Authentication
First, obtain a token by logging in:
curl -H "Content-Type: application/json" -X POST \
-d '{"username":"john","password":"hello"}' \
http://localhost:5000/login
Expected output:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Then, use this token to access protected endpoints:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:5000/api/protected
Expected output:
{
"logged_in_as": "john"
}
Advanced JWT Features
flask-jwt-extended
provides many additional features:
Token Expiration
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(hours=1)
Refresh Tokens
@app.route('/refresh', methods=['POST'])
@jwt_required(refresh=True)
def refresh():
current_user = get_jwt_identity()
new_token = create_access_token(identity=current_user)
return jsonify(access_token=new_token)
Token Revocation
from flask_jwt_extended import get_jwt
# Store for revoked tokens
revoked_tokens = set()
@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
jti = jwt_payload["jti"]
return jti in revoked_tokens
@app.route('/logout', methods=['POST'])
@jwt_required()
def logout():
jti = get_jwt()["jti"]
revoked_tokens.add(jti)
return jsonify(msg="Successfully logged out"), 200
API Keys Authentication
API keys offer a simple authentication mechanism often used for machine-to-machine communication.
Implementing API Key Authentication
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
# Sample API keys
api_keys = {
"abc123": "user1",
"xyz789": "user2"
}
def require_api_key(view_function):
@wraps(view_function)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key and api_key in api_keys:
# You can store the API key user for later use
request.api_user = api_keys[api_key]
return view_function(*args, **kwargs)
else:
return jsonify({"error": "Invalid or missing API key"}), 403
return decorated_function
@app.route('/api/data')
@require_api_key
def get_data():
return jsonify({
"message": f"Hello, {request.api_user}! Here's your data",
"data": [1, 2, 3, 4, 5]
})
if __name__ == '__main__':
app.run(debug=True)
Testing API Key Authentication
curl -H "X-API-Key: abc123" http://localhost:5000/api/data
Expected output:
{
"message": "Hello, user1! Here's your data",
"data": [1, 2, 3, 4, 5]
}
OAuth 2.0 with Flask
OAuth 2.0 is more complex but provides a robust framework for authorization. Here's a simplified example using authlib
:
from flask import Flask, request, jsonify, url_for
from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector
from authlib.oauth2.rfc6749 import grants
from authlib.oauth2.rfc7636 import CodeChallenge
from authlib.oauth2.rfc7662 import BearerTokenValidator
app = Flask(__name__)
# Setup for OAuth server and resource protection would go here
# This is a simplified example
# Protected resource
@app.route('/api/me')
@require_oauth('profile')
def api_me():
user = current_token.user
return jsonify(id=user.id, username=user.username)
# OAuth token endpoint
@app.route('/oauth/token', methods=['POST'])
def issue_token():
return authorization.create_token_response()
# OAuth authorization endpoint
@app.route('/oauth/authorize', methods=['GET', 'POST'])
def authorize():
# Authorization logic would go here
pass
Real-World Authentication Example: RESTful API with Role-Based Access
Let's build a more comprehensive example of a Flask API with JWT authentication and role-based access control (RBAC):
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from flask_jwt_extended import JWTManager, jwt_required, create_access_token, get_jwt_identity, get_jwt
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['JWT_SECRET_KEY'] = 'your-secret-key' # Change in production!
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(hours=1)
db = SQLAlchemy(app)
jwt = JWTManager(app)
# User model with roles
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(200), nullable=False)
role = db.Column(db.String(20), nullable=False, default='user')
def __repr__(self):
return f'<User {self.username}>'
# Create tables
with app.app_context():
db.create_all()
# Create admin user if not exists
if not User.query.filter_by(username='admin').first():
admin = User(
username='admin',
password=generate_password_hash('adminpass'),
role='admin'
)
db.session.add(admin)
db.session.commit()
# Role-based authorization decorator
def role_required(roles):
def wrapper(fn):
@wraps(fn)
def decorator(*args, **kwargs):
claims = get_jwt()
if claims.get('role') in roles:
return fn(*args, **kwargs)
return jsonify(msg="Insufficient permissions"), 403
return decorator
return wrapper
# Add role claim to JWT tokens
@jwt.additional_claims_loader
def add_claims_to_access_token(identity):
user = User.query.filter_by(username=identity).first()
return {'role': user.role}
# Routes
@app.route('/register', methods=['POST'])
def register():
username = request.json.get('username')
password = request.json.get('password')
if not username or not password:
return jsonify(msg="Missing username or password"), 400
if User.query.filter_by(username=username).first():
return jsonify(msg="Username already exists"), 409
new_user = User(
username=username,
password=generate_password_hash(password)
)
db.session.add(new_user)
db.session.commit()
return jsonify(msg=f"User {username} created successfully"), 201
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
if not user or not check_password_hash(user.password, password):
return jsonify(msg="Bad username or password"), 401
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token, user_role=user.role)
@app.route('/api/user-info', methods=['GET'])
@jwt_required()
def user_info():
current_user = get_jwt_identity()
user = User.query.filter_by(username=current_user).first()
return jsonify(
username=user.username,
role=user.role
)
@app.route('/api/admin-only', methods=['GET'])
@jwt_required()
@role_required(['admin'])
def admin_only():
return jsonify(msg="You have accessed an admin-only endpoint")
@app.route('/api/users', methods=['GET'])
@jwt_required()
@role_required(['admin'])
def get_all_users():
users = User.query.all()
result = []
for user in users:
result.append({
'id': user.id,
'username': user.username,
'role': user.role
})
return jsonify(users=result)
if __name__ == '__main__':
app.run(debug=True)
This example demonstrates:
- User registration and login
- Role-based access control
- Protected endpoints
- User database with SQLAlchemy
- Password hashing for security
Security Best Practices
When implementing API authentication, always follow these security practices:
- Use HTTPS: Always serve your API over HTTPS to prevent man-in-the-middle attacks
- Secure Secrets: Never expose JWT secret keys, API keys, or passwords in your code
- Hash Passwords: Always hash passwords before storing them
- Limit Token Lifetime: Set reasonable expiration times for tokens
- Implement Rate Limiting: Prevent brute-force attacks by limiting request rates
- Validate All Input: Never trust client input without validation
- Use Strong Algorithms: For JWT, prefer algorithms like RS256 over HS256 when possible
- Implement Token Revocation: Have a mechanism to invalidate tokens when necessary
Summary
In this tutorial, you've learned:
- Different authentication methods for Flask APIs
- How to implement basic authentication
- Token-based authentication with JWT
- API key authentication
- Role-based access control
- Security best practices for API authentication
With these techniques, you can build secure Flask APIs that protect your users' data and resources from unauthorized access.
Additional Resources
- Flask-JWT-Extended Documentation
- Flask-HTTPAuth Documentation
- OWASP API Security Top 10
- JWT.io - Tool for debugging JWTs
Exercises
- Modify the JWT example to implement refresh tokens.
- Add rate limiting to the login endpoint to prevent brute force attacks.
- Implement a password reset flow for your API users.
- Create a middleware that logs all authentication attempts, successful or not.
- Build a simple client application that authenticates with your API and displays user data.
Happy coding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)