Authentication and Authorization

Introduction

Authentication and authorization are fundamental to application security. Authentication verifies who you are (identity), while authorization determines what you can do (permissions). Getting these wrong can lead to unauthorized access, data breaches, and compromised user accounts.

This reading covers the complete spectrum of authentication and authorization: from password security and hashing to modern approaches like JSON Web Tokens (JWT) and OAuth. You'll learn not just how these systems work, but how to implement them securely and avoid common pitfalls.

Learning Objectives

By the end of this reading, you will be able to:

  • Distinguish between authentication and authorization
  • Implement secure password storage using proper hashing
  • Build session-based authentication systems
  • Use token-based authentication with JWT
  • Understand OAuth 2.0 and OpenID Connect basics
  • Implement role-based and attribute-based access control
  • Recognize and prevent common authentication vulnerabilities
  • Apply security best practices to authentication systems

Authentication vs Authorization

These terms are often confused but represent distinct concepts:

Authentication (AuthN)

Who are you?

Authentication verifies the identity of a user or system. It answers the question "Are you really who you claim to be?"

Common methods:

  • Something you know (password, PIN)
  • Something you have (phone, hardware token)
  • Something you are (fingerprint, face)
  • Somewhere you are (geolocation)

Authorization (AuthZ)

What can you do?

Authorization determines what resources a user can access and what actions they can perform. It answers the question "Are you allowed to do this?"

Common models:

  • Role-Based Access Control (RBAC)
  • Attribute-Based Access Control (ABAC)
  • Access Control Lists (ACL)

Example Scenario

# Authentication: Verify user identity
def login(username, password):
    user = authenticate(username, password)
    if user:
        return create_session(user)
    return None

# Authorization: Check permissions
def delete_post(user, post_id):
    post = get_post(post_id)

    # Check if user is authorized to delete this post
    if user.id == post.author_id or user.role == 'admin':
        delete(post)
        return True

    raise PermissionDenied("You don't have permission to delete this post")

Password Security

Passwords remain the most common authentication method. Securing them properly is critical.

Password Storage: Never Store Plaintext

# ABSOLUTELY NEVER DO THIS
def create_user_terrible(username, password):
    db.execute(
        "INSERT INTO users (username, password) VALUES (?, ?)",
        (username, password)  # STORING PLAINTEXT PASSWORD!
    )

# When database is breached, all passwords are immediately compromised

Password Hashing with Salt

import hashlib
import os
import hmac

class PasswordHasher:
    """Secure password hashing implementation"""

    def __init__(self, iterations=100000):
        self.iterations = iterations

    def hash_password(self, password):
        """
        Hash password using PBKDF2-SHA256

        Returns: Dictionary with salt and hash
        """
        # Generate random salt (32 bytes = 256 bits)
        salt = os.urandom(32)

        # Use PBKDF2 (Password-Based Key Derivation Function 2)
        # Many iterations make brute-force attacks slower
        key = hashlib.pbkdf2_hmac(
            'sha256',           # Hash algorithm
            password.encode(),  # Password as bytes
            salt,               # Random salt
            self.iterations     # Number of iterations
        )

        return {
            'salt': salt.hex(),
            'hash': key.hex(),
            'iterations': self.iterations
        }

    def verify_password(self, password, stored_salt, stored_hash, iterations):
        """Verify password against stored hash"""

        # Hash the provided password with same salt and iterations
        key = hashlib.pbkdf2_hmac(
            'sha256',
            password.encode(),
            bytes.fromhex(stored_salt),
            iterations
        )

        # Constant-time comparison prevents timing attacks
        return hmac.compare_digest(key.hex(), stored_hash)

# Usage example
hasher = PasswordHasher()

# When user registers
password = "MySecurePassword123!"
hashed = hasher.hash_password(password)

print(f"Salt: {hashed['salt'][:32]}...")
print(f"Hash: {hashed['hash'][:32]}...")

# Store in database
db.execute("""
    INSERT INTO users (username, password_salt, password_hash, hash_iterations)
    VALUES (?, ?, ?, ?)
""", ('alice', hashed['salt'], hashed['hash'], hashed['iterations']))

# When user logs in
is_valid = hasher.verify_password(
    'MySecurePassword123!',
    hashed['salt'],
    hashed['hash'],
    hashed['iterations']
)
print(f"Password valid: {is_valid}")

Modern Password Hashing: bcrypt and Argon2

import bcrypt

# bcrypt is designed for passwords and automatically handles salting
class BcryptPasswordHasher:
    def __init__(self, rounds=12):
        """
        rounds: Cost factor (higher = slower = more secure)
        Each increment doubles the time
        12 is a good default (2^12 iterations)
        """
        self.rounds = rounds

    def hash_password(self, password):
        """Hash password using bcrypt"""
        # bcrypt handles salt generation internally
        salt = bcrypt.gensalt(rounds=self.rounds)
        hashed = bcrypt.hashpw(password.encode(), salt)

        # Return as string for storage
        return hashed.decode('utf-8')

    def verify_password(self, password, hashed):
        """Verify password"""
        return bcrypt.checkpw(
            password.encode(),
            hashed.encode()
        )

# Usage
bcrypt_hasher = BcryptPasswordHasher()

# Hash password
password = "MySecurePassword123!"
hashed = bcrypt_hasher.hash_password(password)
print(f"Bcrypt hash: {hashed}")

# Verify password
print(f"Valid: {bcrypt_hasher.verify_password(password, hashed)}")
print(f"Invalid: {bcrypt_hasher.verify_password('wrong', hashed)}")
# Argon2: Winner of Password Hashing Competition, most secure option
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

class SecurePasswordHasher:
    def __init__(self):
        # Default parameters are secure
        self.hasher = PasswordHasher()

    def hash_password(self, password):
        """Hash password using Argon2id"""
        return self.hasher.hash(password)

    def verify_password(self, password, hashed):
        """Verify password"""
        try:
            self.hasher.verify(hashed, password)
            return True
        except VerifyMismatchError:
            return False

# Usage
argon2_hasher = SecurePasswordHasher()
hashed = argon2_hasher.hash_password("MySecurePassword123!")
print(f"Argon2 hash: {hashed}")

Password Policy Implementation

import re

class PasswordPolicy:
    """Enforce password complexity requirements"""

    def __init__(self):
        self.min_length = 12
        self.require_uppercase = True
        self.require_lowercase = True
        self.require_digit = True
        self.require_special = True
        self.common_passwords = self.load_common_passwords()

    def load_common_passwords(self):
        """Load list of commonly used passwords"""
        # In production, load from file like "10k-most-common.txt"
        return {
            'password', '123456', 'password123', 'admin',
            'qwerty', 'letmein', 'welcome', 'monkey123'
        }

    def validate(self, password):
        """
        Validate password against policy

        Returns: (is_valid, error_messages)
        """
        errors = []

        # Check length
        if len(password) < self.min_length:
            errors.append(f"Password must be at least {self.min_length} characters")

        # Check character requirements
        if self.require_uppercase and not re.search(r'[A-Z]', password):
            errors.append("Password must contain uppercase letter")

        if self.require_lowercase and not re.search(r'[a-z]', password):
            errors.append("Password must contain lowercase letter")

        if self.require_digit and not re.search(r'\d', password):
            errors.append("Password must contain digit")

        if self.require_special and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
            errors.append("Password must contain special character")

        # Check against common passwords
        if password.lower() in self.common_passwords:
            errors.append("Password is too common")

        return len(errors) == 0, errors

# Usage
policy = PasswordPolicy()

passwords = [
    "weak",
    "StillWeak123",
    "BetterP@ssw0rd!",
    "password123"
]

for pwd in passwords:
    is_valid, errors = policy.validate(pwd)
    print(f"\nPassword: {pwd}")
    print(f"Valid: {is_valid}")
    if errors:
        for error in errors:
            print(f"  - {error}")

Session-Based Authentication

Traditional web authentication using server-side sessions.

How Sessions Work

  1. User logs in with credentials
  2. Server creates session and stores session data
  3. Server sends session ID to client (usually in cookie)
  4. Client includes session ID in subsequent requests
  5. Server looks up session data by session ID

Implementation

import secrets
from datetime import datetime, timedelta
import json

class SessionManager:
    """Server-side session management"""

    def __init__(self):
        # In production, use Redis or database
        self.sessions = {}
        self.session_timeout = timedelta(hours=2)

    def create_session(self, user_id, user_data=None):
        """Create new session for user"""

        # Generate cryptographically secure session ID
        session_id = secrets.token_urlsafe(32)

        # Store session data
        self.sessions[session_id] = {
            'user_id': user_id,
            'user_data': user_data or {},
            'created_at': datetime.now(),
            'last_activity': datetime.now(),
            'ip_address': self.get_client_ip(),
            'user_agent': self.get_user_agent()
        }

        return session_id

    def get_session(self, session_id):
        """Retrieve session data"""

        session = self.sessions.get(session_id)

        if not session:
            return None

        # Check if session has expired
        if datetime.now() - session['last_activity'] > self.session_timeout:
            self.destroy_session(session_id)
            return None

        # Update last activity time
        session['last_activity'] = datetime.now()

        return session

    def destroy_session(self, session_id):
        """Destroy session (logout)"""
        if session_id in self.sessions:
            del self.sessions[session_id]

    def get_client_ip(self):
        """Get client IP address"""
        # Implementation depends on framework
        return "192.168.1.100"

    def get_user_agent(self):
        """Get client user agent"""
        # Implementation depends on framework
        return "Mozilla/5.0..."

# Flask example
from flask import Flask, request, make_response

app = Flask(__name__)
session_manager = SessionManager()

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    # Authenticate user
    user = authenticate_user(username, password)

    if not user:
        return {'error': 'Invalid credentials'}, 401

    # Create session
    session_id = session_manager.create_session(
        user_id=user.id,
        user_data={'username': user.username, 'role': user.role}
    )

    # Set session cookie
    response = make_response({'success': True})
    response.set_cookie(
        'session_id',
        session_id,
        httponly=True,    # Prevents JavaScript access
        secure=True,      # Only sent over HTTPS
        samesite='Strict' # CSRF protection
    )

    return response

@app.route('/logout', methods=['POST'])
def logout():
    session_id = request.cookies.get('session_id')

    if session_id:
        session_manager.destroy_session(session_id)

    response = make_response({'success': True})
    response.set_cookie('session_id', '', expires=0)
    return response

@app.route('/api/protected')
def protected_route():
    # Get session ID from cookie
    session_id = request.cookies.get('session_id')

    # Validate session
    session = session_manager.get_session(session_id)

    if not session:
        return {'error': 'Unauthorized'}, 401

    # Session is valid, process request
    return {
        'message': 'Access granted',
        'user': session['user_data']['username']
    }

Session Security Best Practices

class SecureSessionManager(SessionManager):
    """Enhanced session manager with security features"""

    def create_session(self, user_id, user_data=None):
        """Create session with enhanced security"""

        session_id = super().create_session(user_id, user_data)

        # Store IP address and user agent for validation
        session = self.sessions[session_id]
        session['ip_address'] = self.get_client_ip()
        session['user_agent'] = self.get_user_agent()

        return session_id

    def get_session(self, session_id):
        """Get session with security checks"""

        session = super().get_session(session_id)

        if not session:
            return None

        # Check if IP address changed (optional, can break with mobile users)
        # if session['ip_address'] != self.get_client_ip():
        #     self.destroy_session(session_id)
        #     return None

        # Check if user agent changed (sign of session hijacking)
        if session['user_agent'] != self.get_user_agent():
            self.destroy_session(session_id)
            return None

        return session

    def rotate_session_id(self, old_session_id):
        """
        Rotate session ID to prevent session fixation attacks
        Call this after privilege escalation (e.g., after login)
        """
        session = self.sessions.get(old_session_id)

        if not session:
            return None

        # Create new session ID
        new_session_id = secrets.token_urlsafe(32)

        # Copy session data to new ID
        self.sessions[new_session_id] = session

        # Delete old session
        del self.sessions[old_session_id]

        return new_session_id

Token-Based Authentication

Modern approach using tokens instead of server-side sessions. Popular for APIs and single-page applications.

JSON Web Tokens (JWT)

JWTs are self-contained tokens that encode user information and claims.

Structure: header.payload.signature

  • Header: Token type and algorithm
  • Payload: Claims (user data, expiration, etc.)
  • Signature: Cryptographic signature to verify integrity
import jwt
from datetime import datetime, timedelta

class JWTAuthManager:
    """JWT-based authentication"""

    def __init__(self, secret_key):
        self.secret_key = secret_key
        self.algorithm = 'HS256'
        self.token_expiry = timedelta(hours=1)
        self.refresh_expiry = timedelta(days=7)

    def create_access_token(self, user_id, user_data=None):
        """Create access token"""

        payload = {
            'user_id': user_id,
            'user_data': user_data or {},
            'exp': datetime.utcnow() + self.token_expiry,
            'iat': datetime.utcnow(),  # Issued at
            'type': 'access'
        }

        token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
        return token

    def create_refresh_token(self, user_id):
        """Create refresh token (longer-lived, for getting new access tokens)"""

        payload = {
            'user_id': user_id,
            'exp': datetime.utcnow() + self.refresh_expiry,
            'iat': datetime.utcnow(),
            'type': 'refresh'
        }

        token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
        return token

    def verify_token(self, token, token_type='access'):
        """Verify and decode token"""

        try:
            payload = jwt.decode(
                token,
                self.secret_key,
                algorithms=[self.algorithm]
            )

            # Verify token type
            if payload.get('type') != token_type:
                return None

            return payload

        except jwt.ExpiredSignatureError:
            # Token has expired
            return None
        except jwt.InvalidTokenError:
            # Token is invalid
            return None

    def refresh_access_token(self, refresh_token):
        """Generate new access token from refresh token"""

        payload = self.verify_token(refresh_token, token_type='refresh')

        if not payload:
            return None

        # Create new access token
        return self.create_access_token(payload['user_id'])

# Usage example
jwt_manager = JWTAuthManager(secret_key='your-secret-key-here')

# Login: Create tokens
user_id = 42
access_token = jwt_manager.create_access_token(
    user_id=user_id,
    user_data={'username': 'alice', 'role': 'user'}
)
refresh_token = jwt_manager.create_refresh_token(user_id)

print(f"Access Token: {access_token}")
print(f"Refresh Token: {refresh_token}")

# Verify token
payload = jwt_manager.verify_token(access_token)
print(f"\nDecoded payload: {payload}")

# Refresh access token when it expires
new_access_token = jwt_manager.refresh_access_token(refresh_token)
print(f"\nNew Access Token: {new_access_token}")

Using JWT in Web APIs

from flask import Flask, request, jsonify
from functools import wraps

app = Flask(__name__)
jwt_manager = JWTAuthManager(secret_key='your-secret-key')

def require_jwt(f):
    """Decorator to protect routes with JWT"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        # Get token from Authorization header
        auth_header = request.headers.get('Authorization')

        if not auth_header:
            return jsonify({'error': 'No authorization header'}), 401

        # Expected format: "Bearer <token>"
        parts = auth_header.split()

        if len(parts) != 2 or parts[0].lower() != 'bearer':
            return jsonify({'error': 'Invalid authorization header'}), 401

        token = parts[1]

        # Verify token
        payload = jwt_manager.verify_token(token)

        if not payload:
            return jsonify({'error': 'Invalid or expired token'}), 401

        # Add user info to request context
        request.user_id = payload['user_id']
        request.user_data = payload.get('user_data', {})

        return f(*args, **kwargs)

    return decorated_function

@app.route('/api/login', methods=['POST'])
def api_login():
    username = request.json.get('username')
    password = request.json.get('password')

    # Authenticate user
    user = authenticate_user(username, password)

    if not user:
        return jsonify({'error': 'Invalid credentials'}), 401

    # Create tokens
    access_token = jwt_manager.create_access_token(
        user_id=user.id,
        user_data={'username': user.username, 'role': user.role}
    )
    refresh_token = jwt_manager.create_refresh_token(user.id)

    return jsonify({
        'access_token': access_token,
        'refresh_token': refresh_token,
        'token_type': 'Bearer'
    })

@app.route('/api/refresh', methods=['POST'])
def api_refresh():
    refresh_token = request.json.get('refresh_token')

    new_access_token = jwt_manager.refresh_access_token(refresh_token)

    if not new_access_token:
        return jsonify({'error': 'Invalid refresh token'}), 401

    return jsonify({'access_token': new_access_token})

@app.route('/api/protected')
@require_jwt
def protected_api():
    # User info available from decorator
    return jsonify({
        'message': f'Hello {request.user_data["username"]}',
        'user_id': request.user_id
    })

JWT vs Sessions

FeatureJWTSessions
StorageClient-sideServer-side
ScalabilityExcellent (stateless)Requires session store
RevocationDifficult (need blacklist)Easy (delete session)
SizeLarger (sent with each request)Small (just session ID)
SecurityVulnerable if secret leakedMore control on server
Use CaseAPIs, microservicesTraditional web apps

OAuth 2.0 Basics

OAuth is a protocol for authorization, allowing third-party apps to access user resources without sharing passwords.

OAuth Roles

  • Resource Owner: User who owns the data
  • Client: Application requesting access
  • Authorization Server: Issues tokens
  • Resource Server: Hosts protected resources

Authorization Code Flow

Most secure OAuth flow for web applications:

import requests
from urllib.parse import urlencode

class OAuthClient:
    """OAuth 2.0 Client Implementation"""

    def __init__(self, client_id, client_secret, redirect_uri):
        self.client_id = client_id
        self.client_secret = client_secret
        self.redirect_uri = redirect_uri

        # OAuth provider endpoints
        self.auth_url = 'https://provider.com/oauth/authorize'
        self.token_url = 'https://provider.com/oauth/token'
        self.api_base = 'https://api.provider.com'

    def get_authorization_url(self, state):
        """
        Step 1: Generate authorization URL

        User is redirected here to grant permission
        """
        params = {
            'client_id': self.client_id,
            'redirect_uri': self.redirect_uri,
            'response_type': 'code',
            'scope': 'read:user read:email',
            'state': state  # CSRF protection
        }

        return f"{self.auth_url}?{urlencode(params)}"

    def exchange_code_for_token(self, code):
        """
        Step 2: Exchange authorization code for access token

        Called after user is redirected back with code
        """
        data = {
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'code': code,
            'redirect_uri': self.redirect_uri,
            'grant_type': 'authorization_code'
        }

        response = requests.post(self.token_url, data=data)
        response.raise_for_status()

        return response.json()
        # Returns: {
        #   'access_token': '...',
        #   'token_type': 'Bearer',
        #   'expires_in': 3600,
        #   'refresh_token': '...',
        #   'scope': 'read:user read:email'
        # }

    def get_user_info(self, access_token):
        """
        Step 3: Use access token to get user info
        """
        headers = {
            'Authorization': f'Bearer {access_token}'
        }

        response = requests.get(
            f'{self.api_base}/user',
            headers=headers
        )
        response.raise_for_status()

        return response.json()

# Flask example of OAuth flow
from flask import Flask, redirect, request, session
import secrets

app = Flask(__name__)
app.secret_key = 'your-secret-key'

oauth_client = OAuthClient(
    client_id='your-client-id',
    client_secret='your-client-secret',
    redirect_uri='http://localhost:5000/callback'
)

@app.route('/login/oauth')
def oauth_login():
    # Generate state token for CSRF protection
    state = secrets.token_urlsafe(32)
    session['oauth_state'] = state

    # Redirect user to OAuth provider
    auth_url = oauth_client.get_authorization_url(state)
    return redirect(auth_url)

@app.route('/callback')
def oauth_callback():
    # Verify state to prevent CSRF
    state = request.args.get('state')
    if state != session.get('oauth_state'):
        return 'Invalid state parameter', 400

    # Get authorization code
    code = request.args.get('code')
    if not code:
        return 'No code provided', 400

    # Exchange code for token
    token_data = oauth_client.exchange_code_for_token(code)
    access_token = token_data['access_token']

    # Get user info
    user_info = oauth_client.get_user_info(access_token)

    # Create session for user
    session['user_id'] = user_info['id']
    session['username'] = user_info['username']

    return redirect('/dashboard')

Role-Based Access Control (RBAC)

Assign permissions to roles, then assign roles to users.

from enum import Enum
from typing import Set

class Permission(Enum):
    """Define available permissions"""
    READ_POST = 'read:post'
    CREATE_POST = 'create:post'
    UPDATE_POST = 'update:post'
    DELETE_POST = 'delete:post'
    READ_USER = 'read:user'
    UPDATE_USER = 'update:user'
    DELETE_USER = 'delete:user'
    ADMIN_PANEL = 'admin:panel'

class Role:
    """Role with assigned permissions"""

    def __init__(self, name: str, permissions: Set[Permission]):
        self.name = name
        self.permissions = permissions

    def has_permission(self, permission: Permission) -> bool:
        return permission in self.permissions

# Define roles
ROLES = {
    'user': Role('user', {
        Permission.READ_POST,
        Permission.CREATE_POST,
        Permission.UPDATE_POST,  # Own posts only
        Permission.READ_USER,
    }),

    'moderator': Role('moderator', {
        Permission.READ_POST,
        Permission.CREATE_POST,
        Permission.UPDATE_POST,  # Any post
        Permission.DELETE_POST,  # Any post
        Permission.READ_USER,
        Permission.UPDATE_USER,
    }),

    'admin': Role('admin', {
        # All permissions
        *list(Permission)
    })
}

class User:
    def __init__(self, user_id, username, role_name):
        self.id = user_id
        self.username = username
        self.role = ROLES[role_name]

    def has_permission(self, permission: Permission) -> bool:
        return self.role.has_permission(permission)

# Authorization decorator
from functools import wraps
from flask import jsonify, request

def require_permission(permission: Permission):
    """Decorator to require specific permission"""
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            # Get current user (from session/JWT)
            user = get_current_user(request)

            if not user:
                return jsonify({'error': 'Unauthorized'}), 401

            if not user.has_permission(permission):
                return jsonify({'error': 'Forbidden'}), 403

            return f(*args, **kwargs)
        return decorated_function
    return decorator

# Usage
@app.route('/api/posts', methods=['POST'])
@require_permission(Permission.CREATE_POST)
def create_post():
    # User has CREATE_POST permission
    return jsonify({'message': 'Post created'})

@app.route('/api/posts/<int:post_id>', methods=['DELETE'])
@require_permission(Permission.DELETE_POST)
def delete_post(post_id):
    # User has DELETE_POST permission
    # Additional check: is user owner or moderator?
    user = get_current_user(request)
    post = get_post(post_id)

    if post.author_id != user.id and user.role.name != 'moderator':
        return jsonify({'error': 'Can only delete your own posts'}), 403

    # Delete post
    return jsonify({'message': 'Post deleted'})

Multi-Factor Authentication (MFA)

Adding additional authentication factors beyond passwords.

Time-Based One-Time Password (TOTP)

import pyotp
import qrcode
from io import BytesIO

class TOTPManager:
    """Manage TOTP-based 2FA"""

    def generate_secret(self, user_email):
        """Generate TOTP secret for user"""
        # Create random secret
        secret = pyotp.random_base32()

        # Create provisioning URI for QR code
        totp = pyotp.TOTP(secret)
        uri = totp.provisioning_uri(
            name=user_email,
            issuer_name='MyApp'
        )

        return secret, uri

    def generate_qr_code(self, uri):
        """Generate QR code image"""
        qr = qrcode.QRCode(version=1, box_size=10, border=5)
        qr.add_data(uri)
        qr.make(fit=True)

        img = qr.make_image(fill_color="black", back_color="white")

        # Convert to bytes
        buffer = BytesIO()
        img.save(buffer, format='PNG')
        buffer.seek(0)

        return buffer.getvalue()

    def verify_totp(self, secret, code):
        """Verify TOTP code"""
        totp = pyotp.TOTP(secret)
        return totp.verify(code, valid_window=1)

# Example flow
totp_manager = TOTPManager()

# When user enables 2FA
user_email = "alice@example.com"
secret, uri = totp_manager.generate_secret(user_email)

# Store secret in database (encrypted!)
# db.execute("UPDATE users SET totp_secret = ? WHERE email = ?", (secret, user_email))

# Show QR code to user to scan with authenticator app
qr_image = totp_manager.generate_qr_code(uri)

# When user logs in, after password verification
user_code = input("Enter 6-digit code from authenticator: ")
is_valid = totp_manager.verify_totp(secret, user_code)

if is_valid:
    print("2FA verification successful")
else:
    print("Invalid code")

Exercises

Basic Exercises

  1. Password Hashing: Implement a user registration and login system using bcrypt for password hashing.

  2. Session Management: Build a simple session manager that:

    • Creates sessions with random IDs
    • Stores user data
    • Expires sessions after 30 minutes of inactivity
  3. JWT Claims: Create a JWT token with custom claims (user role, organization) and verify those claims are present when decoding.

Intermediate Exercises

  1. Complete Auth System: Build a complete authentication system with:

    • User registration with password validation
    • Login with session creation
    • Logout
    • Protected routes
    • Password reset flow (email with token)
  2. RBAC Implementation: Implement role-based access control with:

    • At least 3 roles (user, moderator, admin)
    • At least 5 different permissions
    • Decorator to protect routes by permission
    • Admin interface to manage user roles
  3. Token Refresh Flow: Implement JWT token refresh mechanism:

    • Short-lived access tokens (15 minutes)
    • Long-lived refresh tokens (7 days)
    • Endpoint to refresh access token
    • Automatic token refresh in client

Advanced Exercises

  1. OAuth Provider: Build a simple OAuth 2.0 authorization server that:

    • Implements authorization code flow
    • Issues access and refresh tokens
    • Validates tokens
    • Provides user info endpoint
  2. Attribute-Based Access Control: Implement ABAC where authorization depends on:

    • User attributes (department, level)
    • Resource attributes (owner, visibility)
    • Environmental attributes (time, location)
  3. Security Hardening: Take an existing auth system and add:

    • Brute force protection (rate limiting, account lockout)
    • Session fixation protection
    • CSRF protection
    • Account enumeration prevention
    • Suspicious activity detection

Summary

Authentication and authorization are complex but critical security domains:

  1. Password Security: Never store plaintext passwords. Use modern hashing algorithms like bcrypt or Argon2 with proper salting. Enforce strong password policies.

  2. Sessions: Traditional session-based auth stores state on the server. Secure cookies with HttpOnly, Secure, and SameSite flags. Implement session timeouts and rotation.

  3. JWT Tokens: Stateless authentication suitable for APIs and distributed systems. Shorter-lived access tokens with longer-lived refresh tokens. Be aware of limitations around token revocation.

  4. OAuth 2.0: Standard protocol for delegated authorization. Allows users to grant third-party access without sharing passwords. Authorization code flow is most secure for web apps.

  5. Access Control: RBAC assigns permissions to roles. Implement principle of least privilege. Always check authorization in addition to authentication.

  6. Multi-Factor Authentication: Adds significant security beyond passwords. TOTP is widely supported and relatively easy to implement.

Key Takeaways

  • Authentication (who you are) and authorization (what you can do) are distinct
  • Password hashing is mandatory, use bcrypt or Argon2
  • Sessions are stateful, JWTs are stateless - choose based on your needs
  • Always validate tokens/sessions on every request
  • Implement defense in depth: passwords + 2FA, authentication + authorization
  • OAuth solves the delegation problem without password sharing

Further Reading

  • OWASP Authentication Cheat Sheet
  • RFC 6749: OAuth 2.0 Authorization Framework
  • RFC 7519: JSON Web Token (JWT)
  • "OAuth 2 in Action" by Justin Richer and Antonio Sanso
  • NIST Digital Identity Guidelines (SP 800-63)

Next Steps

Continue to 04-vulnerabilities.md to learn about common security vulnerabilities and how to prevent them in your code.