Authentication and Authorization in Web Applications: A Complete Guide
Security is not an afterthoughtโit’s a fundamental requirement of modern web applications. Two concepts form the foundation of application security: authentication and authorization. While often mentioned together, they serve distinct purposes, and confusing them can lead to serious security vulnerabilities.
In this comprehensive guide, we’ll explore what authentication and authorization are, how they differ, common implementation patterns, and best practices for securing your applications.
Authentication vs Authorization: The Critical Difference
Authentication: Verifying Identity
Authentication answers the question: “Who are you?”
Authentication is the process of verifying that a user is who they claim to be. It’s about confirming identity through credentials like passwords, biometrics, or security tokens.
Examples:
- Logging in with username and password
- Using fingerprint recognition
- Scanning a security badge
- Entering a one-time code from an authenticator app
Authorization: Granting Permissions
Authorization answers the question: “What are you allowed to do?”
Authorization is the process of determining what an authenticated user is permitted to access or perform. It’s about enforcing permissions and access control.
Examples:
- A user can view their own profile but not others'
- An admin can delete users; regular users cannot
- A manager can approve expenses up to $5,000
- A guest can read articles but cannot publish them
The Relationship
Authentication โ Authorization
โ โ
"Who are you?" "What can you do?"
โ โ
Verify identity Grant permissions
A user must be authenticated before authorization can be applied. You can’t grant permissions to someone whose identity you haven’t verified.
Authentication Methods
1. Session-Based Authentication (Traditional)
Session-based authentication stores user state on the server:
from flask import Flask, session, request, redirect, url_for
from werkzeug.security import check_password_hash, generate_password_hash
app = Flask(__name__)
app.secret_key = 'your-secret-key'
# Simulated user database
users = {
'john': generate_password_hash('password123')
}
@app.route('/login', methods=['POST'])
def login():
"""Authenticate user and create session"""
username = request.form.get('username')
password = request.form.get('password')
# Verify credentials
if username in users and check_password_hash(users[username], password):
session['user_id'] = username
session['authenticated'] = True
return redirect(url_for('dashboard'))
return 'Invalid credentials', 401
@app.route('/dashboard')
def dashboard():
"""Protected route requiring authentication"""
if not session.get('authenticated'):
return redirect(url_for('login'))
return f'Welcome, {session["user_id"]}!'
@app.route('/logout')
def logout():
"""Clear session"""
session.clear()
return redirect(url_for('login'))
How it works:
- User submits credentials
- Server verifies credentials
- Server creates a session and stores it in memory or database
- Server sends session ID to client (usually in a cookie)
- Client includes session ID in subsequent requests
- Server validates session ID and retrieves user information
Advantages:
- Simple to implement
- Server has full control
- Easy to revoke access (delete session)
Disadvantages:
- Requires server-side storage
- Doesn’t scale well across multiple servers
- Not ideal for mobile or API clients
2. Token-Based Authentication (JWT)
JWT (JSON Web Tokens) are self-contained tokens that encode user information:
from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from werkzeug.security import check_password_hash, generate_password_hash
from datetime import timedelta
app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key'
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1)
jwt = JWTManager(app)
users = {
'john': generate_password_hash('password123')
}
@app.route('/login', methods=['POST'])
def login():
"""Generate JWT token"""
data = request.get_json()
username = data.get('username')
password = data.get('password')
# Verify credentials
if username not in users or not check_password_hash(users[username], password):
return jsonify({'error': 'Invalid credentials'}), 401
# Create JWT token
access_token = create_access_token(identity=username)
return jsonify({'access_token': access_token})
@app.route('/protected', methods=['GET'])
@jwt_required()
def protected():
"""Protected route requiring valid JWT"""
current_user = get_jwt_identity()
return jsonify({'message': f'Hello, {current_user}!'})
How JWT works:
- User submits credentials
- Server verifies credentials and creates a JWT token
- Token is sent to client
- Client includes token in Authorization header:
Authorization: Bearer <token> - Server validates token signature and expiration
- If valid, server processes request
JWT Structure:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header.Payload.Signature
Advantages:
- Stateless (no server-side storage needed)
- Scales across multiple servers
- Works well for APIs and mobile apps
- Self-contained (includes user information)
Disadvantages:
- Can’t revoke tokens immediately (until expiration)
- Token size can be larger than session ID
- Requires secure storage on client
3. OAuth 2.0: Delegated Authentication
OAuth 2.0 allows users to authenticate using third-party providers:
from flask import Flask, redirect, url_for, session
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.secret_key = 'your-secret-key'
oauth = OAuth(app)
# Configure Google OAuth
google = oauth.register(
name='google',
client_id='your-client-id',
client_secret='your-client-secret',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)
@app.route('/login')
def login():
"""Redirect to Google login"""
redirect_uri = url_for('authorize', _external=True)
return google.authorize_redirect(redirect_uri)
@app.route('/authorize')
def authorize():
"""Handle OAuth callback"""
token = google.authorize_access_token()
user = token.get('userinfo')
if user:
session['user'] = user
return redirect(url_for('dashboard'))
return 'Authorization failed', 401
@app.route('/dashboard')
def dashboard():
"""Protected route"""
user = session.get('user')
if not user:
return redirect(url_for('login'))
return f'Welcome, {user["name"]}!'
OAuth 2.0 Flow:
- User clicks “Login with Google”
- App redirects to Google’s authorization server
- User authenticates with Google
- Google redirects back to app with authorization code
- App exchanges code for access token
- App uses token to get user information
- User is logged in
Advantages:
- Users don’t share passwords with your app
- Leverages existing accounts (Google, GitHub, etc.)
- Centralized authentication management
- Easy to implement
Disadvantages:
- Dependency on third-party provider
- More complex than basic authentication
- Privacy concerns (third-party data sharing)
4. Multi-Factor Authentication (MFA)
MFA requires multiple verification methods:
import pyotp
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/setup-mfa', methods=['POST'])
def setup_mfa():
"""Generate MFA secret for user"""
# Generate secret
secret = pyotp.random_base32()
# Generate QR code URL
totp = pyotp.TOTP(secret)
qr_code_url = totp.provisioning_uri(
name='[email protected]',
issuer_name='MyApp'
)
return jsonify({
'secret': secret,
'qr_code_url': qr_code_url
})
@app.route('/verify-mfa', methods=['POST'])
def verify_mfa():
"""Verify MFA code"""
data = request.get_json()
secret = data.get('secret')
code = data.get('code')
totp = pyotp.TOTP(secret)
if totp.verify(code):
return jsonify({'verified': True})
return jsonify({'verified': False}), 401
Common MFA Methods:
- Time-based One-Time Password (TOTP) - Google Authenticator, Authy
- SMS codes
- Email codes
- Hardware security keys
- Biometric authentication
Authorization Patterns
1. Role-Based Access Control (RBAC)
Users are assigned roles, and roles have permissions:
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
# User roles and permissions
ROLES = {
'admin': ['read', 'write', 'delete', 'manage_users'],
'editor': ['read', 'write'],
'viewer': ['read']
}
# Simulated user database
users = {
'john': {'role': 'admin'},
'jane': {'role': 'editor'},
'bob': {'role': 'viewer'}
}
def require_permission(permission):
"""Decorator to check permissions"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
username = request.headers.get('X-User')
if not username or username not in users:
return jsonify({'error': 'Unauthorized'}), 401
user_role = users[username]['role']
user_permissions = ROLES.get(user_role, [])
if permission not in user_permissions:
return jsonify({'error': 'Forbidden'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/posts', methods=['GET'])
@require_permission('read')
def get_posts():
"""Anyone with read permission can access"""
return jsonify({'posts': []})
@app.route('/posts', methods=['POST'])
@require_permission('write')
def create_post():
"""Only users with write permission can create"""
return jsonify({'message': 'Post created'})
@app.route('/users', methods=['DELETE'])
@require_permission('manage_users')
def delete_user():
"""Only admins can delete users"""
return jsonify({'message': 'User deleted'})
Advantages:
- Simple to understand and implement
- Easy to manage permissions at role level
- Good for most applications
Disadvantages:
- Inflexible for complex permission requirements
- Role explosion (too many roles)
- Difficult to handle exceptions
2. Attribute-Based Access Control (ABAC)
Permissions are based on attributes of users, resources, and environment:
from flask import Flask, request, jsonify
from functools import wraps
app = Flask(__name__)
# User attributes
users = {
'john': {
'department': 'engineering',
'level': 'senior',
'location': 'US'
},
'jane': {
'department': 'marketing',
'level': 'junior',
'location': 'EU'
}
}
# Resource attributes
resources = {
'sensitive_doc': {
'classification': 'confidential',
'department': 'engineering'
},
'public_doc': {
'classification': 'public',
'department': None
}
}
def check_access(user_attrs, resource_attrs):
"""Determine if user can access resource"""
# Public resources are accessible to everyone
if resource_attrs.get('classification') == 'public':
return True
# Confidential resources require matching department
if resource_attrs.get('classification') == 'confidential':
return user_attrs.get('department') == resource_attrs.get('department')
return False
@app.route('/documents/<doc_id>', methods=['GET'])
def get_document(doc_id):
"""Access document based on attributes"""
username = request.headers.get('X-User')
if not username or username not in users:
return jsonify({'error': 'Unauthorized'}), 401
if doc_id not in resources:
return jsonify({'error': 'Not found'}), 404
user_attrs = users[username]
resource_attrs = resources[doc_id]
if not check_access(user_attrs, resource_attrs):
return jsonify({'error': 'Forbidden'}), 403
return jsonify({'document': resource_attrs})
Advantages:
- Highly flexible and granular
- Handles complex permission requirements
- Scales well with many resources
Disadvantages:
- Complex to implement and maintain
- Performance overhead
- Difficult to debug
3. Permission-Based Access Control
Fine-grained permissions assigned directly to users:
# User permissions
user_permissions = {
'john': [
'post:read',
'post:write',
'post:delete',
'user:manage'
],
'jane': [
'post:read',
'post:write'
],
'bob': [
'post:read'
]
}
def require_permission(permission):
"""Check if user has specific permission"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
username = request.headers.get('X-User')
if not username or username not in user_permissions:
return jsonify({'error': 'Unauthorized'}), 401
if permission not in user_permissions[username]:
return jsonify({'error': 'Forbidden'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/posts', methods=['POST'])
@require_permission('post:write')
def create_post():
return jsonify({'message': 'Post created'})
Advantages:
- Very granular control
- Easy to understand individual permissions
- Flexible for complex scenarios
Disadvantages:
- Difficult to manage at scale
- Prone to inconsistencies
- Hard to audit
Security Best Practices
1. Password Security
from werkzeug.security import generate_password_hash, check_password_hash
import re
def validate_password(password):
"""Validate password strength"""
if len(password) < 12:
return False, "Password must be at least 12 characters"
if not re.search(r'[A-Z]', password):
return False, "Password must contain uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain lowercase letter"
if not re.search(r'[0-9]', password):
return False, "Password must contain digit"
if not re.search(r'[!@#$%^&*]', password):
return False, "Password must contain special character"
return True, "Password is strong"
# Hash passwords
hashed = generate_password_hash('MyPassword123!')
# Verify passwords
is_correct = check_password_hash(hashed, 'MyPassword123!')
Password Best Practices:
- Enforce minimum length (12+ characters)
- Require complexity (uppercase, lowercase, numbers, symbols)
- Use strong hashing algorithms (bcrypt, Argon2)
- Never store plain text passwords
- Implement rate limiting on login attempts
- Use password managers
2. Secure Token Storage
# โ Bad: Storing token in localStorage (vulnerable to XSS)
localStorage.setItem('token', token);
# โ Good: Storing token in httpOnly cookie (protected from XSS)
response.set_cookie(
'token',
token,
httponly=True, # Not accessible via JavaScript
secure=True, # Only sent over HTTPS
samesite='Strict' # CSRF protection
)
Token Storage Best Practices:
- Use httpOnly cookies for web applications
- Use secure flag (HTTPS only)
- Use SameSite attribute (CSRF protection)
- For mobile apps, use secure storage (Keychain, Keystore)
- Never store tokens in localStorage
3. HTTPS Enforcement
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
# Enforce HTTPS
Talisman(app, force_https=True)
# Or manually redirect
@app.before_request
def enforce_https():
if not request.is_secure and not app.debug:
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)
HTTPS Best Practices:
- Always use HTTPS in production
- Use valid SSL/TLS certificates
- Implement HSTS (HTTP Strict Transport Security)
- Redirect HTTP to HTTPS
4. CSRF Protection
from flask import Flask, render_template
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
csrf = CSRFProtect(app)
@app.route('/form', methods=['GET'])
def form():
"""Display form with CSRF token"""
return render_template('form.html')
@app.route('/submit', methods=['POST'])
@csrf.protect
def submit():
"""Protected endpoint"""
return jsonify({'message': 'Form submitted'})
HTML Template:
<form method="POST" action="/submit">
{{ csrf_token() }}
<input type="text" name="data">
<button type="submit">Submit</button>
</form>
CSRF Protection Best Practices:
- Use CSRF tokens for state-changing operations
- Validate token on server
- Use SameSite cookies
- Implement proper CORS policies
5. Session Security
from flask import Flask, session
from datetime import timedelta
app = Flask(__name__)
# Session configuration
app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only
app.config['SESSION_COOKIE_HTTPONLY'] = True # Not accessible via JS
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
@app.route('/login', methods=['POST'])
def login():
session.permanent = True
session['user_id'] = user_id
return redirect(url_for('dashboard'))
Session Best Practices:
- Set secure flag (HTTPS only)
- Set httpOnly flag (not accessible via JavaScript)
- Set SameSite attribute
- Implement session timeout
- Regenerate session ID after login
- Clear session on logout
Common Security Pitfalls
Pitfall 1: Storing Sensitive Data in JWT
# โ Bad: Storing sensitive data in JWT (visible in token)
payload = {
'user_id': 123,
'password': 'secret123', # Never do this!
'credit_card': '1234-5678-9012-3456' # Never do this!
}
# โ Good: Only store non-sensitive identifiers
payload = {
'user_id': 123,
'username': 'john'
}
Pitfall 2: Not Validating Tokens
# โ Bad: Not validating token signature
import jwt
token = request.headers.get('Authorization').split()[1]
payload = jwt.decode(token, options={"verify_signature": False})
# โ Good: Always validate signature
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
Pitfall 3: Insufficient Authorization Checks
# โ Bad: Only checking authentication
@app.route('/users/<user_id>')
def get_user(user_id):
if not session.get('authenticated'):
return 'Unauthorized', 401
# Any authenticated user can access any user's data!
return get_user_data(user_id)
# โ Good: Check both authentication and authorization
@app.route('/users/<user_id>')
def get_user(user_id):
current_user = session.get('user_id')
if not current_user:
return 'Unauthorized', 401
# Users can only access their own data
if current_user != user_id and not is_admin(current_user):
return 'Forbidden', 403
return get_user_data(user_id)
Pitfall 4: Weak Password Requirements
# โ Bad: Weak password requirements
if len(password) >= 6:
save_password(password)
# โ Good: Strong password requirements
if validate_password_strength(password):
save_password(hash_password(password))
Choosing the Right Approach
| Scenario | Recommended Approach |
|---|---|
| Traditional web app | Session-based + CSRF tokens |
| REST API | JWT tokens |
| Mobile app | JWT tokens + refresh tokens |
| Third-party integration | OAuth 2.0 |
| High security requirement | MFA + strong passwords |
| Complex permissions | ABAC or permission-based |
| Simple permissions | RBAC |
Conclusion
Authentication and authorization are critical components of web application security. Understanding the differences between them, implementing them correctly, and following security best practices will significantly improve your application’s security posture.
Key Takeaways
- Authentication verifies identity; authorization grants permissions
- Choose authentication method based on your use case (sessions, JWT, OAuth 2.0)
- Implement authorization using RBAC, ABAC, or permission-based systems
- Always hash passwords and validate tokens
- Use HTTPS, secure cookies, and CSRF protection
- Implement MFA for sensitive applications
- Regularly audit and test your security implementation
- Stay updated on security best practices and vulnerabilities
Security is an ongoing process, not a one-time implementation. Continuously monitor, test, and improve your authentication and authorization systems to protect your users and their data.
Comments