Okta Password Spraying Detection in the System Log
Detect password spraying, brute force and credential stuffing against Okta: failure patterns per IP and user, ThreatInsight events, and the success to hunt.
TL;DR. Password spraying is many users, few attempts each, from the same source: failed user.session.start or user.authentication.verify events grouped by client.ipAddress, touching eight or more accounts within an hour at no more than about three attempts per account. Brute force is the opposite: many failures on one account. Both matter only when followed by a success from the same source, so hunt for that success first. Distributed attacks through residential proxies defeat per-IP grouping; pivot on ASN, user agent and ThreatInsight events too.
Three attacks, three shapes
| Attack | Shape in the log | ATT&CK |
|---|---|---|
| Password spraying | one source, many users, 1–3 attempts each | T1110.003 |
| Password guessing (brute force) | many attempts on one user | T1110.001 |
| Credential stuffing | known username/password pairs, spread over many sources, high success rate for reused passwords | T1110.004 |
Spraying stays below lockout thresholds on purpose: a handful of common passwords per account, across hundreds of accounts. That is why per-user lockout is not a detection.
Okta's Identity Threat Research team reported large credential stuffing campaigns in spring 2024 that were routed through Tor and residential proxies, so that the traffic appeared to come from ordinary users' devices rather than from VPS providers (Okta Security: how to block anonymizing services). Keep that in mind when a spray "comes from" hundreds of consumer IPs.
The events
| eventType | What to read |
|---|---|
user.session.start with outcome.result = FAILURE | outcome.reason (for example INVALID_CREDENTIALS), IP, ASN, user agent |
user.authentication.verify with FAILURE | same |
user.session.start with SUCCESS from the same source | the compromise |
user.account.lock | lockouts triggered by the attempts |
security.threat.detected | request from an IP ThreatInsight classed as malicious |
security.attack.start | ThreatInsight detected that the org is under attack |
security.breached_credential.detected | a credential associated with a known breach was used |
security.threat.detected and security.attack.start are the two queries Okta Security suggests for monitoring these campaigns (source). When ThreatInsight is in "log and enforce" mode, requests from IPs it rates as high threat may be blocked before authentication and never become user.session.start failures, which changes what your counts mean.
How the analyzer detects it
| Rule | Group by | Threshold | Escalation |
|---|---|---|---|
access.password_spray | client IP | ≥ 8 distinct users with failures within 1 h, average ≤ 3 failures per user → high | a successful user.session.start from that IP within 1 h → critical |
access.brute_force | user | ≥ 10 failures within 15 min → medium | a successful sign-in within 30 min → high |
threat.threatinsight | client IP | any ThreatInsight event, or debugData.threatSuspected = true → medium | — |
threat.breached_credential | — | any security.breached_credential.detected → medium | — |
The "average attempts per user" condition is what separates a spray from a noisy NAT. An office egress IP where many people mistype their password on Monday morning produces many users with one failure each, but it also produces a steady stream of successes for those same users. Look at the success ratio before you panic.
Hunting a spray the rules might miss
The per-IP rule is precise but easy to evade with a proxy network. Complement it by hand:
- Group failures by ASN instead of IP (
securityContext.asNumber). A hosting ASN with failures against 50 of your users is a spray even if every request uses a different IP. - Group by user agent. Spraying tools often use one fixed, sometimes outdated, user-agent string across all their IPs.
- Look at the targeted usernames. Sprays walk alphabetical lists, include former employees, or follow a naming pattern (
firstname.lastname) guessed from public sources. - Find the successes. For every IP, ASN or user agent that appears in the failures, list its successful sign-ins. That short list is your incident.
- Check what the successful accounts did next: MFA prompts (fatigue attempt?), factor enrollment, SSO.
In the analyzer, the Entities tab lists IPs with ASN, country, event count and failure count: sort by failures, then click Events on the top sources.
Reading outcome.reason
Not every failure means the same thing, and the reason field is where a spray and a user problem part ways. INVALID_CREDENTIALS on many accounts from one source is the spray itself. Failures that end in user.account.lock show the attacker overshot your lockout policy, which is useful for scoping (which accounts now need unlocking) but also tells you they were not careful. A cluster of failures for one user from their own usual network, followed by a success from the same network, is a person who forgot a password change, not an attack. Always read the reason together with the source.
If a sign-in succeeded
A successful password against an account that requires MFA means the attacker now needs the second factor, which is exactly the setup for MFA fatigue or a helpdesk reset. Act before they get it:
- Reset the password and clear sessions for every account with a success from the attacking source.
- Check each for pushes, factor enrollments, and SSO after the success.
- Block the source with a blocklist network zone; block anonymizers with an enhanced dynamic zone.
- Tell the affected users: their password is known, and it may be reused elsewhere.
Prevention
- ThreatInsight in "Log and enforce security based on threat level" mode rather than log-only (Okta Help Center).
- Enhanced dynamic zones to block anonymizers, Tor and proxies at sign-in, a control Okta Security recommends for these campaigns (source).
- Passwordless and phishing-resistant authenticators: there is no password to spray.
- Breached password protection where available, and user education on reuse.