Detection rules › Elastic
Okta Successful Login After Credential Attack
Correlates Okta credential attack alerts with subsequent successful authentication for the same user account, identifying potential compromise following brute force, password spray, or credential stuffing attempts.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
| Credential Access | T1110.001 Brute Force: Password Guessing, T1110.003 Brute Force: Password Spraying, T1110.004 Brute Force: Credential Stuffing |
Event coverage
| Provider | Event |
|---|---|
| Okta-user | user.session.start |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Failed Logins from Unknown or Invalid User (Kusto)
- First Occurrence of Okta User Session Started via Proxy (Elastic)
- High-Risk Admin Activity (Kusto)
- Multiple Okta Sessions Detected for a Single User (Elastic)
- Multiple Okta User Authentication Events with Same Device Token Hash (Elastic)
- New Device/Location sign-in along with critical operation (Kusto)
- Okta AiTM Session Cookie Replay (Elastic)
- Okta Login Signal (Panther)
Rule body elastic
[metadata]
creation_date = "2026/02/12"
integration = ["okta"]
maturity = "production"
updated_date = "2026/04/10"
[rule]
author = ["Elastic"]
description = """
Correlates Okta credential attack alerts with subsequent successful authentication for the same user account,
identifying potential compromise following brute force, password spray, or credential stuffing attempts.
"""
false_positives = [
"A user experiencing legitimate login issues (forgotten password, typos) may trigger credential attack alerts before successfully authenticating.",
"Automated password reset flows where a user fails multiple times then succeeds after resetting their password.",
]
from = "now-6h"
interval = "30m"
language = "esql"
license = "Elastic License v2"
name = "Okta Successful Login After Credential Attack"
note = """## Triage and analysis
### Investigating Okta Successful Login After Credential Attack
This rule correlates credential attack alerts with subsequent successful authentication for the same user account. The correlation is user-centric, capturing IP rotation scenarios where attackers may login from a different IP after obtaining credentials.
#### Possible investigation steps
- Identify the user account and review the timeline between the attack and successful login.
- Compare the attack source IPs versus the login source IP to identify potential IP rotation.
- Review the original credential attack alert to understand the scope and nature of the attack.
- Check the authentication method used and whether MFA was required and satisfied.
- Review the session activity following the successful login for signs of account takeover.
- Verify with the user if the login was legitimate.
### False positive analysis
- Users experiencing legitimate login issues may trigger attack alerts before successfully authenticating.
- Automated password reset flows where a user fails multiple times then succeeds after resetting may trigger this rule.
- The rule correlates on user identity only, so it fires when a user is targeted and later logs in, even if from different IPs.
### Response and remediation
- If compromise is suspected, reset the user's password and revoke all active sessions.
- Reset MFA if the attacker may have enrolled their own device.
- Block the source IP at the network perimeter.
- Review the user's recent activity for signs of lateral movement or data access.
- Check for persistence mechanisms such as new OAuth apps, API tokens, or enrolled devices.
"""
references = [
"https://support.okta.com/help/s/article/Troubleshooting-Distributed-Brute-Force-andor-Password-Spray-attacks-in-Okta",
"https://www.okta.com/identity-101/brute-force/",
"https://developer.okta.com/docs/reference/api/system-log/",
"https://developer.okta.com/docs/reference/api/event-types/",
"https://www.elastic.co/security-labs/testing-okta-visibility-and-detection-dorothy",
"https://www.elastic.co/security-labs/monitoring-okta-threats-with-elastic-security",
"https://www.elastic.co/security-labs/starter-guide-to-understanding-okta",
]
risk_score = 73
rule_id = "50742e15-c5ef-49c8-9a2d-31221d45af58"
setup = """## Setup
This rule requires the following:
1. The Okta Fleet integration, Filebeat module, or similarly structured data for Okta System Logs.
2. The correlated credential attack detection rules must be enabled (at least one):
- Potential Okta Credential Stuffing (Single Source) (94e734c0-2cda-11ef-84e1-f661ea17fbce)
- Potential Okta Password Spray (Single Source) (42bf698b-4738-445b-8231-c834ddefd8a0)
- Potential Okta Brute Force (Device Token Rotation) (23f18264-2d6d-11ef-9413-f661ea17fbce)
- Potential Okta Brute Force (Multi-Source) (5889760c-9858-4b4b-879c-e299df493295)
- Potential Okta Password Spray (Multi-Source) (2d3c27d5-d133-4152-8102-8d051619ec4a)
3. Alerts from these rules must be written to the `.alerts-security.*` indices.
The rule queries both alert indices and Okta log indices to correlate attack alerts with successful logins."""
severity = "high"
tags = [
"Domain: Identity",
"Use Case: Identity and Access Audit",
"Use Case: Threat Detection",
"Data Source: Okta",
"Data Source: Okta System Logs",
"Tactic: Credential Access",
"Tactic: Initial Access",
"Resources: Investigation Guide",
"Rule Type: Higher-Order Rule",
]
timestamp_override = "event.ingested"
type = "esql"
query = '''
FROM .alerts-security.*, logs-okta.system-* METADATA _id, _version, _index
// Filter for credential attack alerts OR successful Okta authentications
| WHERE
(
// Credential attack alerts from the five correlated rules
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce", // Credential Stuffing
"42bf698b-4738-445b-8231-c834ddefd8a0", // Password Spraying
"23f18264-2d6d-11ef-9413-f661ea17fbce", // DT Brute Force
"5889760c-9858-4b4b-879c-e299df493295", // Distributed Brute Force
"2d3c27d5-d133-4152-8102-8d051619ec4a" // Distributed Spray
)
)
OR (
// Successful Okta authentication events
data_stream.dataset == "okta.system"
AND (event.action LIKE "user.authentication.*" OR event.action == "user.session.start")
AND okta.outcome.result == "SUCCESS"
AND okta.actor.alternate_id IS NOT NULL
)
// correlation - alerts may store user/IP in different fields than raw logs
| EVAL
Esql.user = COALESCE(okta.actor.alternate_id, user.name, user.email),
Esql.source_ip = COALESCE(okta.client.ip, client.ip, source.ip)
// Must have user identity to correlate
| WHERE Esql.user IS NOT NULL
// Classify events and capture timestamps/IPs by event type
| EVAL
Esql.is_attack_alert = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), 1, 0
),
Esql.is_success_login = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", 1, 0
),
Esql.attack_ip = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), Esql.source_ip, null
),
Esql.login_ip = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", Esql.source_ip, null
),
Esql.attack_ts = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), @timestamp, null
),
Esql.login_ts = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", @timestamp, null
)
// Aggregate by user (catches IP rotation: spray from IP A, login from IP B)
| STATS
Esql.attack_count = SUM(Esql.is_attack_alert),
Esql.login_count = SUM(Esql.is_success_login),
Esql.earliest_attack = MIN(Esql.attack_ts),
Esql.latest_attack = MAX(Esql.attack_ts),
Esql.earliest_login = MIN(Esql.login_ts),
Esql.latest_login = MAX(Esql.login_ts),
Esql.attack_source_ips = VALUES(Esql.attack_ip),
Esql.login_source_ips = VALUES(Esql.login_ip),
Esql.all_source_ips = VALUES(Esql.source_ip),
Esql.alert_rule_ids = VALUES(kibana.alert.rule.rule_id),
Esql.alert_rule_names = VALUES(kibana.alert.rule.name),
Esql.event_action_values = VALUES(event.action),
Esql.geo_country_values = VALUES(client.geo.country_name),
Esql.geo_city_values = VALUES(client.geo.city_name),
Esql.source_asn_values = VALUES(source.as.number),
Esql.source_asn_org_values = VALUES(source.as.organization.name),
Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),
Esql.device_values = VALUES(okta.client.device),
Esql.is_proxy_values = VALUES(okta.security_context.is_proxy)
BY Esql.user
// Calculate time gap between latest attack and earliest subsequent login
| EVAL Esql.attack_to_login_minutes = DATE_DIFF("minute", Esql.latest_attack, Esql.earliest_login)
// Correlation: attack BEFORE login + success within reasonable window (3 hours)
| WHERE
Esql.attack_count > 0
AND Esql.login_count > 0
AND Esql.latest_attack < Esql.earliest_login
AND Esql.attack_to_login_minutes <= 180
| SORT Esql.login_count DESC
| KEEP Esql.*
'''
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1110"
name = "Brute Force"
reference = "https://attack.mitre.org/techniques/T1110/"
[[rule.threat.technique.subtechnique]]
id = "T1110.001"
name = "Password Guessing"
reference = "https://attack.mitre.org/techniques/T1110/001/"
[[rule.threat.technique.subtechnique]]
id = "T1110.003"
name = "Password Spraying"
reference = "https://attack.mitre.org/techniques/T1110/003/"
[[rule.threat.technique.subtechnique]]
id = "T1110.004"
name = "Credential Stuffing"
reference = "https://attack.mitre.org/techniques/T1110/004/"
[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"
[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"
[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"
[rule.threat.tactic]
id = "TA0001"
name = "Initial Access"
reference = "https://attack.mitre.org/tactics/TA0001/"
Stages and Predicates
Stage 1: from
FROM .alerts-security.*, logs-okta.system-* METADATA _id, _version, _index
Stage 2: where
| WHERE
(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
)
)
OR (
data_stream.dataset == "okta.system"
AND (event.action LIKE "user.authentication.*" OR event.action == "user.session.start")
AND okta.outcome.result == "SUCCESS"
AND okta.actor.alternate_id IS NOT NULL
)
Stage 3: eval
| EVAL
Esql.user = COALESCE(okta.actor.alternate_id, user.name, user.email),
Esql.source_ip = COALESCE(okta.client.ip, client.ip, source.ip)
Stage 4: where
| WHERE Esql.user IS NOT NULL
Stage 5: eval
| EVAL
Esql.is_attack_alert = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), 1, 0
),
Esql.is_success_login = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", 1, 0
),
Esql.attack_ip = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), Esql.source_ip, null
),
Esql.login_ip = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", Esql.source_ip, null
),
Esql.attack_ts = CASE(
kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
), @timestamp, null
),
Esql.login_ts = CASE(
data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS", @timestamp, null
)
Esql.attack_ip =kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
)Esql.source_ipnullEsql.attack_ts =kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
)@timestampnullEsql.is_attack_alert =kibana.alert.rule.rule_id IN (
"94e734c0-2cda-11ef-84e1-f661ea17fbce",
"42bf698b-4738-445b-8231-c834ddefd8a0",
"23f18264-2d6d-11ef-9413-f661ea17fbce",
"5889760c-9858-4b4b-879c-e299df493295",
"2d3c27d5-d133-4152-8102-8d051619ec4a"
)10Esql.is_success_login =data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS"10Esql.login_ip =data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS"Esql.source_ipnullEsql.login_ts =data_stream.dataset == "okta.system"
AND okta.outcome.result == "SUCCESS"@timestampnullStage 6: stats
| STATS
Esql.attack_count = SUM(Esql.is_attack_alert),
Esql.login_count = SUM(Esql.is_success_login),
Esql.earliest_attack = MIN(Esql.attack_ts),
Esql.latest_attack = MAX(Esql.attack_ts),
Esql.earliest_login = MIN(Esql.login_ts),
Esql.latest_login = MAX(Esql.login_ts),
Esql.attack_source_ips = VALUES(Esql.attack_ip),
Esql.login_source_ips = VALUES(Esql.login_ip),
Esql.all_source_ips = VALUES(Esql.source_ip),
Esql.alert_rule_ids = VALUES(kibana.alert.rule.rule_id),
Esql.alert_rule_names = VALUES(kibana.alert.rule.name),
Esql.event_action_values = VALUES(event.action),
Esql.geo_country_values = VALUES(client.geo.country_name),
Esql.geo_city_values = VALUES(client.geo.city_name),
Esql.source_asn_values = VALUES(source.as.number),
Esql.source_asn_org_values = VALUES(source.as.organization.name),
Esql.user_agent_values = VALUES(okta.client.user_agent.raw_user_agent),
Esql.device_values = VALUES(okta.client.device),
Esql.is_proxy_values = VALUES(okta.security_context.is_proxy)
BY Esql.user
Stage 7: eval
| EVAL Esql.attack_to_login_minutes = DATE_DIFF("minute", Esql.latest_attack, Esql.earliest_login)
Stage 8: where
| WHERE
Esql.attack_count > 0
AND Esql.login_count > 0
AND Esql.latest_attack < Esql.earliest_login
AND Esql.attack_to_login_minutes <= 180
Stage 9: sort
| SORT Esql.login_count DESC
Stage 10: keep
| KEEP Esql.*
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
Esql.attack_count | gt |
|
Esql.attack_to_login_minutes | le |
|
Esql.latest_attack | lt |
|
Esql.login_count | gt |
|
Esql.user | is_not_null | |
data_stream.dataset | eq |
|
event.action | eq |
|
event.action | wildcard |
|
kibana.alert.rule.rule_id | in |
|
okta.actor.alternate_id | is_not_null | |
okta.outcome.result | eq |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
Esql.* | KEEP Esql.* |