Posted in : Azure, Microsoft, Office 365, Powershell Av Stina Perbo Utas Översätt med 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