0

In powershell we have a script that gets info from Azure REST API using Resource Owner Password Credentials.

https://learn.microsoft.com/bs-latn-ba/azure/active-directory/develop/v2-oauth-ropc

The script works perfectly with users that don't have MFA enabled. For user with MFA it doesn't work. I tried to use an app password that I created on the user account with MFA but this didn't work also.

https://support.microsoft.com/en-au/help/12409/microsoft-account-app-passwords-and-two-step-verification

The script is running as a service so user interaction is no option. We also need to use ROPC because the info we needed is only available trough delegated permisions on the Azure app.

Is there anyone that has experience with this?

Here is the script:

$tenantid = '*************************'
$subscriptionid = '*********************'
$clientid = '***********************'
$clientsecret = '******************'
$username = '*****************'
$password = '************************'

##################################################################
##################################################################
##################################################################

$return = Invoke-Command -ScriptBlock { 
param($tenantid,$subscriptionid,$clientid,$clientsecret,$username,$password)    

Add-Type -AssemblyName System.Web

$encPass = [System.Web.HttpUtility]::UrlEncode($password)
$encScope = [System.Web.HttpUtility]::UrlEncode('https://management.azure.com/user_impersonation')
$encSecret = [System.Web.HttpUtility]::UrlEncode($clientsecret)

$body = "client_id=$clientid&scope=$encScope&username=$username&password=$encPass&grant_type=password&client_secret=$encSecret"

$auth = Invoke-WebRequest "https://login.microsoftonline.com/$tenantid/oauth2/v2.0/token" -Method Post -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing

$token = ($auth | ConvertFrom-Json).access_token
$headers = @{
    'Authorization'="Bearer $($token)"
}

$data = Invoke-WebRequest "https://management.azure.com/subscriptions/$subscriptionid/providers/Microsoft.Advisor/recommendations?api-version=2017-04-19" -Method GET -Headers $headers -UseBasicParsing

New-Object PSObject -Property @{
    content=$data.content
}

} -ArgumentList $tenantid,$subscriptionid,$clientid,$clientsecret,$username,$password

$content = $return.content

Write-Host $content

The output when I use an user with MFA enabled:

Invoke-WebRequest : {"error":"invalid_grant","error_description":"AADSTS50076: Due to a configuration change made by your administrator, or because you moved to a new location, you must use multi-factor authentication to 
access '*******'.\r\nTrace ID: d9a7f9f2-c52c-40ca-b057-9513bd353900\r\nCorrelation ID: 3329e686-7bd0-409d-b7da-91e49221bacc\r\nTimestamp: 2019-10-02 
13:19:36Z","error_codes":[50076],"timestamp":"2019-10-02 
13:19:36Z","trace_id":"d9a7f9f2-c52c-40ca-b057-9513bd353900","correlation_id":"3329e686-7bd0-409d-b7da-91e49221bacc","error_uri":"https://login.microsoftonline.com/error?code=50076","suberror":"basic_action"}
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:27 char:9
+ $auth = Invoke-WebRequest "https://login.microsoftonline.com/$tenanti ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
ConvertFrom-Json : Cannot bind argument to parameter 'InputObject' because it is null.
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:29 char:19
+ $token = ($auth | ConvertFrom-Json).access_token
+                   ~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [ConvertFrom-Json], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ConvertFromJsonCommand

Invoke-WebRequest : {"error":{"code":"AuthenticationFailedMissingToken","message":"Authentication failed. The 'Authorization' header is missing the access token."}}
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:34 char:9
+ $data = Invoke-WebRequest "https://management.azure.com/subscriptions ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

The output when I change the password with an app password created on the user account

Invoke-WebRequest : {"error":"invalid_grant","error_description":"AADSTS50126: Invalid username or password.\r\nTrace ID: 3674934a-120b-48f3-96d8-7ec8ddf44300\r\nCorrelation ID: 
593aecd7-bbb2-4c5a-96e1-050bc00047ac\r\nTimestamp: 2019-10-02 13:26:46Z","error_codes":[50126],"timestamp":"2019-10-02 
13:26:46Z","trace_id":"3674934a-120b-48f3-96d8-7ec8ddf44300","correlation_id":"593aecd7-bbb2-4c5a-96e1-050bc00047ac","error_uri":"https://login.microsoftonline.com/error?code=50126"}
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:27 char:9
+ $auth = Invoke-WebRequest "https://login.microsoftonline.com/$tenanti ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand
ConvertFrom-Json : Cannot bind argument to parameter 'InputObject' because it is null.
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:29 char:19
+ $token = ($auth | ConvertFrom-Json).access_token
+                   ~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [ConvertFrom-Json], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.ConvertFromJsonCommand

Invoke-WebRequest : {"error":{"code":"AuthenticationFailedMissingToken","message":"Authentication failed. The 'Authorization' header is missing the access token."}}
At C:\Users\Wouter.sterkens\Documents\VS Projects\Azure Monitoring\advisor.ps1:34 char:9
+ $data = Invoke-WebRequest "https://management.azure.com/subscriptions ...
+         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
    + FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

1 Answers1

3

As far as I know, app password is used to complete MFA with the clients which do not support modern authentication. Now, you use ROPC OAuth flow. APP password does not support it.

According to the situation, I suggest you finish MFA manually to get refresh token then we use refresh token to get access token and call API. Because MFA refresh token will not expire until you revoke it. Or you use OAuth 2.0 client credentials flow to get the access token. For example

User refresh token

  1. Register Azure AD application

  2. Use OAuth 2.0 authorization code flow to complete MFA and get refresh token

$Params = @{
    'client_id' = 'b0114608-677e-4eca-ae22-60c32e1782d9' 
    'redirect_URI' = 'https://www.baidu.com'
    'response_type'='code'
    'scope' = 'offline_access openid https://management.azure.com/user_impersonation'
}
$ClientSecret =''
$TeantID = ''
$Query = "?"; $Params.Keys | % {$Query+= "$($_)=$($Params.Item($_))&"} ; $Query = $Query.TrimEnd('&')


$IE= new-object -ComObject "InternetExplorer.Application"
$IE.Visible = $true
$IE.navigate2("https://login.microsoftonline.com/$($TeantID)/oauth2/v2.0/authorize$Query")

write-host "get authorization code"
pause

Add-Type -AssemblyName System.Web
[System.Web.HttpUtility]::ParseQueryString(([uri] $IE.LocationURL).Query)['code']
$Code = [System.Web.HttpUtility]::ParseQueryString(([uri] $IE.LocationURL).Query)['code']
$IE.Quit()

$TokenResult = Invoke-RestMethod -Method Post -ContentType 'application/x-www-form-urlencoded' -Uri "https://login.microsoftonline.com/$($TeantID)/oauth2/v2.0/token" -Body @{
    client_id     = $Params.client_id
    scope         = ''
    code          = $Code
    redirect_uri  = $Params.Redirect_URI
    grant_type    = 'authorization_code'
    client_secret = $ClientSecret
}

$TokenResult.refresh_token
  1. Get Access token and call the api
$TokenResult = Invoke-RestMethod -Method Post -ContentType 'application/x-www-form-urlencoded' -Uri "https://login.microsoftonline.com/$($TeantID)/oauth2/v2.0/token" -Body @{
    client_id     = ''
    scope         = 'https://management.azure.com/user_impersonation'
    redirect_uri  = ''
    grant_type    = 'refresh_token'
    client_secret = ''
    refresh_token =''
}



 Invoke-RestMethod -Method Get -Uri '' -Headers @{Authorization = "Bearer "+ $TokenResult.access_token}

Use OAuth 2.0 client credentials flow

$TokenResult = Invoke-RestMethod -Method Post -ContentType 'application/x-www-form-urlencoded' -Uri "https://login.microsoftonline.com/$($TeantID)/oauth2/v2.0/token" -Body @{
    client_id     = ''
    scope         = 'https://management.azure.com/.default'
    grant_type    = 'client_credentials'
    client_secret = ''

}

 Invoke-RestMethod -Method Get -Uri '' -Headers @{Authorization = "Bearer "+ $TokenResult.access_token}

Update According to your need, you can create a service principal and assign RABC role to the service principal. Then you can OAuth 2.0 client credentials flow to get access token and call Azure rest api. The detailed steps are as below

  1. Create a service principal and assign RABC role to the service principal
Connect-AzAccount
$password=''
$credentials = New-Object Microsoft.Azure.Commands.ActiveDirectory.PSADPasswordCredential -Property @{ StartDate=Get-Date; EndDate=Get-Date -Year 2024; Password=$password'}
$sp = New-AzAdServicePrincipal -DisplayName jimtest1 -PasswordCredential $credentials

New-AzRoleAssignment -ApplicationId $sp.ApplicationId -RoleDefinitionName Owner
  1. Get access token
# get access token
$TeantID='hanxia.onmicrosoft.com'
$TokenResult = Invoke-RestMethod -Method Post -ContentType 'application/x-www-form-urlencoded' -Uri "https://login.microsoftonline.com/$($TeantID)/oauth2/v2.0/token" -Body @{
    client_id     = $sp.ApplicationId # the application id of service principal
    scope         = 'https://management.azure.com/.default'
    grant_type    = 'client_credentials'
    client_secret = $password # you use it in step 1

}
  1. Call Azure Rest API
#list resource group
$values =Invoke-RestMethod -Method Get -Uri "https://management.azure.com/subscriptions/e5b0fcfa-e859-43f3-8d84-5e5fe29f4c68/resourcegroups?api-version=2019-05-10" -Headers @{
Authorization = "Bearer "+ $TokenResult.access_token
ContentType = 'application/json'
}

enter image description here

For more details, please refer to

https://learn.microsoft.com/en-us/powershell/azure/create-azure-service-principal-azureps?view=azps-2.7.0

https://learn.microsoft.com/en-us/azure/azure-resource-manager/resource-manager-api-authentication#get-app-only-access-token-for-azure-resource-manager

Jim Xu
  • 21,610
  • 2
  • 19
  • 39
  • Thank you for the answer. The option with the refresh token in not possible for us I think. We are not able to authenticate manually because the script is running as a servive. The client credential flow would be a good solution if https://management.azure.com uses application permissions but that not the case. Only delegated permissions are available on this API. – WouterSterkens Oct 03 '19 at 07:54
  • @WouterSterkens You can use service principal to call Azure REST API. For more details, please read my update – Jim Xu Oct 03 '19 at 08:51
  • it works! But I don't understand how :d Normally I create the app and configure api permisions. With the service principal there is also an app created but there are no api permissions needed. There's also a difference in the scope. SP uses .default, default app uses user_impersonation Can you explain me where the differance lies between these 2 configurations? – WouterSterkens Oct 03 '19 at 14:55
  • Firstly, regarding why the service principal has no api permissions, because we has assigned an RABC role to it then it has permission to access Azure resource. And you also can assign other api permsiioins to it then use it to access you other service protected by Azure AD. For more details, please refer to https://learn.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals and https://learn.microsoft.com/en-us/azure/role-based-access-control/overview. – Jim Xu Oct 07 '19 at 01:31
  • Secondly, the ```/.defaul``` scope is a built-in scope for every application that refers to the static list of permissions configured. It is necessary in the client credentials flow. For more details, please refer to https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope – Jim Xu Oct 07 '19 at 01:39
  • @WouterSterkens Do you have any other concern? If you have no other concern, could you please accept the answer? It may help more people. – Jim Xu Oct 10 '19 at 02:26
  • Is there any way to not open popup for credentials, this solution is good but with this solution can not be automated. – Ashish-BeJovial Sep 01 '20 at 08:12
  • 1
    @Ashish-BeJovial you can use OAuth 2.0 client credentials flow – Jim Xu Sep 01 '20 at 08:14
  • @Jim Xu what will be the uri, I am following same way and getting following error Invoke-RestMethod : Invalid URI: The hostname could not be parsed. At line:1 char:2 + Invoke-RestMethod -Method Get -Uri '' -Headers @{Authorization = "Be ... + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Invoke-RestMethod], UriFormatException + FullyQualifiedErrorId : System.UriFormatException,Microsoft.PowerShell.Commands.InvokeRestMethodCommand – Ashish-BeJovial Sep 01 '20 at 14:37