Skip to content

Authentication Logging and Audit — Tracking Auth Events for Security and Compliance

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Authentication Logging and Audit. We cover key concepts, practical examples, and best practices to help you master this topic.

Authentication logging records every authentication event — successes, failures, token issuances, and revocations — in a structured format for security monitoring, compliance auditing, and Incident Response.

What You'll Learn

Structured auth event logging, audit trail requirements for SOC2 and GDPR, log aggregation with ELK/Datadog, suspicious pattern detection, and alerting on auth anomalies.

Why It Matters

Without auth logging, you cannot detect brute force attacks, credential stuffing, or token theft. Audit logs are required for SOC2, HIPAA, and PCI-DSS compliance. Structured logs enable automated threat detection.

Real-World Use

AWS CloudTrail logs every API authentication event. Auth0 provides detailed auth logs. Durga Antivirus Pro logs all authentication events with structured JSON, feeding into a SIEM for real-time threat detection.

Code Example: Structured Auth Logging Middleware

import json, time, uuid
from flask import Flask, request, g, has_request_context
import logging

app = Flask(__name__)

# Configure structured JSON logging
class JSONFormatter(logging.Formatter):
    def format(self, record):
        log_entry = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage()
        }
        if hasattr(record, "auth_event"):
            log_entry["auth"] = record.auth_event
        return json.dumps(log_entry)

handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)

def log_auth_event(event_type, **details):
    """Log a structured authentication event."""
    event = {
        "event_id": str(uuid.uuid4()),
        "event_type": event_type,
        "timestamp": time.time(),
        "ip": request.remote_addr if has_request_context() else "unknown",
        "user_agent": request.headers.get("User-Agent", "") if has_request_context() else "",
        **details
    }

    app.logger.info(f"Auth event: {event_type}", extra={"auth_event": event})

@app.before_request
def log_auth_attempt():
    """Log authentication attempts before processing."""
    if request.path.startswith("/api/auth/"):
        auth_header = request.headers.get("Authorization", "")
        has_token = bool(auth_header and auth_header.startswith("Bearer "))

        if request.path == "/api/auth/login":
            username = request.json.get("username", "") if request.is_json else ""
            log_auth_event(
                "login_attempt",
                username=username,
                method="password",
                endpoint=request.path
            )

@app.after_request
def log_auth_result(response):
    """Log authentication results after processing."""
    if request.path.startswith("/api/auth/"):
        if response.status_code == 401:
            log_auth_event(
                "login_failure",
                status_code=401,
                endpoint=request.path
            )
        elif response.status_code == 200 and "login" in request.path:
            log_auth_event(
                "login_success",
                status_code=200,
                endpoint=request.path
            )
    return response

Code Example: Audit Trail for Token Operations

import sqlite3

class AuthAuditTrail:
    """Stores immutable audit records of auth events."""

    def __init__(self, db_path="auth_audit.db"):
        self.conn = sqlite3.connect(db_path)
        self._create_table()

    def _create_table(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS auth_audit (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                event_type TEXT NOT NULL,
                user_id TEXT,
                ip_address TEXT,
                user_agent TEXT,
                details TEXT,
                hash TEXT NOT NULL
            )
        """)
        self.conn.commit()

    def _compute_hash(self, row_data):
        """Compute SHA-256 hash of the previous row + current data for tamper evidence."""
        prev_hash = self.conn.execute(
            "SELECT hash FROM auth_audit ORDER BY id DESC LIMIT 1"
        ).fetchone()
        prev_hash = prev_hash[0] if prev_hash else "0" * 64
        return hashlib.sha256(
            (prev_hash + json.dumps(row_data, sort_keys=True)).encode()
        ).hexdigest()

    def record(self, event_type, user_id=None, ip_address=None,
               user_agent=None, details=None):
        """Record an immutable audit entry."""
        import datetime
        row = {
            "timestamp": datetime.datetime.utcnow().isoformat(),
            "event_type": event_type,
            "user_id": user_id,
            "ip_address": ip_address,
            "user_agent": user_agent,
            "details": json.dumps(details) if details else None
        }

        row["hash"] = self._compute_hash(row)

        self.conn.execute("""
            INSERT INTO auth_audit
                (timestamp, event_type, user_id, ip_address, user_agent, details, hash)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, tuple(row.values()))
        self.conn.commit()

        return row

    def verify_integrity(self):
        """Verify the audit trail has not been tampered with."""
        rows = self.conn.execute(
            "SELECT id, timestamp, event_type, user_id, ip_address, "
            "user_agent, details, hash FROM auth_audit ORDER BY id"
        ).fetchall()

        prev_hash = "0" * 64
        for row in rows:
            row_dict = {
                "timestamp": row[1],
                "event_type": row[2],
                "user_id": row[3],
                "ip_address": row[4],
                "user_agent": row[5],
                "details": row[6]
            }
            expected_hash = hashlib.sha256(
                (prev_hash + json.dumps(row_dict, sort_keys=True)).encode()
            ).hexdigest()

            if expected_hash != row[7]:
                return False, f"Tampering detected at row {row[0]}"

            prev_hash = row[7]

        return True, "Audit trail intact"

audit = AuthAuditTrail()

# Usage during login
def login_with_audit():
    user = authenticate_user(request.json)
    if user:
        token = issue_token(user)
        audit.record(
            event_type="token_issued",
            user_id=user["id"],
            ip_address=request.remote_addr,
            user_agent=request.headers.get("User-Agent"),
            details={"token_type": "access", "expires_in": 900}
        )
        return jsonify({"access_token": token})
    else:
        audit.record(
            event_type="login_failed",
            user_id=request.json.get("username"),
            ip_address=request.remote_addr,
            details={"reason": "invalid_password"}
        )
        return jsonify({"error": "Invalid credentials"}), 401

Code Example: Suspicious Activity Detection

import time
from collections import defaultdict, deque

class SuspiciousActivityDetector:
    """Detect suspicious auth patterns and trigger alerts."""

    def __init__(self):
        self.failed_attempts = defaultdict(lambda: deque(maxlen=100))
        self.alert_handlers = []

    def on_alert(self, handler):
        self.alert_handlers.append(handler)

    def _trigger_alert(self, alert_type, details):
        for handler in self.alert_handlers:
            handler(alert_type, details)

    def record_attempt(self, username, ip_address, success):
        """Record an auth attempt and check for suspicious patterns."""
        now = time.time()

        if not success:
            self.failed_attempts[ip_address].append(now)
            self.failed_attempts[username].append(now)

        # Check: brute force from single IP
        ip_failures = [
            t for t in self.failed_attempts[ip_address]
            if t > now - 300
        ]
        if len(ip_failures) >= 10:
            self._trigger_alert("brute_force_ip", {
                "ip": ip_address,
                "attempts": len(ip_failures),
                "window": "5 minutes"
            })

        # Check: credential stuffing (many usernames from one IP)
        username_count = len(set(
            self.failed_attempts.keys()
        ))
        if username_count > 20 and not success:
            pass

        # Check: impossible travel (same user from different geo)
        # Requires geo-IP lookup integration
        if success:
            self._check_impossible_travel(username, ip_address)

    def _check_impossible_travel(self, username, ip_address):
        """Detect if the same user logs in from distant locations rapidly."""
        pass  # Implement with geo-IP database


detector = SuspiciousActivityDetector()

@detector.on_alert
def send_security_alert(alert_type, details):
    """Send alert to security team."""
    print(f"[ALERT] {alert_type}: {json.dumps(details)}")
    # In production: send to Slack, PagerDuty, or SIEM

@app.after_request
def detect_suspicious(response):
    if request.path == "/api/auth/login":
        username = request.json.get("username", "")
        success = response.status_code == 200
        detector.record_attempt(username, request.remote_addr, success)
    return response

Common Mistakes

1. Logging Passwords or Tokens

Never log plaintext passwords, full tokens, or secrets. Log token IDs (jti), truncated tokens, or hashes. Token truncation: first 8 chars + "...".

2. No Tamper Protection

Standard logs can be modified by an attacker. Use hash chaining (Blockchain-style) for audit trails. Each entry includes the hash of the previous entry.

3. Logging Without Context

Log entries missing IP, user agent, timestamp, or event ID are useless for investigation. Always include at least: timestamp, event type, user identifier, source IP.

4. Ignoring Log Storage Costs

Auth logs grow quickly. Implement log rotation and retention policies. Keep detailed logs for 90 days, summary logs for 1 year (or as required by compliance).

5. No Log Monitoring

Collecting logs without monitoring is security theater. Set up alerts for: >5 failed logins in 1 minute, logins from new locations, logins outside business hours.

Practice Questions

  1. What should be included in every auth log entry?
  2. How does hash chaining prevent audit trail tampering?
  3. What patterns indicate a brute force attack in auth logs?
  4. What is the difference between logging and auditing?
  5. Why should passwords never appear in auth logs?

Answers:

  1. Timestamp, event type, user identifier, source IP, user agent, success/failure status, event ID. Include request path and response status code.
  2. Each entry includes the hash of the previous entry. Changing any entry breaks all subsequent hashes. Tampering is detected by recomputing the chain.
  3. Multiple failed logins (10+) from a single IP in 5 minutes, or failed logins for many different usernames from one IP in a short window.
  4. Logging captures events for debugging and monitoring. Auditing creates an immutable, tamper-evident record for compliance and legal purposes.
  5. If the log is compromised, passwords in plaintext are exposed. Hash or mask sensitive fields. Log the user identifier instead of the password.

Challenge: Build a complete auth logging and audit system with structured JSON logging, tamper-evident audit trail with hash chaining, suspicious activity detection with alerts, and a log viewer dashboard.

FAQ

What auth events should be logged?

Login attempts (success and failure), token issuance, token refresh, token revocation, password changes, MFA setup, MFA verification, account lockout, and permission changes.

How long should auth logs be retained?

90 days for detailed logs, 1 year for summarized logs, 7 years for compliance (HIPAA, PCI-DSS). Check your specific regulatory requirements.

What is a SIEM and do I need one?

A Security Information and Event Management system aggregates and analyzes logs from multiple sources. For small APIs, ELK stack (Elasticsearch, Logstash, Kibana) is sufficient.

How do I handle GDPR right-to-be-forgotten with audit logs?

Audit logs are exempt from deletion if needed for security purposes. Pseudonymize user identifiers after the retention period.

Should I log API key usage?

Yes. Log which API key was used, by which service, for which endpoint, and at what time. Hash the key and log the key ID.

Mini Project

Build a Flask API with structured JSON auth logging, tamper-evident SQLite audit trail with hash chaining, suspicious activity detection (brute force, credential stuffing), and a simple dashboard for reviewing auth events.

What's Next

Now learn about Authentication Security and OWASP Top 10 for securing your authentication implementation against common vulnerabilities.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro