Documentation & Tools →
Sign In
Free Cheatsheet

Entra ID Security Cheatsheet

The identity plane is the perimeter. How attackers hit Entra ID, credentials, tokens, conditional access, privileged roles, app and workload identities, how to detect each in KQL, and how to harden against it. No account needed.

These run in Microsoft Sentinel (Logs) and the Entra portal's log views. Identity is where most modern intrusions begin and persist, so the detections here target the identity stack directly: the sign-in, the token, the role, the app. Tune thresholds and org values to your tenant before alerting.

The identity attack surface

An attacker in Entra ID is after one of a few things: valid credentials, a live token, a privileged role, or a foothold in an application or workload identity that outlives the user. Each maps to a detection surface. The sections below follow that surface, the highest-value identity detections, each as a real query you can adapt.

Attacker goalWhere it shows
Valid credentialsSigninLogs: spray, stuffing, anomalous success, risk detections.
A live session tokenSigninLogs: AiTM proxy indicators, concurrent sessions, token replay.
A privileged roleAuditLogs: direct role adds, PIM bypass, standing privilege.
App / workload footholdAuditLogs / AADServicePrincipalSignInLogs: client secrets, federated creds, OAuth grants.
A trusted outside identityExternal / B2B guest activity, redemption, risk.

Sign-in log forensics

SigninLogs is the primary identity evidence source, but it has two streams: interactive (a human at a prompt) and non-interactive (tokens, refresh, background). Attackers using a stolen token surface in the non-interactive stream, so reading both, and comparing them, is where token misuse first shows.

// Detect potential token replay: different IPs in interactive // vs non-interactive for the same user within 1 hour let timeRange = 1h; let interactiveIPs = SigninLogs | where TimeGenerated > ago(timeRange) | where ResultType == 0 | distinct UserPrincipalName, IPAddress; AADNonInteractiveUserSignInLogs | where TimeGenerated > ago(timeRange) | where ResultType == 0 | join kind=inner interactiveIPs on UserPrincipalName | where IPAddress != IPAddress1 | summarize NonInteractiveIP = any(IPAddress), InteractiveIP = any(IPAddress1), EventCount = count() by UserPrincipalName | where EventCount > 5 // Results: users with concurrent sessions from different IPs // Validate: is the non-interactive IP a VPN, proxy, or Microsoft IP?

The tell above: the same user authenticated from two different IPs in a way consistent with a token used somewhere the original sign-in did not originate. Pair it with Identity Protection's own risk scoring, which flags many of these on its own.

// EI5.2: Detailed sign-in risk detection analysis SigninLogs | where TimeGenerated > ago(7d) | where RiskLevelDuringSignIn != "none" and isnotempty(RiskLevelDuringSignIn) | extend RiskDetections = parse_json(RiskEventTypes_V2) | mv-expand RiskDetection = RiskDetections | extend DetectionType = tostring(RiskDetection) | summarize Count = count(), Users = dcount(UserPrincipalName), SuccessCount = countif(ResultType == 0), BlockedCount = countif(ResultType == 53003) by DetectionType, RiskLevelDuringSignIn | order by Count desc // Shows which specific detections are most common in your tenant // SuccessCount > 0 for high-risk detections = risk policies may not be enforcing // BlockedCount > 0 = CA policies are responding to risk signals (good)

Confirms TP: a risk detection that Identity Protection raised on a sign-in that nonetheless succeeded, the attacker got in despite the risk signal, and conditional access did not block it.

Conditional access: attacks and validation

Conditional access is the control attackers most want to evade. Two moves dominate: authenticating through legacy protocols that do not support modern auth (and therefore skip MFA), and weakening or excluding a policy once privileged. Hunt both.

// EI4.2: Detect password spray through legacy protocols SigninLogs | where TimeGenerated > ago(24h) | where ClientAppUsed in ("Exchange ActiveSync", "IMAP4", "POP3", "Authenticated SMTP", "Other clients") | summarize Attempts = count(), SuccessCount = countif(ResultType == 0), FailCount = countif(ResultType != 0), DistinctUsers = dcount(UserPrincipalName), DistinctIPs = dcount(IPAddress) by ClientAppUsed, bin(TimeGenerated, 1h) | where DistinctUsers > 5 and FailCount > 20 | order by Attempts desc // Pattern: many users targeted, many failures, few successes // High DistinctUsers with high FailCount = spray campaign in progress // ANY SuccessCount > 0 = compromised accounts: investigate immediately

Confirms TP: password-spray failures arriving over legacy authentication protocols, which bypass conditional-access MFA entirely. If legacy auth is not blocked tenant-wide, this is your exposure.

Token security

Token theft is the highest-value identity attack because it sidesteps the password and MFA both. Adversary-in-the-middle phishing proxies the real login, captures the issued token, and replays it. The detections target the proxy infrastructure and the replay pattern.

// EI7.3: Detect potential AiTM proxy indicators in sign-in logs SigninLogs | where TimeGenerated > ago(24h) | where ResultType == 0 | where RiskLevelDuringSignIn in ("medium", "high") | extend RiskDetections = tostring(RiskEventTypes_V2) | where RiskDetections has_any ("anomalousToken", "tokenIssuerAnomaly", "unfamiliarFeatures", "impossibleTravel") | extend DeviceOS = tostring(DeviceDetail.operatingSystem) | extend IsCompliant = tostring(DeviceDetail.isCompliant) | extend Browser = tostring(DeviceDetail.browser) | project TimeGenerated, UserPrincipalName, IPAddress, Country = tostring(LocationDetails.countryOrRegion), RiskDetections, DeviceOS, IsCompliant, Browser, AppDisplayName | order by TimeGenerated desc // Indicators of proxy-based theft: // - anomalousToken: token properties inconsistent with legitimate authentication // - Non-compliant device or empty device details (proxy does not report a real device) // - IP from hosting provider (proxy infrastructure, not user's ISP) // - Followed by non-interactive sign-ins from a DIFFERENT IP (token replay)

Confirms TP: sign-in telemetry consistent with AiTM proxy infrastructure, an MFA-satisfied sign-in routed through a cloud-hosting ASN that is not a normal egress for the user.

// EI7.4: Detect concurrent sessions from different IPs (possible cookie theft) SigninLogs | where TimeGenerated > ago(24h) | where ResultType == 0 | where IsInteractive == false // Non-interactive = token renewal | summarize DistinctIPs = dcount(IPAddress), IPs = make_set(IPAddress, 10), Apps = make_set(AppDisplayName, 10) by UserPrincipalName, bin(TimeGenerated, 1h) | where DistinctIPs > 2 // Same user, same hour, 3+ different IPs | order by DistinctIPs desc // Multiple IPs for the same user in the same hour from non-interactive sign-ins // May indicate: VPN switching (legitimate), mobile IP hopping (legitimate), // or cookie replay from attacker infrastructure (investigate) // Check: are any of the IPs hosting providers? (attacker indicator)

Confirms TP: concurrent sessions for one identity from different IPs in a window too tight for legitimate movement, the original session and the attacker's replayed one, live at once.

PIM and privileged identity

Standing privilege is the prize. Privileged Identity Management exists to make privilege just-in-time and audited, so the attacks are: find permanent privileged roles to ride, or add privilege by a direct assignment that skips PIM. Audit the standing roles and watch the activation log.

// EI6.1: Audit all permanent (active) privileged role assignments AuditLogs | where TimeGenerated > ago(1d) | where OperationName == "Add member to role" | extend Target = tostring(TargetResources[0].userPrincipalName) | extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue) // This only shows recent additions. For the complete picture, use Graph API // or the Entra admin center: Identity → Roles and administrators

The audit above lists permanent (always-on) privileged assignments, every one is standing attack surface that should arguably be PIM-eligible instead of active.

// EI6.5: Failed PIM activation attempts AuditLogs | where TimeGenerated > ago(7d) | where OperationName has "PIM" | where Result == "failure" or ResultReason has "denied" or ResultReason has "failed" | extend Actor = tostring(InitiatedBy.user.userPrincipalName) | extend Role = tostring(TargetResources[0].modifiedProperties[1].newValue) | extend FailureReason = tostring(ResultReason) | project TimeGenerated, Actor, Role, FailureReason | order by TimeGenerated desc // Failed activations: the user attempted to activate but could not complete // Common legitimate failures: FIDO2 key not recognized, approval timeout // Suspicious failures: multiple failures from a user who normally activates successfully // Multiple failures across different roles = possible attacker exploring available privileges

Confirms TP: a burst of failed PIM activation attempts can indicate an attacker probing for a role they cannot quite reach, worth correlating with the account's other recent activity.

Applications and workload identity

Apps and workload identities authenticate as themselves, not as a user, so a credential added to an app outlives any password reset on the human who owns it. The two highest-value detections: a new client secret or certificate on an app registration, and federated-credential abuse on a workload identity.

// EI9.3: Detect client secret additions to application registrations AuditLogs | where TimeGenerated > ago(7d) | where OperationName in ("Add service principal credentials", "Update application – Certificates and secrets management") | extend Actor = tostring(InitiatedBy.user.userPrincipalName) | extend AppName = tostring(TargetResources[0].displayName) | extend CredentialDetails = tostring(TargetResources[0].modifiedProperties) | project TimeGenerated, Actor, AppName, OperationName, CredentialDetails | order by TimeGenerated desc // Every credential addition or modification in the last 7 days // EVERY result should be authorized through change management // An unexpected credential addition may indicate: // - An attacker establishing persistence (adding their own secret to an existing app) // - A developer creating a new secret without following the credential policy

Confirms TP: a client secret or certificate added to an application or service principal outside a known change, a classic durable-persistence move, the app now authenticates without the user.

// EI10.5: Monitor federated credential usage across environments AADServicePrincipalSignInLogs | where TimeGenerated > ago(7d) | where ResultType == 0 | where ServicePrincipalName has_any ("app-dev", "app-staging", "app-production", "deploy", "pipeline", "cicd") | extend Environment = case( ServicePrincipalName has "dev", "Development", ServicePrincipalName has "staging" or ServicePrincipalName has "qa", "Staging", ServicePrincipalName has "prod" or ServicePrincipalName has "production", "Production", "Unknown") | summarize SignIns = count(), IPs = make_set(IPAddress, 5), Resources = make_set(ResourceDisplayName, 5) by ServicePrincipalName, Environment | order by Environment, SignIns desc // CI/CD pipeline identity activity grouped by environment // Production identities should only sign in from known CI/CD IP ranges // Development identities signing in to production resources = environment leak (investigate)

Confirms TP: federated-credential usage on a workload identity from an unexpected environment, or a stale federated credential suddenly active, points at workload-identity abuse.

External identities

Guest and B2B identities are a trusted door from outside the tenant. A compromised guest, or an over-permissioned one, is access an attacker did not have to phish for. Inventory and risk-score them.

// EI11.1: Per-guest risk scoring SigninLogs | where TimeGenerated > ago(90d) | where UserType == "Guest" and ResultType == 0 | extend Country = tostring(LocationDetails.countryOrRegion) | extend AuthMethod = tostring(parse_json(AuthenticationDetails)[0].authenticationMethod) | summarize SignIns = count(), DistinctApps = dcount(AppDisplayName), DistinctCountries = dcount(Country), DistinctIPs = dcount(IPAddress), UsesWeakAuth = countif(AuthMethod has "email" or AuthMethod has "oneTimePasscode"), LastSignIn = max(TimeGenerated) by UserPrincipalName, HomeTenantId | extend RiskScore = DistinctApps * 3 // Broad access = higher risk + DistinctCountries * 5 // Multiple countries = higher risk + iff(UsesWeakAuth > 0, 20, 0) // Weak auth = significant risk bump + iff(DistinctIPs > 10, 10, 0) // Many IPs = unusual | order by RiskScore desc // Per-guest risk score: prioritize governance attention // High RiskScore: broad access + multiple countries + weak auth = priority review // Low RiskScore: single app + single country + strong auth = standard governance

Confirms TP: a guest identity with elevated risk, broad access, or activity inconsistent with its expected use is a trust-boundary exposure worth scoping like an internal compromise.

Hardening checklist

Detection finds the attack; configuration prevents it. The defensive baseline for Entra ID, each item closes one of the surfaces above.

ControlCloses
Block legacy authenticationThe MFA-bypass path for spray and stuffing. The single highest-value control.
Phishing-resistant MFARaises the bar against AiTM token theft (FIDO2/passkeys resist proxying).
Conditional access baselineRequire compliant device / managed app; block risky sign-ins; no broad exclusions.
PIM for all privileged rolesRemoves standing privilege; makes elevation just-in-time and audited.
Token / session lifetimeShortens the window a stolen token stays valid; continuous access evaluation.
App & workload restrictionsLimit who can consent to apps; review app credentials; restrict workload-identity federation.
External-identity governanceRestrict B2B, review guest access, expire stale guests.
These are abbreviated real detections. Each query is from the course, shown with its core logic. The full versions, with the conditional-access validation suite, Identity Protection automation, Defender for Identity integration, and the complete hardening design, are in Entra ID Security.
Worked example, AiTM token theft

A user satisfies MFA, but the sign-in routes through a cloud-hosting ASN the user never uses (AiTM proxy indicator). Minutes later, SigninLogs shows a non-interactive session for the same user from a second IP, concurrent with the first. The attacker proxied the login, captured the token, and is now replaying it. The password held; the token did not.

Respond: revoke sessions (not just reset the password, the token survives a reset), then check for the persistence that survives a revoke: a new app client secret, a federated credential, an added CA exclusion. Phishing-resistant MFA is the control that would have prevented the proxy.

Quick lookup

Identity attackTable + signal
Password spray / stuffingSigninLogs, error 50126 across many accounts; check legacy-auth path
Token theft (AiTM)SigninLogs, proxy-ASN sign-in + concurrent/replayed session
Risky sign-in that succeededSigninLogs + AADUserRiskEvents, risk raised but ResultType 0
Privilege escalationAuditLogs, direct role add outside PIM; standing privileged roles
App persistenceAuditLogs, client-secret/cert added to app or service principal
Workload-identity abuseAADServicePrincipalSignInLogs, federated-cred usage from new env
Guest / B2B abuseExternal identity activity, guest risk scoring
CA evasionSigninLogs, legacy-auth success; AuditLogs, CA policy change

From detecting identity attacks to designing the defense

This cheatsheet is the detection set. Entra ID Security teaches the whole identity stack: conditional-access architecture, token and PIM security, application and workload identity, and the complete hardened design that closes these surfaces.

Explore the course