How much time your users are wasting with “traditional” MFA?

Recently, I came across a post on LinkedIn which demonstrated that Passkey authentication is way faster than traditional Password+MFA notification login. It made me curious: how much time does it exactly take to do MFA?

TL;DR
– This report uses the SignInLogs table which needs to be configured in Diagnostic settings
– Unfortunately I did not manage to gather the same info from AADSignInEventsBeta table in Defender or sign-in logs from Microsoft Graph
– Everything written here is based on my tests and measurements, so it may contain inaccurate conclusions

The query that will display the authentication method, the average and overall time spent with completing the MFA prompt:

let StrongAuthRequiredSignInAttempts = SigninLogs
	| where ResultType == "50074"
	| distinct ResultType,UniqueTokenIdentifier,CorrelationId;
let MFA1 =SigninLogs
	| join kind=inner StrongAuthRequiredSignInAttempts on UniqueTokenIdentifier
	| mv-expand todynamic(AuthenticationDetails)
	| project stepdate=todatetime(AuthenticationDetails.authenticationStepDateTime), authMethod = tostring(AuthenticationDetails.authenticationMethod), stepResult = tostring(AuthenticationDetails.authenticationStepResultDetail), RequestSequence = todouble(AuthenticationDetails.RequestSequence), StatusSequence = todouble(AuthenticationDetails.StatusSequence), CorrelationId,RequestSeq_UnixTime = unixtime_milliseconds_todatetime(todouble(AuthenticationDetails.RequestSequence)), UniqueTokenIdentifier, StatusSeq_UnixTime = unixtime_milliseconds_todatetime(todouble(AuthenticationDetails.StatusSequence)), MFAMethod =tostring(MfaDetail.authMethod)
    | summarize make_set(stepResult), MFAStart=min(stepdate), MFAEnd=max(stepdate), TimeSpent=totimespan(max(stepdate)-min(stepdate)),TimeSpentv2=totimespan(maxif(StatusSeq_UnixTime, StatusSequence > 1)-minif(RequestSeq_UnixTime, RequestSequence > 1)) by UniqueTokenIdentifier,MFAMethod
    | where set_stepResult has "MFA successfully completed"
    ;
MFA1
| where isnotempty(MFAMethod)
| project MFAMethod,TimeSpent = coalesce(TimeSpentv2,TimeSpent)
| summarize AverageMFATime=avg(TimeSpent),SumMFATime=sum(TimeSpent) by MFAMethod

Example result:

Explanation

The first step was to find those sign-in attempts that are interrupted, because MFA is needed. This can be easly found as there is a ResultDescription column where we can filter for “Strong Authentication is required.” entries:

SigninLogs
| where ResultDescription == "Strong Authentication is required."

Or use the ResultType column, where 50074 state code indicates the same (reference: https://login.microsoftonline.com/error?code=50074).

The first catch is that not the entire sign-in session has this field populated with the same value (for logical reasons). Let’s take a simple login to the Azure portal with Authenticator Code as MFA:

In this example, I intentionally waited 30 seconds to provide the code (after successful password entry) [code prompt started on 2024.12.09 9:41:15, code sent on 9:41:45]. The TimeGenerated field is a bit misleading, because it is the creation timestamp of the event entry not the authentication event (this part is stored in the AuthenticationDetails column).
It is also worth mentioning that the CorrelationId remains the same in a browser session (even if session policies require re-authentication) – so if for example the Azure portal is kept open in the browser but re-authentication happens, the CorrelationId is the same but the authentication steps (reentering password, new MFA prompt) need to be handled separately. This is why I’m using the UniqueTokenIdentifier.

But let’s get back to the example and extend the AuthenticationDetails column:

Some fields are not totally clear for me, but according to my measures the most accurate timespan of “doing MFA” is the time between the “MFA required in Azure AD” and the “MFA completed in Azure AD” events (it’s not totally accurate because I spent some time to change the MFA method).

However, this approach (time between “MFA required” and “MFA completed”) will not cover all other MFA methods, because “MFA required” is not always present in the logs. For example, the next sign-in example was using Mobile app notification as MFA:

At this point the possible solution is to either write a query for each authentication method or try to find a unified approach. I opted for the unified option: assume that the “MFA start time” is the first logged AuthenticationStepDate and the “MFA end time” is the last logged AuthenticationStepDate where we have “MFA successfully completed” entry (this one seems to be present in every MFA type).

This looks almost appropriate, but in the case of “Mobile app notification” I found the RequestSequence and StatusSequence fields which are Unix timestamps and look more precise:

But since these fields are not always present, I chose the KQL coalesce() function to return the TimeSpentv2 value when present – otherwise return the TimeSpent value.

Note1: the summarize operator needs to group by UniqueTokenIdentifier and MFAMethod, because without the MFAMethod, the “Password” will also be returned as authentication factor.

Note2: when calculating TimeSpentv2, there were other authentication steps where StatusSequence fields were empty, 0 or 1. They are clearly not Unix timestamps, so only values greater than 1 are considered here

+1 point for passkey authentication 🙃

Comments are closed.