Skip to main content

Python Authentication and Authorization: Secure Guide

Authentication is the process of verifying that a user is who they claim to be (typically by validating a username and password); authorization is the process of determining what authenticated users are allowed to do. Together, they form the access control layer of an application. A compromised authentication system allows attackers to impersonate legitimate users, access sensitive data, and perform unauthorized actions. A weak authorization system allows authenticated users to access resources belonging to other users or to perform admin actions without permission. Python provides cryptographic libraries and frameworks that make both authentication and authorization straightforward when implemented correctly.

Password Hashing: The Foundation of Authentication

The most common mistake in authentication is storing passwords in plain text or using weak hashing algorithms. If an attacker steals your password database, plaintext passwords are immediately compromised. Weak hashing algorithms like MD5 or SHA-1 can be reversed via lookup tables or brute-force attacks in seconds. Instead, use a slow, salted hashing algorithm like bcrypt, which is specifically designed to resist brute-force attacks:

# Secure password hashing with bcrypt
import bcrypt

def hash_password(password: str) -> str:
# bcrypt.hashpw automatically generates a salt and applies key stretching
# cost=12 means the algorithm performs 2^12 = 4096 iterations (slow on purpose)
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')

def verify_password(password: str, hashed: str) -> bool:
# Check if the provided password matches the stored hash
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))

# Usage
stored_hash = hash_password('SecurePassword123!')
# Later, during login:
if verify_password(user_input, stored_hash):
print("Login successful")
else:
print("Invalid password")

Bcrypt performs key stretching (multiple iterations of the hashing algorithm) to deliberately slow down the process. Hashing a password takes ~100 milliseconds, which is imperceptible to the user but makes brute-force attacks (trying millions of passwords) computationally expensive. The salt ensures that even if two users have the same password, their hashes are different, preventing rainbow table attacks.

Stateful Sessions vs. Stateless JWT Authentication

Web applications use two main authentication models. Stateful sessions store session data on the server (e.g., in a database or in-memory cache); when a user logs in, the server creates a session and sends a session ID to the client as a cookie. On subsequent requests, the client sends the session ID, and the server looks up the session to verify the user's identity. Stateless JSON Web Tokens (JWT) encode user information in a digitally signed token; the server does not need to store anything. When a user logs in, the server issues a token; on subsequent requests, the client sends the token, and the server verifies the signature to trust the token's contents.

Stateful sessions are simpler and better for logout (delete the session on the server). JWT is better for microservices and mobile apps (no server-side state required). Here is a simple JWT implementation:

# JWT authentication with PyJWT
import jwt
from datetime import datetime, timedelta
from typing import Optional

SECRET_KEY = 'your-secret-key-keep-this-secure'

def create_jwt_token(user_id: int, expires_in_hours: int = 24) -> str:
# Create a token with user_id and expiration time
payload = {
'user_id': user_id,
'exp': datetime.utcnow() + timedelta(hours=expires_in_hours),
'iat': datetime.utcnow(),
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token

def verify_jwt_token(token: str) -> Optional[int]:
try:
# Verify the signature and expiration
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
return payload.get('user_id')
except jwt.ExpiredSignatureError:
print("Token has expired")
return None
except jwt.InvalidTokenError:
print("Invalid token")
return None

# Usage
token = create_jwt_token(user_id=42)
user_id = verify_jwt_token(token)

JWT tokens should be stored in secure, HTTP-only cookies (not localStorage, which is vulnerable to XSS). The httpOnly flag prevents JavaScript from accessing the cookie, mitigating XSS-based token theft.

Role-Based Access Control (RBAC)

Authorization is implemented using role-based or permission-based access control. Roles group permissions (e.g., "Admin" can delete users, "Editor" can edit posts, "Viewer" can only read). Here is a simple RBAC implementation in Flask:

# Flask RBAC with decorators
from flask import Flask, request, jsonify
from functools import wraps
from typing import Callable

app = Flask(__name__)

# Mock database of users and their roles
users = {
'alice': {'password_hash': '...', 'role': 'admin'},
'bob': {'password_hash': '...', 'role': 'editor'},
}

def require_role(*allowed_roles):
"""Decorator to protect routes by role."""
def decorator(f: Callable):
@wraps(f)
def decorated_function(*args, **kwargs):
# In a real app, extract user from JWT or session
user_role = request.headers.get('X-User-Role')
if user_role not in allowed_roles:
return jsonify({'error': 'Unauthorized'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator

@app.route('/api/users', methods=['DELETE'])
@require_role('admin')
def delete_user():
# Only admins can delete users
return jsonify({'message': 'User deleted'})

@app.route('/api/posts', methods=['POST'])
@require_role('admin', 'editor')
def create_post():
# Admins and editors can create posts
return jsonify({'message': 'Post created'})

Role-based access control is straightforward and scales well for small to medium applications. For complex permissions (e.g., "can edit own posts but not others"), use attribute-based access control (ABAC), which evaluates conditions on the resource and user.

Multi-Factor Authentication (MFA)

MFA requires users to provide two or more forms of verification (something they know, like a password; something they have, like a TOTP code; something they are, like a fingerprint). TOTP (Time-based One-Time Password) is the standard for time-based codes; libraries like pyotp implement it:

# TOTP-based MFA
import pyotp
import qrcode

def setup_mfa(username: str) -> tuple[str, str]:
"""Generate a TOTP secret and QR code for the user."""
secret = pyotp.random_base32() # Random 32-character base32 string
totp = pyotp.TOTP(secret)

# Generate a QR code for the authenticator app
uri = totp.provisioning_uri(username, issuer_name='MyApp')
qr = qrcode.make(uri)

return secret, qr

def verify_mfa(secret: str, code: str) -> bool:
"""Verify a TOTP code."""
totp = pyotp.TOTP(secret)
# Allow a 30-second window tolerance for clock skew
return totp.verify(code, valid_window=1)

# Usage
secret, qr = setup_mfa('[email protected]')
# User scans QR code with authenticator app, receives 6-digit code
if verify_mfa(secret, '123456'):
print("MFA successful")

TOTP is resistant to phishing because the codes are time-based and unique; stealing a code is ineffective if the attacker cannot use it within 30 seconds. Always require MFA for admin and sensitive accounts.

Key Takeaways

  • Use bcrypt (or scrypt, or Argon2) to hash passwords with key stretching; never store plaintext passwords or use weak algorithms like MD5.
  • JWT tokens are stateless and work well for APIs and microservices; sessions are simpler and better for traditional web apps.
  • Implement role-based access control (RBAC) using decorators or middleware to enforce authorization rules on protected routes.
  • Always require multi-factor authentication (MFA) for admin accounts and sensitive operations; TOTP is the standard.
  • Store JWT tokens in secure, HTTP-only cookies to mitigate XSS-based token theft.

Frequently Asked Questions

How often should users change their passwords?

Modern security guidance recommends NOT forcing regular password changes unless there is evidence of compromise. Instead, encourage strong passwords and enable breach monitoring. Force a password reset only when a breach is detected.

Is it safe to include sensitive data in JWT tokens?

No. JWT tokens are Base64-encoded, which is not encrypted. Never include passwords, credit card numbers, or PII in JWT payload. Use a symmetric key (HS256) for small internal systems; use asymmetric keys (RS256) for multi-service systems.

Can I store JWT tokens in localStorage instead of cookies?

localStorage is vulnerable to XSS (attackers can steal tokens via injected JavaScript). HTTP-only cookies are significantly safer because JavaScript cannot access them. If you must use localStorage, implement robust XSS protections.

What is the difference between authentication and authorization?

Authentication verifies identity (who are you?); authorization determines permissions (what can you do?). You must authenticate before you can authorize.

Should I implement MFA for all users or only admins?

Start with MFA for admin accounts and high-value targets (email, payment processors). For regular users, offer MFA as optional but recommended. Gradually increase adoption as your security posture improves.

Further Reading