New-login security alerts ("we noticed a new login to your account").
On every login (PhoenixKitWeb.Users.Auth.log_in_user/3), the request's
(ip_address, user_agent_hash) pair is checked against
PhoenixKit.Users.Auth.KnownDevice rows for that user, and a row is
persisted for every new pair either way (still used to enrich the
self-service "Active Sessions" list with browser/OS/location per
session — see PhoenixKit.Users.Sessions.list_user_device_sessions/2).
A user.new_login_detected activity entry is always logged too, for the
audit trail.
The reader-facing alarms — the email
(PhoenixKit.Users.Auth.UserNotifier.deliver_new_login_alert/2, gated
behind new_login_alert_enabled) and the in-app notification — are
narrower than "unrecognized pair", though, and are skipped when either:
- this is the very first
KnownDevicerow the account has ever had (see below), or - the browser/OS (
user_agent_hashalone, regardless of IP) HAS been seen before for this account — an IP alone changing is not "a new device" from the user's point of view. Most residential/mobile connections don't have a static IP, so alerting on IP change alone fires on a large fraction of logins from an already-trusted browser and trains people to ignore the email — the exact alert-fatigue failure this codebase already avoids elsewhere (seePhoenixKit.Utils.SessionFingerprint, which treats an IP-only mismatch as a mere warning, never an alarm).
The first-device skip exists because registration ends by logging the new
user in through this exact path (log_in_user/3), and an account with no
device history yet cannot help but treat its own signup as "a new device" —
without it, every signup on an installation with alerts on immediately
received a "we noticed a new login" security email about the login it just
performed to finish registering. The device is still recorded (so the
second login, from anywhere else, correctly reads as new), and the
activity entry still logs for the audit trail — only the two
reader-facing alarms are suppressed.
A recognized (ip, ua) pair just bumps last_seen_at — no alert, no email.
Sends synchronously (matching every other PhoenixKit auth email — confirmation, password reset, magic link — none of which are queued through Oban): a "new device" login is inherently rare per user (every subsequent login from the same device is silent), so the odd extra round-trip on a first-time login doesn't justify background-job infrastructure this feature would otherwise be the only user of. A send failure is logged and swallowed — it must never block sign-in.
Summary
Functions
Records a login from conn for user, alerting on a new device.
Whether new-login alerts are enabled (setting new_login_alert_enabled,
default false).
Best-effort "City, Country" string for ip_address, or nil.
Functions
@spec check(map(), Plug.Conn.t()) :: :ok
Records a login from conn for user, alerting on a new device.
No-ops entirely (no DB write, no email) when the feature is disabled. Never raises — a failure here must never block sign-in.
@spec enabled?() :: boolean()
Whether new-login alerts are enabled (setting new_login_alert_enabled,
default false).
Best-effort "City, Country" string for ip_address, or nil.
Never raises; a lookup failure (disabled, rate-limited, invalid IP) just means the alert email omits the location line.