Skip to content

How to Configure Audit Logging

Structured JSON logging for all authorization decisions. Designed for compliance (SOC2, HIPAA, GDPR).

Basic Configuration

from fastapi_topaz import TopazConfig, AuditLogger

config = TopazConfig(
    authorizer_options=AuthorizerOptions(url="localhost:8282"),
    policy_path_root="myapp",
    identity_provider=get_identity,
    policy_instance_name="myapp",

    audit_logger=AuditLogger(
        log_allowed=True,
        log_denied=True,
        log_skipped=False,
        log_unauthenticated=True,
        log_manual_checks=False,
        include_resource_context=True,  # off by default: may contain user data
    ),
)

Event Types

authorization.middleware.allowed      # Policy check passed
authorization.middleware.denied       # Policy check failed (403)
authorization.middleware.skipped      # Route excluded
authorization.middleware.unauthenticated  # No identity (401)
authorization.dependency.allowed      # Dependency check passed
authorization.dependency.denied       # Dependency check failed
authorization.check.allowed           # is_allowed() returned True
authorization.check.denied            # is_allowed() returned False

Audit events are emitted from the core check_decision path, so every authorization check produces exactly one event regardless of how it was triggered — middleware, dependency, or manual API (is_allowed, check_relation, ...). Manual-check events require log_manual_checks=True. With log_skipped=True, requests that bypass authorization (excluded routes or methods) emit authorization.middleware.skipped events at level_skipped.

Log Schema

Allowed Access

{
  "timestamp": "2024-01-15T10:30:00.123Z",
  "level": "INFO",
  "event": "authorization.middleware.allowed",
  "request_id": "req-789",
  "identity": {
    "type": "sub",
    "value": "user-123"
  },
  "authorization": {
    "policy_path": "myapp.GET.documents.__id",
    "decision": "allowed",
    "cached": false,
    "latency_ms": 2.3
  },
  "request": {
    "method": "GET",
    "path": "/documents/456",
    "ip": "192.168.1.100"
  }
}

Denied Access

{
  "timestamp": "2024-01-15T10:31:00.456Z",
  "level": "WARN",
  "event": "authorization.denied",
  "identity": {"type": "sub", "value": "user-456"},
  "authorization": {"policy_path": "myapp.DELETE.documents", "decision": "denied"},
  "request": {"method": "DELETE", "path": "/documents/789"}
}

Custom Handler

Send to SIEM (Splunk, Datadog):

async def send_to_splunk(event: AuditEvent) -> None:
    await splunk_client.send(event.to_dict())

config = TopazConfig(
    ...
    audit_logger=AuditLogger(handler=send_to_splunk),
)

Multiple outputs:

async def multi_handler(event: AuditEvent) -> None:
    logger.info(event.to_json())
    await splunk_client.send(event.to_dict())
    metrics.increment("auth_decisions", tags={"decision": event.decision})

config = TopazConfig(
    audit_logger=AuditLogger(handler=multi_handler),
)

Query Examples

-- Who accessed document-123 in the last 24 hours?
SELECT identity.value, timestamp, authorization.decision
FROM auth_logs
WHERE resource.object_id = 'document-123'
  AND timestamp > NOW() - INTERVAL '24 hours'

-- Denied requests by user
SELECT identity.value, COUNT(*) as denied_count
FROM auth_logs
WHERE authorization.decision = 'denied'
GROUP BY identity.value
ORDER BY denied_count DESC

Configuration Reference

Parameter Type Default Description
log_allowed bool True Log successful authorizations
log_denied bool True Log denied authorizations
log_skipped bool False Log excluded routes as authorization.middleware.skipped events
log_unauthenticated bool True Log 401 events
log_manual_checks bool False Log is_allowed() calls
include_resource_context bool False Include resource details; resource context often carries user data (emails, names, document attributes), so review what your providers put in it before enabling
include_request_headers bool False Include HTTP request headers in events; authorization, proxy-authorization, cookie, x-api-key, x-auth-token and x-csrf-token values are redacted
handler Callable None Custom async handler

Failure Handling and Redaction

Audit logging never changes the authorization outcome. If a handler raises, or the default logger cannot serialize an event, the error is logged with its traceback through the fastapi_topaz.audit logger and the request continues with the decision already made. to_json() serializes values that JSON does not support (such as datetime or custom objects in resource context) with str().

When the identity type is IDENTITY_TYPE_JWT, identity.value is written as [REDACTED], because the value is a bearer token. Other identity types (such as IDENTITY_TYPE_SUB) keep their value.

Client IP Caveat

The client_ip field in audit events is taken from the x-forwarded-for / x-real-ip headers when present, falling back to the socket peer address. These headers are client-spoofable unless a trusted reverse proxy in front of your app strips or overwrites them. Do not treat client_ip as authenticated for forensics unless your deployment guarantees those headers.

See Also