Disclaimer: All research and opinions expressed here are my own and are independent of any employer or organisation.

FOCI Cross-Resource Token Pivot — Non-Interactive Sign-In Burst

hunt-2025-044 v1.0 Entra ID · Azure created Paul Newton

Hypothesis

Microsoft's Family of Client IDs (FOCI) allows a refresh token obtained via one first-party application to be exchanged for access tokens scoped to any other service in the family — Exchange, Teams, Azure Management, Key Vault, Office Management — without re-authenticating the user. After capturing a single device code token, an attacker can silently pivot to every Microsoft service the victim can access with a single API call per resource. This produces a burst of non-interactive sign-ins from the same client_id against multiple distinct resource endpoints within a short window — a pattern that does not occur in legitimate first-party application behaviour. FOCI-capable client IDs include Azure CLI (04b07795-8542-4bc9-aaaa-59d79c0a3df9), Azure PowerShell (1950a258-227b-4e31-a9cf-717495945fc2), and Microsoft Authentication Broker (29d9ed98-a469-4536-ade2-f981bc1d605e).

FOCI Multi-Resource Token Exchange Burst

Detects a single OAuth client_id being used to exchange a refresh token for access tokens against three or more distinct resource endpoints within a 10-minute window, via non-interactive sign-ins. This is the direct behavioural signature of FOCI pivoting — one captured token silently unlocked across multiple Microsoft services.

KQL
let FOCIAppIds = dynamic([
    "04b07795-8542-4bc9-aaaa-59d79c0a3df9",  // Azure CLI
    "1950a258-227b-4e31-a9cf-717495945fc2",  // Azure PowerShell
    "29d9ed98-a469-4536-ade2-f981bc1d605e",  // Microsoft Authentication Broker
    "d3590ed6-52b3-4102-aeff-aad2292ab01c",  // Microsoft Office
    "04b07795-8ddb-461a-bbee-02f9e1bf7b46"   // Azure CLI (alt)
]);
let CIDRASN = externaldata(CIDR:string, CIDRASN:int, CIDRASNName:string)
    ['https://firewalliplists.gypthecat.com/lists/kusto/kusto-cidr-asn.csv.zip']
    with (ignoreFirstRecord=true);
let GraphLogs =
    MicrosoftGraphActivityLogs
    | where TimeGenerated > ago(1h)
    | evaluate ipv4_lookup(CIDRASN, IPAddress, CIDR, return_unmatched=true)
    | extend GraphASN = CIDRASN, GraphASNName = CIDRASNName
    | project SignInActivityId, GraphIPAddress = IPAddress, GraphASN, GraphASNName,
              ResponseStatusCode, UserAgent, RequestMethod, RequestUri;
let AADLogs =
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(1h)
    | where ResultType == 0
    | where AppId in (FOCIAppIds)
    | evaluate ipv4_lookup(CIDRASN, IPAddress, CIDR, return_unmatched=true)
    | extend ASN = CIDRASN, ASNName = CIDRASNName;
AADLogs
| join kind=leftouter (GraphLogs) on $left.CorrelationId == $right.SignInActivityId
| summarize
    Resources     = make_set(ResourceDisplayName),
    ResourceCount = dcount(ResourceDisplayName),
    FirstSeen     = min(TimeGenerated),
    LastSeen      = max(TimeGenerated),
    IPs           = make_set(IPAddress),
    ASNs          = make_set(ASNName),
    UserAgents    = make_set(UserAgent),
    RequestUris   = make_set(RequestUri),
    SignInCount   = count(),
    AppIdCount    = dcount(AppId)
    by UserPrincipalName, AppId, AppDisplayName, bin(TimeGenerated, 10m)
| where ResourceCount >= 3
| where AppIdCount == 1
| extend SpanMinutes = datetime_diff('minute', LastSeen, FirstSeen)
| project FirstSeen, LastSeen, SpanMinutes,
          UserPrincipalName, AppDisplayName, AppId,
          AppIdCount, ResourceCount, Resources,
          IPs, ASNs, UserAgents, RequestUris, SignInCount
| order by FirstSeen desc

Triage steps

  1. 1 Review AppId against known FOCI client IDs — Azure CLI (04b07795-8542-4bc9-aaaa-59d79c0a3df9), Azure PowerShell (1950a258-227b-4e31-a9cf-717495945fc2), Microsoft Authentication Broker (29d9ed98-a469-4536-ade2-f981bc1d605e), Microsoft Office (d3590ed6-52b3-4102-aeff-aad2292ab01c)
  2. 2 Check IPs against the user's normal sign-in baseline — attacker FOCI pivots will originate from the C2 server or residential proxy, not the user's device
  3. 3 Review Resources — a pivot covering Exchange + Teams + Azure Management in one burst is high confidence; Exchange only may be lower confidence
  4. 4 Check whether a device code sign-in (interactive, AuthenticationProtocol == "deviceCode") for the same user preceded this burst within the last hour
  5. 5 If confirmed, revoke all refresh tokens via Entra and audit downstream Graph API activity (mail search, file access, Teams messages) for the affected user
  6. 6 Review AADNonInteractiveUserSignInLogs for the same user over the prior 30 days to establish whether this AppId has been used before

True positive example

FirstSeen
2026-05-01T14:02:00Z
LastSeen
2026-05-01T14:04:37Z
SpanMinutes
2
UserPrincipalName
[email protected]
AppDisplayName
Microsoft Azure CLI
AppId
04b07795-8542-4bc9-aaaa-59d79c0a3df9
ResourceCount
6
Resources
["Microsoft Graph","Exchange / Outlook","MS Teams","Azure Management","Azure Key Vault","Office Management"]
IPs
["45.142.212.100"]
SignInCount
6

Six non-interactive token exchanges for a FOCI-eligible client (Azure CLI) against all six AUTHOV-targeted resources within 2 minutes, all from a single datacenter IP not in the user's baseline. A device code sign-in for the same user to Azure CLI was recorded 8 minutes prior. This is the textbook FOCI pivot pattern following a successful device code phish.

Back to all hunts