Without audit logs for authentication events, you cannot detect an ongoing attack, reconstruct a breach timeline, or satisfy incident response obligations. CWE-778 (Insufficient Logging) and OWASP A09 (Security Logging and Monitoring Failures) document that undetected breaches are the norm, not the exception, for applications with no auth logging. NIST 800-53 AU-2 requires auditable events to include logon attempts, account changes, and authorization failures. A password reset initiated by an attacker, account lockouts triggered by credential stuffing, or privilege escalation — none of these are visible without structured, persistent auth event logging.
Info because missing auth logs don't cause a breach, but they guarantee that any breach — in progress or completed — goes undetected and unrecoverable.
Log auth events to a persistent database table with timestamp, event type, user ID, and IP address:
// lib/audit-log.ts
type AuditEvent =
| 'auth.login.success' | 'auth.login.failure'
| 'auth.logout' | 'auth.account_locked'
| 'auth.password_reset.requested' | 'authz.forbidden'
export async function logAuditEvent(
event: AuditEvent,
ctx: { userId?: string; email?: string; ip: string }
) {
await db.auditLog.create({
data: { event, userId: ctx.userId ?? null, ipAddress: ctx.ip, createdAt: new Date() },
})
}
// In login handler:
await logAuditEvent('auth.login.failure', { email, ip })
Do not rely on console.log — container restarts discard stdout. Log to a database table or a structured logging service (Axiom, Datadog) with at least 90-day retention.
ID: security-hardening.infra-monitoring.security-logging
Severity: info
What to look for: Enumerate every authentication event type (login success, login failure, logout, password change, MFA enrollment). For each, check whether authentication events (successful logins, failed logins, password changes, account lockouts, MFA events) and authorization failures are logged with timestamps, user identifiers, and IP addresses. Verify that logs are written to a persistent store, not just stdout that gets discarded.
Pass criteria: Auth events (login success/failure, password reset, role changes) are logged with timestamp, user ID or email, IP, and outcome. Authorization failures (403 responses) are captured. Logs are written to a persistent logging service or database — at least 4 auth event types must be logged. Report: "X auth event types found, all Y are logged with timestamp and user ID."
Fail criteria: No auth event logging. Only stdout/console.log without persistence. Logs lack user context or IP address.
Skip (N/A) when: The application is in early prototype stage with no production users. Or auth is fully managed by a provider that handles audit logging (Clerk, Auth0 dashboard).
Detail on fail: "No auth event logging found — failed login attempts, password resets, and account lockouts not recorded" or "Events logged to console.log only — logs not persisted; production logs discarded after container restart"
Remediation: Implement structured auth audit logging:
// lib/audit-log.ts
type AuditEvent =
| 'auth.login.success'
| 'auth.login.failure'
| 'auth.logout'
| 'auth.password_reset.requested'
| 'auth.password_reset.completed'
| 'auth.account_locked'
| 'authz.forbidden'
export async function logAuditEvent(
event: AuditEvent,
context: { userId?: string; email?: string; ip: string; metadata?: Record<string, unknown> }
) {
await db.auditLog.create({
data: {
event,
userId: context.userId ?? null,
email: context.email ?? null,
ipAddress: context.ip,
metadata: context.metadata ?? {},
createdAt: new Date(),
},
})
}
// In login handler:
await logAuditEvent('auth.login.failure', {
email: req.body.email,
ip: req.headers.get('x-forwarded-for') ?? '0.0.0.0',
metadata: { reason: 'invalid_password' },
})