Posted in : Azure, Microsoft, Office 365, Powershell By Stina Perbo Utas Translate with Google ⟶

5 years ago

Edit: There is now a Github page for this project
https://github.com/Freakling/Powershell-MicrosoftGraphAPI
Microsoft Graph is a very powerful tool to query organization data, and it’s also really easy to do using Graph explorer but it’s not built for automation.
While the concept I’m presenting in this blogpost isn’t something entirely new, I believe my take on it is more elegant and efficient than what I’ve seen other people use.

So, what am I bringing to the table?

  • Zero dependancies to Azure modules, .net Core & Linux compatibility!
  • Recursive/paging processing of Graph data (without the need for FollowRelLink, currently only available in powershell 6.0)
  • Authenticates using an Azure AD Application/service principal
  • REST compatible (Get/Put/Post/Patch/Delete)
  • Supports json-batch jobs
  • Supports automatic token refresh. Used for extremely long paging jobs
  • Accepts Application ID & Secret as a pscredential object, which allows the use of Credential stores in Azure automation or use of Get-Credential instead of writing credentials in plaintext

Sounds great, but what do I need to do in order to query the Graph API?

First things first, create a Azure AD application, register a service principal and delegate Microsoft Graph/Graph API permissions.
Plenty of people has done this, so I won’t provide an in-depth guide. Instead we’re going to walk through how to use the functions line-by-line.
When we have an Azure AD Application we need to build a credential object using the service principal appid and secret.

$credential = New-Object System.Management.Automation.PSCredential($appID,(ConvertTo-SecureString $SPSecret -AsPlainText -Force))

Then we aquire a token, here we require a tenantID in order to let Azure know the context of the authorization token request.

$token = Get-MSGraphAuthToken -credential $credential -tenantID $TenantID

Once a token is aquired, we are ready to call the Graph API. So let’s list all users in the organization.

$Users = Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users' -token $token

In the response, we see a value property which contains the first 100 users in the organization.
At this point some of you might ask, why only 100? Well that’s the default limit on graph queries, but this can be expanded by using a $top filter on the uri which allows you to query up to 999 users at the same time.
The cool thing with my function is that it detects if your query doesn’t return all the data (has a follow link) and gives a warning in the console.

PS C:\> $Users = Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users' -token $token
WARNING: Query contains more data, use recursive to get all!

So, we just add $top=999 and use the recursive parameter to get them all!

$Users = Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users?$top=999' -token $token -recursive

What if I want to get $top=1 (wat?) users, but recursive? Surely my token will expire after 15 minutes of querying?

Well, yes. That’s why we can pass a tokenrefresh and credentials right into the function and never worry about tokens expiring!

$Users = Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users?$top=1' -token $token -recursive -tokenrefresh -credential $credential -tenantID $TenantID

What if I want to delete a user?

That works as well. Simply change the method (Default = GET) to DELETE and go!

Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users/1800a20f-b228-49d8-a1f6-a3d36b560909' -token $token -method DELETE

Deleting users is fun and all, but how do we create a user?

Define the user details in the body and use the POST method.

$body = @{
    "accountEnabled"= $true
    "displayName" = "Vikingur Saemundsson"
    "mailNickname" = "Vikingur.Saemundsson"
    "userPrincipalName" = "vikingur.saemundsson@xenit.se"
    "passwordProfile" = @{
        "forceChangePasswordNextSignIn" = $true
        "password" = "S00perSecur3Pa55w0rd!"
    }
} | ConvertTo-Json
Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/users' -Body $body -token $token -method POST

What about json-batching, and why is that important?

Json-batching is basically up to 20 unique queries in a single call. Many organizations have thousands of users, if not hundreds of thousands of users, and that adds up since much of the queries need to be run against individual users. And that takes time. Executing jobs with json-batching that used to take 1 hour now takes about 3 minutes to run. 8 hours long jobs now takes about 24 minutes. If you’re not already sold on json-batching then I have no idea why you’re still reading this post.
This can be used statically by creating a body with embedded queries, or as in the example below, dynamically. We have all users flat in a $users variable. Then we determine how many times we need to run the loop and build a $body json object with 20 requests in a single query, then we run the query using the $batch operation and POST method and put them into a $responses array and tada! We’ve made the querying of Graph 20x more efficient.

[array]$responses = @()
$Loops = [math]::Ceiling($Users.Count/10)
$n = 1
$i = 0
0..$Loops | ForEach-Object{
    Write-Progress -Activity "getting user details details" -Status "Job: $_/$Loops" -PercentComplete (100/$Loops*$_)
    $body = @{
        requests = @(
            $users[$i..($i+9)] | ForEach-Object{
            @{
                id="$($n)"
                method="GET"
                url = "/users/$($_.id)/drive/root/children"
            }
            @{
                id="$($n+1)"
                method="GET"
                url = "/users/$($_.id)/mailFolders/inbox/messageRules"
            }
            $n += 2
        }
        )
    } | ConvertTo-Json
    $responses += (Invoke-MSGraphQuery -URI 'https://graph.microsoft.com/v1.0/$batch' -Body $body -token $token -method 'POST').responses
    $i++
}
$responses

Sounds cool, what more can I do?

Almost anything related to the Office 365 suite. Check out the technical resources and documentation for more information. Microsoft is constantly updating and expanding the api functionality. Scroll down for the functions, should work on Powershell 4 and up!

 

Technical resources:

Creating an Azure AD application
https://www.google.com/search?q=create+azure+ad+application
Graph API
https://docs.microsoft.com/en-gb/graph/use-the-api
About batch requests
https://docs.microsoft.com/en-gb/graph/json-batching
Known issues with Graph API
https://docs.microsoft.com/en-gb/graph/known-issues
Thanks to:
https://blogs.technet.microsoft.com/cloudlojik/2018/06/29/connecting-to-microsoft-graph-with-a-native-app-using-powershell/
https://medium.com/@mauridb/calling-azure-rest-api-via-curl-eb10a06127

Functions

please check github page for updated versions

Function Get-MSGraphAuthToken{
<#
.NOTES
   Name: Get-MSGraphAuthToken.ps1
   Author: Vikingur Saemundsson, Xenit AB
   Date Created: 2019-02-26
   Version History:
       2019-02-26 - Vikingur Saemundsson
#>
[cmdletbinding()]
Param(
    [parameter(Mandatory=$true)]
    [pscredential]$credential,
    [parameter(Mandatory=$true)]
    [string]$tenantID
    )
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    #Get token
    $AuthUri = "https://login.microsoftonline.com/$TenantID/oauth2/token"
    $Resource = 'graph.microsoft.com'
    $AuthBody = "grant_type=client_credentials&client_id=$($credential.UserName)&client_secret=$($credential.GetNetworkCredential().Password)&resource=https%3A%2F%2F$Resource%2F"
    $Response = Invoke-RestMethod -Method Post -Uri $AuthUri -Body $AuthBody
    If($Response.access_token){
        return $Response.access_token
    }
    Else{
        Throw "Authentication failed"
    }
}
Function Invoke-MSGraphQuery{
<#
.NOTES
   Name: Invoke-MSGraphQuery.ps1
   Author: Vikingur Saemundsson, Xenit AB
   Date Created: 2019-02-26
   Version History:
       2019-02-26 - Vikingur Saemundsson
#>
[CmdletBinding(DefaultParametersetname="Default")]
Param(
    [Parameter(Mandatory=$true,ParameterSetName='Default')]
    [Parameter(Mandatory=$true,ParameterSetName='Refresh')]
    [string]$URI,
    [Parameter(Mandatory=$false,ParameterSetName='Default')]
    [Parameter(Mandatory=$false,ParameterSetName='Refresh')]
    [string]$Body,
    [Parameter(Mandatory=$true,ParameterSetName='Default')]
    [Parameter(Mandatory=$true,ParameterSetName='Refresh')]
    [string]$token,
    [Parameter(Mandatory=$false,ParameterSetName='Default')]
    [Parameter(Mandatory=$false,ParameterSetName='Refresh')]
    [ValidateSet('GET','POST','PUT','PATCH','DELETE')]
    [string]$method = "GET",
    [Parameter(Mandatory=$false,ParameterSetName='Default')]
    [Parameter(Mandatory=$false,ParameterSetName='Refresh')]
    [switch]$recursive,
    [Parameter(Mandatory=$true,ParameterSetName='Refresh')]
    [switch]$tokenrefresh,
    [Parameter(Mandatory=$true,ParameterSetName='Refresh')]
    [pscredential]$credential,
    [Parameter(Mandatory=$true,ParameterSetName='Refresh')]
    [string]$tenantID
)
    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
    $authHeader = @{
        'Accept'= 'application/json'
        'Content-Type'= 'application/json'
        'Authorization'= $Token
    }
    [array]$returnvalue = $()
    Try{
        If($body){
            $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Body $Body -Method $method -ErrorAction Stop
        }
        Else{
            $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Method $method -ErrorAction Stop
        }
    }
    Catch{
        If(($Error[0].ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.Message -eq 'Access token has expired.' -and $tokenrefresh){
            $token =  Get-MSGraphAuthToken -credential $credential -tenantID $TenantID
            $authHeader = @{
                'Content-Type'='application\json'
                'Authorization'=$Token
            }
            $returnvalue = $()
            If($body){
                $Response = Invoke-RestMethod -Uri $URI -Headers $authHeader -Body $Body -Method $method -ErrorAction Stop
            }
            Else{
                $Response = Invoke-RestMethod -Uri $uri -Headers $authHeader -Method $method
            }
        }
        Else{
            Throw $_
        }
    }
    $returnvalue += $Response
    If(-not $recursive -and $Response.'@odata.nextLink'){
        Write-Warning "Query contains more data, use recursive to get all!"
        Start-Sleep 1
    }
    ElseIf($recursive -and $Response.’@odata.nextLink’){
        If($PSCmdlet.ParameterSetName -eq 'default'){
            If($body){
                $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -ErrorAction SilentlyContinue
            }
            Else{
                $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -ErrorAction SilentlyContinue
            }
        }
        Else{
            If($body){
                $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -body $body -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue
            }
            Else{
                $returnvalue += Invoke-MSGraphQuery -URI $Response.'@odata.nextLink' -token $token -method $method -recursive -tokenrefresh -credential $credential -tenantID $TenantID -ErrorAction SilentlyContinue
            }
        }
    }
    Return $returnvalue
}

Tags : .net core, Azure, Azure AD, Azure AD Application, GET, Graph Explorer, Graph. Graph API, Invoke-MSGraphQuery, Invoke-RestMethod, json, json-batch, Microsoft, Microsoft Graph, Microsoft Graph API, POST, PowerShell, powershell linux, Query, Rest, Service Principal

Personlig rådgivning

Vi erbjuder personlig rådgivning med författaren för 1400 SEK per timme. Anmäl ditt intresse i här så återkommer vi så snart vi kan.

Comments

Brendon says

This is a great resource but you code has some errors:
* minus symbols "-" are corrupted in statement "-Uri $URI –Headers"
* the condition "ElseIf($recursive){" is wrong, should be "ElseIf($recursive -and $Response.'@odata.nextLink'){"

The version on Github is fixed: https://github.com/Freakling/Powershell-MicrosoftGraph

BUT I didn't find that until much later so the version here caused me lots of trouble.

You should upgrade Get-MSGraphAuthToken to use oauth2/v2.0

Also your page rendering is terrible. The headers are too big and the content column is too narrow!

"Invoke-MSGraphQuery" is a great function but this page does it no credit.

says

Thank you for the comment! I've updated the page and added a disclaimer that the functions in the code is not most up to date.

Also, I agree that the page formatting is sub-par and have initiated a internal dialogue to make it wider.

Please create a issue on the github page for oauth2/v2.0 and I'll see what I can do.

Add comment

Your comment will be revised by the site if needed.