Legacy MFA settings in the Entra portal and how to control them programmatically

Azure Multi-factor authentication or “MFA for Office 365” or “legacy MFA” has been around for a decade now, and so far has resisted Microsoft’s attempts to get rid of it in favor of Conditional access policies. After years of quietly ignoring this feature, hitting the supportability deadline for the MSOL PowerShell module finally forced Microsoft to add support for managing per-user MFA via the Graph API. Just recently, we also got the UI bits “ported” to the Entra portal. This in turn comes with a new way for getting and updating the tenant-wide settings programmatically, which is what we will explore in this article.

First thing first, let’s take a look at said UI bits. I cannot say that Microsoft has done a good job visibility-wise, as the settings are a bit hidden. To get to them you need to navigate to the Multifactor authentication page in the Entra portal, then hit the barely visible Additional cloud-based multifactor authentication settings link, which looks just like a plain documentation link. Once you get to them however, you will be pleased to find parity with the familiar controls exposed in the old Azure MFA portal.

The screenshot below shows the Users tab. The experience is pretty much the same and you can perform operations such as enabling, disabling or enforcing MFA for a user, as well as manage the three additional controls (require re-register, clear app passwords and clear device MFA state). You can also perform actions in bulk by selecting a set of users and hitting the corresponding button, or by leveraging the Bulk update button on top. You will also find the familiar filters.

EntraMFA

The Service settings tab also gives you parity with the old experience. Herein, you can toggle tenant-wide settings such as the use of application passwords, the set of trusted IPs/ranges, the methods available for performing MFA and so on. Here is how it looks like:

EntraMFA1

Overall, Microsoft did a good job here, with the only remark being the rather diminished visibility of the new experience. But hopefully articles such as this will help on that front.

The more interesting part of the story is what this integration with the Entra portal brings on the API front. Sadly, we do not yet get Graph API endpoints for this experience, apart from the aforementioned per-user MFA toggle. But, exposing these in the Entra portal and its sibling Entra ID blade in the Azure portal has the side effect of enabling new programmatic means to get and even manage these settings. A quick browser trace will give you all the necessary details, which we summarize below.

First things first, we will need to handle authentication. If you examine the access token used for performing UI operations, you will notice the audience is set to ‘74658136-14ec-4630-ad9b-26e160ff0fc6‘ (which is the ADIbizaUX app). Which is very well known scenario with multiple workarounds available for getting a valid access token. One common scenario is to use the Azure PowerShell module, which allows us to obtain “user impersonation” access token for the aforementioned resource. To make things even easier (or worse, depending on your view point), it even supports the ROPC flow (don’t get me started on this).

So, we now have an understanding on what we need, authentication-wise. You can in fact find multiple examples online that exploit the Azure module for similar scenarios, so I will not go into too much detail here. Still, we need at least one example,  so here is how to obtain an access token for the resource via the MSAL methods:

#Load the MSAL binaries
Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\MSAL\Microsoft.Identity.Client.dll"

#Leverage the ADIbizaUX app
$app = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create("1950a258-227b-4e31-a9cf-717495945fc2").WithRedirectUri("urn:ietf:wg:oauth:2.0:oob").WithTenantId("tenant.onmicrosoft.com").Build()

#Set the scope
$Scopes = New-Object System.Collections.Generic.List[string]
$Scope = "74658136-14ec-4630-ad9b-26e160ff0fc6/.default"
$Scopes.Add($Scope)

#Get the token
$token = $app.AcquireTokenInteractive($Scopes).WithLoginHint("user@domain.com").ExecuteAsync().Result

Or if you really want to live dangerously, you can also leverage the ROPC flow as mentioned above:

$pass = Get-Credential user@domain.com
$token = $app.AcquireTokenByUsernamePassword($Scopes,$pass.UserName,$pass.Password).ExecuteAsync().Result

Yes, yes, all of these are interactive, but I’m not going to perpetuate some people’s bad practice of providing examples with hardcoded credentials. Do this at your own peril.

Once we have a valid access token, we can set the authentication header. Speaking of which, the backend seems to requires the presence of x-ms-client-request-id header as well, something we can easily adhere to.

$authHeader = @{
   'Content-Type'='application\json'
   'Authorization'="Bearer $($token.AccessToken)"
   "x-ms-client-request-id" = [guid]::NewGuid().ToString()
}

The last piece of the puzzle is the actual endpoint to call, which we can again easily get from a browser trace. It might come as a shock, but the value is “https://main.iam.ad.ext.azure.com/api/MultiFactorAuthentication/”. Without further ado, here is how to fetch the MFA service settings:

$uri = "https://main.iam.ad.ext.azure.com/api/MultiFactorAuthentication/bec/tenant/setting"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader
$res.Content | ConvertFrom-Json

EntraMFA2

And of course, you can also make changes to the settings. For that, you need to issue a PATCH request against the endpoint and provide a JSON payload with all the settings. Yes, all of them, even the ones you do not intend to change. Failing to do so will result in any non-listed setting value being nulled, which is definitely something you should avoid. An easy way to work around this would be to just use the output of the GET request, make any changes to the setting values therein and provide it as input for the PATCH request.

You might have noticed that two of the settings in the output above are missing from the UI, namely the allowMFAPin and pinMinimumLength ones. In all fairness, those are missing from the old UI, too. But if you absolutely have to, you can toggle them by changing the corresponding values. Without further ado, here is an example on how to update the tenant settings configuration. We toggle the use of application passwords to be disabled, and set the maximum duration to remember any given device to 14 days:

$json = @{
    'allowAppPasswords' = $false
    'allowMFAPin' = $false
    'pinMinimumLength' = 0
    'callToPhone' = $true
    'canEnablePhone' = $true
    'smsToPhone' = $true
    'phoneAppNotification' = $true
    'phoneAppOTP' = $true
    'allowRememberMyDevices' = $true
    'rememberMyDevicesDuration' = 14
    'skipMfaForCorpnetClaim' = $false
    'ipAllowList' = @("1.2.3.4/32")
} | ConvertTo-Json

$uri = "https://main.iam.ad.ext.azure.com/api/MultiFactorAuthentication/bec/tenant/setting"
$res = Invoke-WebRequest -Uri $uri -Headers $authHeader -Method Patch -Body $json -ContentType "application/json"
$res.Content

Successful execution is indicated by a simple “true” reply from the server (200 OK status code). And that’s pretty much all there is to it. Before closing the article, just to reiterate that the method we’re using here relies on impersonating the user, so you must run this code with an account that has sufficient permissions. Consult the documentation for the least privileged role. And please do not store credentials in scripts, even ones leveraging the ROPC flow.

1 thought on “Legacy MFA settings in the Entra portal and how to control them programmatically

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.