Detection rules › Elastic

Okta Successful Login After Credential Attack

Status
production
Severity
high
Time window
6h
Group by
Esql.user
Author
Elastic
Source
github.com/elastic/detection-rules

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

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

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 =
ifkibana.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
elsenull
Esql.attack_ts =
ifkibana.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
elsenull
Esql.is_attack_alert =
ifkibana.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
else0
Esql.is_success_login =
ifdata_stream.dataset == "okta.system" AND okta.outcome.result == "SUCCESS"1
else0
Esql.login_ip =
ifdata_stream.dataset == "okta.system" AND okta.outcome.result == "SUCCESS"Esql.source_ip
elsenull
Esql.login_ts =
ifdata_stream.dataset == "okta.system" AND okta.outcome.result == "SUCCESS"@timestamp
elsenull

Stage 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.

FieldKindValues
Esql.attack_countgt
  • 0
Esql.attack_to_login_minutesle
  • 180
Esql.latest_attacklt
  • Esql.earliest_login
Esql.login_countgt
  • 0
Esql.useris_not_null
  • (no value, null check)
data_stream.dataseteq
  • okta.system
event.actioneq
  • user.session.start
event.actionwildcard
  • user.authentication.*
kibana.alert.rule.rule_idin
  • 23f18264-2d6d-11ef-9413-f661ea17fbce
  • 2d3c27d5-d133-4152-8102-8d051619ec4a
  • 42bf698b-4738-445b-8231-c834ddefd8a0
  • 5889760c-9858-4b4b-879c-e299df493295
  • 94e734c0-2cda-11ef-84e1-f661ea17fbce
okta.actor.alternate_idis_not_null
  • (no value, null check)
okta.outcome.resulteq
  • SUCCESS

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.

FieldSource
Esql.*KEEP Esql.*