Logging and Monitoring
Why logging matters for security
You cannot defend against what you cannot see. Without logs, a brute-force attack looks like normal traffic. A successful account takeover is invisible. A password reset flood goes unnoticed.
Security logging records the events that tell you something is wrong. Rate limit hits, failed logins, account lockouts, password resets, suspicious patterns — these are the signals.
What to log
Throughout this course, we have added log() calls at key points. Here is the complete list of security events worth logging:
Authentication events
log("login_success", { email, ip, userId });
log("login_failed", { email, ip, reason: "wrong_password" });
log("login_failed", { email, ip, reason: "user_not_found" });
log("logout", { userId, ip }); Rate limiting and lockout
log("rate_limited", { key: "ip", ip, email });
log("rate_limited", { key: "email", ip, email });
log("login_locked", { email, ip }); Password events
log("reset_token_created", { email });
log("reset_requested_unknown_email", { email, ip });
log("reset_failed", { ip, reason: "invalid_or_expired_token" });
log("password_reset", { userId, ip });
log("password_changed", { userId, ip }); Token events
log("refresh_token_reuse", { family, userId });
log("token_revoked", { jti, userId }); Request logging
log("request", { method, path, status, duration, ip });
log("unhandled_error", { error, ip }); What NEVER to log
[!WARNING] Never log these values. If your logs are compromised, these become attack material.
Passwords. Not the plain text, not the hash. Logging the request body of a login endpoint would capture the password.
// WRONG
log("login_attempt", { email, password }); // password in logs!
log("login_attempt", { body: c.input.body }); // body contains password!
// CORRECT
log("login_attempt", { email }); Session IDs. If session IDs appear in logs and the logs are compromised, the attacker can hijack sessions.
// WRONG
log("session_created", { sessionId, userId });
// CORRECT
log("session_created", { userId }); Access tokens and refresh tokens. Same reason as session IDs.
Reset tokens. Same reason — a leaked reset token in logs could be used to reset a password.
Full request bodies for sensitive endpoints. Login, signup, password change, and reset endpoints all receive sensitive data in the body. Log the event, not the data.
Patterns to watch for
With structured logging in place, you can build alerts for:
Brute-force: Many login_failed events for the same email in a short period.
Credential stuffing: Many login_failed events from the same IP with different emails.
Account takeover: A login_success from an unusual IP or country, followed by a password_changed.
Password reset flood: Many reset_token_created events for the same email.
Token theft: A refresh_token_reuse event (someone used a token that was already consumed).
In a production system, you would feed these logs into a monitoring tool (Datadog, Grafana, CloudWatch) and set up alerts. For now, console.log with JSON output gives you the foundation.
Exercises
Exercise 1: Review all the log() calls in your codebase. Make sure none of them include passwords, tokens, or session IDs. Search for password, token, session in your log calls.
Exercise 2: Simulate a brute-force attack (send many failed login requests) and look at the log output. Can you identify the attack from the logs alone?
Exercise 3: Add a log("signup", { email, ip }) call to the signup route. What else would you log on signup? (Consider: duplicate email attempts, validation failures.)
Why should you never log passwords, even hashed ones?