Home > OS >  Cannot call Graph API using delegated permissions in PowerShell
Cannot call Graph API using delegated permissions in PowerShell

Time:03-30

I have an Azure App Registration setup as below:

Authentication

Not shown: two redirect URIs for API Permissions

We have a web API that can correctly access Graph against this App Registration by using a hybrid on-behalf-of flow and IConfidentialClientApplication, along with a bootstrap token provided by an Office add-in.

The creation of this App Registration is actually done by a PowerShell script, and I want to add a feature that will try to login as the script user to make a test Graph API call (against the /me endpoint) to make sure it is setup correctly. The script can ensure that the App Registration is granted admin consent for the Graph API permissions so the script user does not have to provide consent.

From what I understand, the authorization code flow can be used to test these delegated permissions. However, I've tried two approaches with different results as below using the MSAL module to get the token and then use that token in the authorization header for a Graph call to simply return details about the logged in user via the /me endpoint:

  1. If I don't pass a client secret to the Get-MsalToken cmdlet, I do get a login prompt but Get-MsalToken always returns the error 'AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'
  2. If I do provide a client_secret, I do NOT get the login prompt and can successfully get a token, but the Graph call fails saying that the /me request is only valid with delegated authentication flow. Which makes sense because we need a user context.

So I'm stuck - I can't use the user context with the interactive login without it expecting a client secret that if I do use is using a different flow that's not supporting delegated permissions. What am I doing wrong? Or do I need to use a completely different authentication flow?

Does the fact that the App Registration is designed to handle SSO with Office web add-ins have anything to do with it? Meaning, I have a scope (e.g. api://MYWEBSERVER/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/access_as_user, as used by the add-in manifests for purposes of issuing a bootstrap token from the Office JS library to our Web API) and a list of authorized client applications (Office apps) in the 'Expose an API' section. I'm wondering if only those applications are authorized, and not the PowerShell script...

I've also seen some suggestions about using the device code flow and I'm wondering if I'm missing a step to get a refresh token (as demonstrated here but I can't get that example to work either using a client secret (I can't figure out the proper Get-MsalToken parameter combination).


    $graphEndPointUrl = "https://graph.microsoft.com/v1.0/me"
    
    $connectionDetails = @{
        'TenantId'    = $tenantId #Set variable to your own value
        'ClientId'    = $clientId #Set variable to your own value
        'Scopes' = 'https://graph.microsoft.com/.default'
    }
    #Using the above information, it throws this error on Invoke-RestMethod:
        #ERROR: "{"error":"invalid_client","error_description":"AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'.        
        #"error_codes":[7000218]
        #"error_uri":"https://login.microsoftonline.com/error?code=7000218"}" 

    $connectionDetails = @{
        'TenantId'    = $tenantId #Set variable to your own value
        'ClientId'    = $clientId #Set variable to your own value
        'ClientSecret' = $clientSecret | ConvertTo-SecureString -AsPlainText -Force #Set $clientSecret variable to your own value
        'Scopes' = 'https://graph.microsoft.com/.default'
    } 
    #Using the above information, it throws this error on Invoke-RestMethod:
        #ERROR: "{"error":{"code":"BadRequest","message":"/me request is only valid with delegated authentication flow."
            
    Import-Module MSAL.PS

    try {

        $myAccessToken = Get-MsalToken @connectionDetails

        Write-Host $myAccessToken.AccessToken

        $authHeader = @{
            'Authorization' = $myAccessToken.CreateAuthorizationHeader()
        }
        $response = Invoke-RestMethod -Uri $graphEndPointUrl -Headers $authHeader
        #Error thrown below

        if ($null -eq $response) {                          
            Write-Error "An unexpected error occurred making a test Graph API call to the $($graphEndPointUrl) endpoint" -ForegroundColor Red            
        }                    
        else {
            Write-Host "The test Graph API call to the $($graphEndPointUrl) endpoint was successful!"                           
        }
    }
    catch {
        $result = $_.Exception.Response.GetResponseStream()
        $reader = New-Object System.IO.StreamReader($result)
        $reader.BaseStream.Position = 0
        $reader.DiscardBufferedData()
        $responseBody = $reader.ReadToEnd();
        $errorObj = ConvertFrom-Json -InputObject $responseBody
        Write-Host "An unexpected error occurred making a test Graph API call to the $($graphEndPointUrl) endpoint: $($errorObj.error.message)"
    } 

CodePudding user response:

Update

Everything you need I believe is written in this Microsoft article - https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow

I noticed a key difference in the tutorial you referenced and your setup, thats the ID Token that is used with the hybrid setup, you can read more about it in the MS guide, check response_mode .

So principal goes as follow > You get an authorization code > use that code to generate your access and refresh tokens > then you can use different API calls with the Access token in the header.

As for your code, here are the things that are missing or are not correctly implemented.

  1. You are missing the RedirectUri param. Make sure the URL is exact I have just put https://localhost as an example.
$connectionDetails = @{
    'TenantId'     = $tenantId #Set variable to your own value
    'ClientId'     = $clientId #Set variable to your own value
    'ClientSecret' = $clientSecret | ConvertTo-SecureString -AsPlainText -Force #Set $clientSecret variable to your own value
    'Scopes'       = 'https://graph.microsoft.com/.default'
    'RedirectUri'  = 'https://localhost'
}  
  1. Your header is not implementing the Authorization properly. Check out the example from the tutorial
$myAuthMethods = (Invoke-RestMethod -Headers @{Authorization = "Bearer $($myAccessToken.AccessToken)" } `
   -Uri "https://graph.microsoft.com/beta/me/authentication/methods" `
   -Method Get).value
$myAuthMethods

The header have the following structure, where the word Bearer is before the token.

$header = @{
    "Authorization" = "Bearer $($myAccessToken.AccessToken)"
}

Make sure you have the access token $myAccessToken.AccessToken in the header.

At that point, you should at least get a different error if request does not proceed.

CodePudding user response:

The issue is that an additional "Mobile and desktop applications" platform needs to be added to the App Registration on the Authentication page (with "https://login.microsoftonline.com/common/oauth2/nativeclient" as a Redirect URI), as it only has a web platform currently. Then Get-MsalToken works without a client Id:

$myAccessToken = Get-MsalToken -ClientId $clientID -TenantId $tenantId -Interactive -Scopes 'https://graph.microsoft.com/.default' -RedirectUri 'https://login.microsoftonline.com/common/oauth2/nativeclient'

However, an additional request is needed to get a refresh token and use that for the Graph call:

$myAccessToken = Get-MsalToken -ClientId $clientID -TenantId $tenantId -Silent
  • Related