14.5 Non-Interactive Sign-In Deep Dive

3-5 hours · Module 14

Non-Interactive Sign-In Deep Dive

The AADNonInteractiveUserSignInLogs table is the primary evidence source for token replay. Most analysts focus on SigninLogs (interactive) and miss the non-interactive table entirely — which is exactly where token replay evidence lives.


Why non-interactive sign-ins matter

When the attacker replays a stolen refresh token via API (not browser), the event appears ONLY in AADNonInteractiveUserSignInLogs. It does not appear in SigninLogs. An analyst who queries only SigninLogs misses the entire token replay activity.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Compare interactive vs non-interactive volume for a user
let User = "r.williams@northgateeng.com";
let TimeWindow = ago(7d);
SigninLogs
| where TimeGenerated > TimeWindow
| where UserPrincipalName == User
| summarize InteractiveCount = count()
| extend Table = "SigninLogs"
| union (
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > TimeWindow
    | where UserPrincipalName == User
    | summarize NonInteractiveCount = count()
    | extend Table = "AADNonInteractiveUserSignInLogs"
)

In a typical M365 environment, non-interactive sign-ins outnumber interactive sign-ins 10:1 or more. Applications continuously refresh tokens in the background. An attacker using API-based access generates dozens of non-interactive sign-ins per hour — all invisible if you only check SigninLogs.


Key fields for non-interactive investigation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Detailed non-interactive sign-in analysis
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(7d)
| where UserPrincipalName == "r.williams@northgateeng.com"
| where IPAddress !in ("192.0.2.10", "192.0.2.15")
| project
    TimeGenerated,
    IPAddress,
    AppDisplayName,           // Which application used the token
    AppId,                    // Application ID  correlate with OAuth consents
    ResourceDisplayName,      // Which resource was accessed
    UserAgent = tostring(DeviceDetail.browser),
    OS = tostring(DeviceDetail.operatingSystem),
    TokenIssuerType,
    AuthenticationProcessingDetails,
    ResultType,
    ResultDescription
| order by TimeGenerated desc

AppId is the critical field. If the AppId matches a known Microsoft application (Outlook, SharePoint, Teams): the attacker imported the stolen token into a legitimate application. If the AppId matches an OAuth application consented during the compromise window: the attacker is using a persistent application with its own credentials. Cross-reference AppId with the OAuth consent query from subsection 14.4 Step 4.

UserAgent reveals the attacker’s tooling. Legitimate M365 applications use recognisable user agents (Outlook, Edge, Chrome). Attacker scripts and API tools use: Python-based user agents (python-requests/2.28), custom strings, or missing user agent entirely. A non-interactive sign-in with a Python user agent from a non-corporate IP is conclusive evidence of scripted token abuse.


Building the attacker activity timeline from non-interactive logs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Complete attacker activity timeline  what resources, when, how often
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(30d)
| where UserPrincipalName == "r.williams@northgateeng.com"
| where IPAddress !in ("192.0.2.10", "192.0.2.15")
| summarize
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    SigninCount = count(),
    Resources = make_set(ResourceDisplayName, 10),
    Apps = make_set(AppDisplayName, 10),
    UserAgents = make_set(tostring(DeviceDetail.browser), 5)
    by IPAddress
| extend ActiveDays = datetime_diff('day', LastSeen, FirstSeen)
| order by FirstSeen asc

This query produces the attack timeline. Total duration of attacker access, which resources were accessed, and the activity volume per IP. This goes directly into the IR report timeline section.

Subsection artifact: The non-interactive investigation queries and the attacker activity timeline query. These are essential additions to your token investigation playbook — the queries most analysts do not know to run.


Knowledge check


Production patterns in AADNonInteractiveUserSignInLogs

Non-interactive sign-ins have distinct patterns that reveal whether they are legitimate background activity or attacker token usage.

Legitimate background pattern. The user signs in interactively once in the morning. For the rest of the day, Outlook, Teams, OneDrive, and SharePoint silently renew tokens via non-interactive sign-ins. Volume: 50-200 non-interactive sign-ins per day per user. IPs: consistent corporate IPs. Apps: the same 3-5 applications. User agents: legitimate application user agents (“Outlook/16.0”, “Microsoft Office/16.0”).

Attacker token usage pattern. Non-interactive sign-ins from a non-corporate IP. Unusual user agent (Python, curl, or empty). Accessing resources the user does not normally access (e.g., Graph API when the user typically only uses Outlook). Activity during unusual hours (if the user is in the UK and the non-interactive sign-ins occur at 03:00 UTC with no corresponding VPN connection).

The AppId fingerprint. Every non-interactive sign-in includes an AppId — the application that requested the token. Legitimate M365 applications have well-known AppIds. An attacker using a custom script will either: use a registered OAuth application (AppId from the consent in Module 15), or use a known client ID for a public client (like the Azure CLI or Graph Explorer). Query the AppId against your known application inventory: unfamiliar AppIds warrant investigation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Profile non-interactive sign-in patterns for a user
// Run against a known-clean user to establish baseline, then compare with suspect
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(7d)
| where UserPrincipalName == "r.williams@northgateeng.com"
| summarize
    DailyCount = count(),
    UniqueApps = dcount(AppDisplayName),
    UniqueIPs = dcount(IPAddress),
    Apps = make_set(AppDisplayName, 10),
    IPs = make_set(IPAddress, 10),
    UserAgents = make_set(tostring(DeviceDetail.browser), 5)
    by bin(TimeGenerated, 1d)
| order by TimeGenerated desc

Baseline this for your environment. Run the query for 5-10 users across different roles. Record: typical daily non-interactive count, typical number of unique apps, typical user agents. This baseline is the reference for detecting anomalous token usage — a user whose non-interactive count jumps from 100/day to 500/day, or whose unique apps jumps from 3 to 8, warrants investigation.

Try it yourself

Run the non-interactive profiling query for your own account and for 2-3 colleagues (with their permission or using a test account). Compare: daily counts, applications, IPs, and user agents. This builds your intuition for what "normal" non-interactive traffic looks like — essential for recognising the anomalies that indicate token replay.

What you should observe

Most users show 50-200 non-interactive sign-ins per day from 3-5 applications on 1-2 IPs. Mobile users show additional IPs (cellular). Users with many browser tabs may show higher counts. An attacker stands out: non-corporate IP, unusual user agent, and access to resources outside the user's normal pattern.

Check your understanding

1. You search SigninLogs for suspicious sign-ins from r.williams. You find nothing. Is r.williams' account clean?

Not necessarily. Token replay via API generates non-interactive sign-ins that appear ONLY in AADNonInteractiveUserSignInLogs — not in SigninLogs. You must check both tables. An analyst who only queries SigninLogs misses all API-based token replay activity. Always check both tables during token replay investigation.
Yes — SigninLogs contains all authentication events
Check AuditLogs instead
Non-interactive sign-ins are always benign