Skip to main content

Documentation

No results found.
Features

Dashboard Login Protection (LoginShield)

Escalating brute-force protection for the dashboard login, layered on top of Fortify's rate limiter. Normal sign-ins never see it — password-manager autofill stays friction-free.

The layers

  1. Rate limiting (always on, Fortify). POST /login is throttled to 5 attempts/minute per email + IP (FortifyServiceProvider). The 2FA challenge (5/min per pending session) and passkey login (6/min per IP) have their own limiters.
  2. Failure counting + activity log. Every failed attempt on the dashboard guard increments two decaying counters — per email and per source IP — via TrackLoginShieldFailures listening to the auth Failed event. The ActivityLog feature records each attempt (auth category, login_failed, with email/IP/user-agent). A successful sign-in clears the email counter only; the IP counter is left to decay so one compromised account can't reset an attacking IP's state.
  3. Escalating anti-bot challenge. Once either counter reaches challenge_threshold (default 3 within a 15-minute window), the Fortify login pipeline pipe EnsureLoginShieldChallenge requires the POST to carry a valid Spam Shield payload: an HMAC-signed single-use token bound to IP + user agent, a SHA-256 proof-of-work, and a clean automation fingerprint (no navigator.webdriver). Verified by LoginShield::verifyPayload() — deliberately WITHOUT the form shield's min-interactions / time-on-page checks, which would flag password-manager autofill. The pipe runs before the two-factor redirect so an armed bot can't use the 2FA redirect as a credential-validity oracle. Rejections re-increment the counters (blocked requests fire no Failed event), keeping the armed state fresh during an attack.
  4. Admin alerting. At alert_threshold (default 10) the admin-or-higher users are emailed (LoginAttackAlertMail) — at most once per key per window — and a login_attack_detected entry is written to the activity log.

Client flow

The login page renders the challenge assets only when armed for the visitor (IP counter over threshold, or the session flag flashed by a bounced POST) — see the $loginShieldActive block in login.blade.php. login-shield.js arms the shared SpamShield client (challenge fetched from /_fc/challenge under the reserved form id -2, PoW solved in a background worker while the visitor types) and fills the hidden shield_payload input on submit.

Worst-case human UX: a visitor targeted by email from a different IP gets one "Additional verification is required" bounce, after which the page arms and subsequent submits solve silently. With JavaScript disabled an armed visitor cannot pass the challenge (a <noscript> notice says so); passkeys and admin SSO are unaffected side doors, and the armed state decays with the window.

Configuration

Editable from Dashboard → Settings → Admin Sign-In → Login Protection (the card writes security.login_shield_* Settings). Each Setting overrides the matching config/cms.php → security.login_shield key, which supplies the default and stays env-overridable:

Setting Config key Env Default
security.login_shield_enabled enabled CMS_LOGIN_SHIELD true
security.login_shield_challenge_threshold challenge_threshold CMS_LOGIN_SHIELD_CHALLENGE_THRESHOLD 3 failures
security.login_shield_alert_threshold alert_threshold CMS_LOGIN_SHIELD_ALERT_THRESHOLD 10 failures
security.login_shield_window_minutes window_minutes CMS_LOGIN_SHIELD_WINDOW_MINUTES 15
security.login_shield_alert_email alert_email CMS_LOGIN_SHIELD_ALERT_EMAIL true

Scoped to the Fortify (web) guard — feature-module logins (Memberships) keep their own protections. Counters live in the cache, so they reset on cache:clear; that only disarms the challenge until the next failures.

Tests: tests/Feature/Auth/LoginShieldTest.php.

LoginShield defends the real login form against someone guessing a password. The bulk of hostile traffic never reaches it — it's bots probing for WordPress paths that don't exist here (/wp-login.php, /xmlrpc.php). Those are refused at the web-server layer, with an opt-in pre-boot guard for servers that ignore .htaccess; see Shedding bot traffic before it costs anything.