PowerShell, MS Graph API, Azure Automation, and Intune

The goal of this post is to share my experience and to teach and help others who need it, to make life easier. I want to focus on building some usable PowerShell functions to get you automating with Azure Automation PowerShell Runbooks (and PowerShell itself) using MS Graph API, in which the same concepts can be used for other APIs as well, so you can tie different services together!

Note: I’m publishing this post early to make the finished content available immediately, and to receive feedback while I’m working on it. I unfortunately don’t have a lot of free time, so check back periodically for updates.

I’ve been quite busy lately working with automation using different APIs, PowerShell, and Azure. Quite a few of the things I am doing were tough to figure out, either because there was so little information online about it, or really, nothing at all. This led me to figuring it out on my own, collaborating with colleges, or designing ways to do things that simply are not directly enabled.

Overview

I’m going to try to briefly cover or build through the following things (not necessarily in this order), enough to get you going and to meet the goals of this post:

  • MS Graph REST API
    • OAuth2 Token
      • Connecting with registered Azure App and User credentials
    • Request Header
    • Request Body
  • Credential Manager – Using in PowerShell and Runbooks
    • Windows 10 Credential Manager
    • Azure Automation Credential Manager
  • Auth Token refresh or adding timestamp
  • Building request header, GET
  • Building request body for POST and PATCH
  • Automation via PowerShell and MS Graph API
  • If you want to see more, let me know in the comments!

MS Graph REST API

Microsoft’s Graph API is excellent. Basically, you can use the Microsoft Graph REST APIs to access, create, and manipulate data in basically all Microsoft services, such as Azure Active Directory, Office 365 services, Enterprise Mobility / Intune and Security services, Windows 10 services, Dynamics 365, and more. When you pair this with a scripting or programming language, you can automate all the things! You can build some pretty cool dashboards, apps, anything really.

Connecting to MS Graph API

If you want to connect to a modern REST API like MS Graph, you need to obtain an OAuth2 token. To do this, you will need to authenticate with one of the following:

  • User credentials
  • Registered App credentials (ID & Secret)
  • User + Registered App credentials

Then, either the App must have the appropriate “App Permissions”, or appropriate “delegated” permissions, and/or the user must have the appropriate Administrative role permissions… altogether for the given API you want to work with.

There are plenty of simple guides out there covering how to create an Azure AD user, assign appropriate Administrative role permissions to the user, register an app, and assign appropriate API permissions to the app. That’s outside the scope of this post. As I explained earlier, I want to instead focus on building some usable PowerShell functions to get you automating!

For rest of this post, I’m going to assume you have a user, a registered app, and they have the appropriate Administrative roles and API permissions. I have provided some references to this at the bottom of the post.

Credential Manager

The first concern in this whole thing is credentials. How can I automate all the things if I need to input credentials, because you obviously don’t want to store them in a file, whether hashed, encrypted, or whatever. To me, it adds too much extra hassle and risk potential.

Wouldn’t you rather store them securely inside a built-in “Credential Manager”, that you can simply use from within a PowerShell script, whether running locally or from an Azure PowerShell Runbook? I thought so!

For automating scripts on a Windows Server, you have a PowerShell Module available in which you can import and use to retrieve stored credentials that are in the built-in Windows Credential Manager.

For PowerShell Runbooks in Azure Automation, you can do the same thing, but that functionality is already built in! You don’t need to do anything!

Credential Manager in Windows Server

One important note to keep in mind, is that typically, your script will run as the local built-in SYSTEM account. What I’m getting at, is when your script runs as SYSTEM, it will be referencing the SYSTEM account’s Credential Manager.

This means you need to add credentials to the SYSTEM account’s Credential Manager, which also means you need to do this by running PowerShell as the SYSTEM account. This is easy to do with PSEXEC:

psexec.exe -i -s PowerShell_ISE.exe

Once you are running PowerShell as the local SYSTEM account, you can enter the following commands to add credentials into the SYSTEM account’s Windows Credential Manager:

Get-Credential -UserName "appID" -Message "Enter password" | New-StoredCredential -Target "EasyToReferenceAppIDName" -Persist Enterprise
Get-Credential -UserName "serviceUserName" -Message "Enter password" | New-StoredCredential -Target "EasyToReferenceUNName" -Persist Enterprise

What that will do is prompt you to enter a password, then it will save the username or client ID (app ID) and password you entered in the Windows Credential Manager under the reference name you specified after the -Target switch.

Using Credentials in PowerShell Scripts

The first thing we need to do in the PowerShell script is install the module, them import it. I like to run a check first. The whole thing looks like this:

# Check if CredentialManager PowerShell module is loaded, if not, load it.
    $credManMod = Get-Module -Name CredentialManager
    if (!$credManMod) {
        Install-Module -Name CredentialManager
        Import-Module -Name CredentialManager
    }

Note: There is a newer module available called “BetterCredentials“. You may use that one as well, similarly, but you’ll need to use the -AllowClobber switch when installing the module. I don’t know how that will effect functionality.

Then call the credentials from Windows Credential Manager into your PowerShell script. In this example, the script will need ClientID and User credentials to later authenticate before obtaining an OAuth2 token:

# Get the stored credentials in Windows Credential Manager, and make it easy to use with MS Graph authentication
	$graphClientCreds = Get-StoredCredential -Target "EasyToReferenceClientIDName" -AsCredentialObject
    $graphUserCreds = Get-StoredCredential -Target "EasyToReferenceUNName" -AsCredentialObject

Credential Manager in Azure Automation

In your Azure Automation account:

Home > Automation Accounts > your-automation-account > Credentials > Add a Credential

All you need to do here is name the credentials (used for referencing them within the PowerShell Runbook), and save the username and password.

Using Credentials in Azure PowerShell Runbooks

This is similar to PowerShell on Windows, but a bit easier and less work involved, and only a slight twist to get it working. There’s no need for a module, as that functionality is built-in! All we need to do is retrieve the credentials and store them in a variable to use a bit later:

# Get the stored credentials from Azure Automation Credential Manager to use in the PowerShell Runbook for MS Graph authentication
    $graphClientCreds = Get-AutomationPSCredential -Name 'EasyToReferenceAppIDName'
    $graphClientPW = $graphClientCreds.GetNetworkCredential().Password
    $graphUserCreds = Get-AutomationPSCredential -Name 'EasyToReferenceUNName'
    $graphUserPW = $graphUserCreds.GetNetworkCredential().Password

Requesting an OAuth2 Token

Now that we have a way to securely store and retrieve credentials, it’s time to request and obtain an OAuth2 Token that will be used to authenticate every MS Graph API request.

Each time we need to connect to the API, we will need to provide this token in the request header. Basically, the way we get this auth token is by POSTing a REST API request to MS Graph. This request will contain the following:

  • Method – The type of request we are sending (GET, POST, PATCH, etc.)
  • URI – A URL to the API where we are sending the request to
  • Body – This contains all the info required to get you your Auth Token
    • scope: Scope of the request
    • grant_type: Type of authentication
    • client_id: The Azure registered App/Client ID
    • client_secret: It’s password
    • username: The username used to delegate permissions from
    • password: The users’ password

If everything works out after sending the above request to MS Graph, we will receive our Token, which we can easily include in the header of every future API request.

Building the Token Request

Let’s build the token request body according to what we need from above. The method type in this case will be set later using a switch in the cmdlet.

First we define the Uri:
(change “contoso” to match your own Azure tenant name, or this will not work)

$tenantName = "contoso"
$graphRequestUri = "https://login.microsoftonline.com/$tenantName.onmicrosoft.com/oauth2/v2.0/token"

Then we need to build the token request body using one of the following:

# MS Graph Token Request Body for Windows PowerShell
$graphTokenRequestBody = @{
	"scope" = "https://graph.microsoft.com/.default";
	"grant_type" = "password";
	"client_id" = "$($graphClientCreds.UserName)";
	"client_secret" = "$($graphClientCreds.Password)";
	"username" = "$($graphUserCreds.UserName)";
	"password" = "$($graphUserCreds.Password)";
}
# MS Graph Token Request Body for Azure PowerShell Runbook:
$graphTokenRequestBody = @{
	"scope" = "https://graph.microsoft.com/.default";
	"grant_type" = "password";
	"client_id" = "$($graphClientCreds.UserName)";
	"client_secret" = "$graphClientPW";
	"username" = "$($graphUserCreds.UserName)";
	"password" = "$graphUserPW";
}

Token Refresh

The grant_type of “password” does not give us a refresh token. So I’ve come up with a way to automatically grab another Auth Token when it’s about to expire. They expire after an hour.

To take advantage of a function I wrote to automatically refresh, it requires a timestamp added to the token at the time the token was received. I’ll point this out below in the completed function to connect to MS Graph and receive an Auth Token.

Getting Your OAuth2 Token!

Putting it all together in a simple function, this is what you will end up with.

# Example function for obtaining OAuth2 Token for MS Graph API.
# This example is specific to Windows PowerShell 5.1
function Connect-MSGraphAPI {

	# Check if CredentialManager PowerShell module is loaded, if not, load it:
    $credManMod = Get-Module -Name CredentialManager
    if (!$credManMod) {
        Install-Module -Name CredentialManager
        Import-Module -Name CredentialManager
    }

	# Define the URI using your own Azure tenant name:
    $tenantName = "contoso"
	$graphRequestUri = "https://login.microsoftonline.com/$tenantName.onmicrosoft.com/oauth2/v2.0/token"

    # Get the stored credentials in Windows Credential Manager, and make it easy to use with MS Graph authentication:
	$graphClientCreds = Get-StoredCredential -Target "EasyToReferenceClientIDName" -AsCredentialObject
    $graphUserCreds = Get-StoredCredential -Target "EasyToReferenceUNName" -AsCredentialObject
	
	# MS Graph Token request body for Windows Powershell:
    $graphTokenRequestBody = @{
        "scope" = "https://graph.microsoft.com/.default";
        "grant_type" = "password";
        "client_id" = "$($graphClientCreds.UserName)";
        "client_secret" = "$($graphClientCreds.Password)";
        "username" = "$($graphUserCreds.UserName)";
        "password" = "$($graphUserCreds.Password)";
    }
	
	# Get the current datetime and add one hour to it:
	$graphTokenExpirationDate = (Get-Date).AddHours(1)
	
	# Make sure the error variable starts empty:
    $GraphAPITokenRequestError = $null
	
	# Send the Post request to Microsoft and receive an OAuth2 token:
	$script:GraphAPIAuthResult = (Invoke-RestMethod -Method Post -Uri $graphRequestUri -Body $graphTokenRequestBody -ErrorAction SilentlyContinue -ErrorVariable GraphAPITokenRequestError)

	# If there's an error requesting the token, say so, display the error, and break:
    if ($GraphAPITokenRequestError) {
        Write-Output "FAILED - Unable to retreive MS Graph API Authentication Token - $($GraphAPITokenRequestError)"
        Break
	}
	
	# Add a +1 hour timestamp to the OAuth2 token response:
    $script:GraphAPIAuthResult | Add-Member -NotePropertyName expiration_time -NotePropertyValue $graphTokenExpirationDate
}

If it is successful, the response will be in the $script:GraphAPIAuthResult variable and, you will get a response that includes the following:

  • Token_type: Bearer
  • scope: The permissions scope of the received token – a list of permissions the token gives authorization to.
  • expires_in: When the token expires. It is one hour.
  • access_token: The actual token itself. It looks like a Base64 encoded block of text.
  • expiration_time: This is the extra bit we manually added, that says when the token expires. This is useful and explained more soon.

Now that we have our OAuth2 Token, we are free make all the MS Graph REST API requests we would like, for the next hour until it expires. We’ll get to that, but first, all MS Graph API requests must include a header. So let’s built that next!

Building an MS Graph API Header

Now that we have our token, we’ll need to include it in the header of each MS Graph API request we send.

This header must include two things:

  • Authorization: Token type and the token itself.
  • Host: The API where we are sending the request to.

To make life easy, let’s create a function to do this:

# SET THE HEADER FOR ALL MS GRAPH API REQUESTS
	function Set-GraphAPIRequestHeader {
		$script:graphAPIReqHeader = @{
			Authorization = "Bearer $($script:GraphAPIAuthResult.access_token)"
			Host = "graph.microsoft.com"
		}
}

Verify and Refresh the Token

This one will probably be the most important function in your automation script. This verifies the token and it’s status, gets another one if close to expiration or missing, and sets the headers with the current and valid token. This is why we set the expiration property in the token request earlier!

This is actually the function you will use throughout your API or automation scripts every time you want to make a MS Graph API request. It does all the dirty work regarding obtaining your auth token and setting the request header to the current token. It’s simple, but highly effective… it works very well in my use cases and I haven’t come across a need to do it differently. I’m always open to suggestions and critique for improvement, however!

# CHECK AUTH TOKEN STATUS, GET ANOTHER IF CLOSE TO EXPIRATION, SET HEADERS WITH IT IF ALL IS WELL
	function Invoke-GraphAPIAuthTokenCheck {
		$currentDateTimePlusTen = (Get-Date).AddMinutes(10)
		if ($script:GraphAPIAuthResult) {
			if (!($currentDateTimePlusTen -le $script:GraphAPIAuthResult.expiration_time)) {
				Connect-MSGraphAPI
				Set-GraphAPIRequestHeader
			} else {
				Set-GraphAPIRequestHeader
			}
		} else {
			Connect-MSGraphAPI
			Invoke-GraphAPIAuthTokenCheck
		}
	}

Note: Just to be clear, each time you want to send an MS Graph API request, use this function. It will automatically call the other functions we’ve gone over when needed, and take care of refreshing the token if needed.

End Result: Completed Function!

This is basically what the whole top part of your script should look like. Pay attention to the very last line “Invoke-GraphAPIAuthTokenCheck“. This is the only part you need from this section from here on out when you want to make an API request. I also want to be clear that this exact same concept will work with other OAuth2 REST APIs as well, not just MS Graph API. Only very slight tweaking is necessary of the body and header, which really only depends on what the other APIs require 🙂

# Example function for obtaining OAuth2 Token for MS Graph API.
# This example is specific to Windows PowerShell 5.1
	function Connect-MSGraphAPI {
	
		# Check if CredentialManager PowerShell module is loaded, if not, load it:
		$credManMod = Get-Module -Name CredentialManager
		if (!$credManMod) {
			Install-Module -Name CredentialManager
			Import-Module -Name CredentialManager
		}

		# Define the URI using your own Azure tenant name:
		$tenantName = "contoso"
		$graphRequestUri = "https://login.microsoftonline.com/$tenantName.onmicrosoft.com/oauth2/v2.0/token"

		# Get the stored credentials in Windows Credential Manager, and make it easy to use with MS Graph authentication:
		$graphClientCreds = Get-StoredCredential -Target "EasyToReferenceClientIDName" -AsCredentialObject
		$graphUserCreds = Get-StoredCredential -Target "EasyToReferenceUNName" -AsCredentialObject
		
		# MS Graph Token request body for Windows Powershell:
		$graphTokenRequestBody = @{
			"scope" = "https://graph.microsoft.com/.default";
			"grant_type" = "password";
			"client_id" = "$($graphClientCreds.UserName)";
			"client_secret" = "$($graphClientCreds.Password)";
			"username" = "$($graphUserCreds.UserName)";
			"password" = "$($graphUserCreds.Password)";
		}
		
		# Get the current datetime and add one hour to it:
		$graphTokenExpirationDate = (Get-Date).AddHours(1)
		
		# Make sure the error variable starts empty:
		$GraphAPITokenRequestError = $null
		
		# Send the Post request to Microsoft and receive an OAuth2 token:
		$script:GraphAPIAuthResult = (Invoke-RestMethod -Method Post -Uri $graphRequestUri -Body $graphTokenRequestBody -ErrorAction SilentlyContinue -ErrorVariable GraphAPITokenRequestError)

		# If there's an error requesting the token, say so, display the error, and break:
		if ($GraphAPITokenRequestError) {
			Write-Output "FAILED - Unable to retreive MS Graph API Authentication Token - $($GraphAPITokenRequestError)"
			Break
		}
		
		# Add a +1 hour timestamp to the OAuth2 token response:
		$script:GraphAPIAuthResult | Add-Member -NotePropertyName expiration_time -NotePropertyValue $graphTokenExpirationDate
	}

# SET THE HEADER FOR ALL MS GRAPH API REQUESTS
	function Set-GraphAPIRequestHeader {
		$script:graphAPIReqHeader = @{
			Authorization = "Bearer $($script:GraphAPIAuthResult.access_token)"
			Host = "graph.microsoft.com"
		}
	}

# CHECK AUTH TOKEN STATUS, GET ANOTHER IF CLOSE TO EXPIRATION, SET HEADERS WITH IT IF ALL IS WELL
	function Invoke-GraphAPIAuthTokenCheck {
		$currentDateTimePlusTen = (Get-Date).AddMinutes(10)
		if ($script:GraphAPIAuthResult) {
			if (!($currentDateTimePlusTen -le $script:GraphAPIAuthResult.expiration_time)) {
				Connect-MSGraphAPI
				Set-GraphAPIRequestHeader
			} else {
				Set-GraphAPIRequestHeader
			}
		} else {
			Connect-MSGraphAPI
			Invoke-GraphAPIAuthTokenCheck
		}
	}

Invoke-GraphAPIAuthTokenCheck

More to come! This may end up being a multi-part post.

 

 

7 Comments

  1. Finally i found :

    $test = Invoke-RestMethod -Method Get -Uri $uri -ErrorAction SilentlyContinue -ErrorVariable GraphAPITokenRequestError -Headers $script:graphAPIReqHeader

    🙂

  2. Hello,

    Thanks for this code is very usefull but how send an api request after the connection to the Graph API ?

  3. Hi Timothy, I am get an error when trying to run the code, the creds for the Automation Account are accepted but then an application called IntuneAutomation can’t be found. I don’t have app registered with that name. IntuneAutomation isn’t referenced in the code runbook, so I can’t see where it is getting picked up from. Any ideas?

    Any help would be much appreciated.

    FAILED – Unable to retrieve MS Graph API Authentication Token – System.Management.Automation.CmdletInvocationException: {“error”:”unauthorized_client”,”error_description”:”AADSTS700016: Application with identifier ‘IntuneAutomation’ was not found in the directory ‘mytenantname .onmicrosoft.com’. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.\r\nTrace ID: 89683461-74c7-40ea-9634-ea4f1f7c0000\r\nCorrelation ID: 2d43be05-294c-4c74-89ac-f2e527c71f5a\r\nTimestamp: 2019-07-24 07:54:37Z”,”error_codes”:[700016],”timestamp”:”2019-07-24 07:54:37Z”,”trace_id”:”89683461-74c7-40ea-9634-ea4f1f7c0000″,”correlation_id”:”2d43be05-294c-4c74-89ac-f2e527c71f5a”,”error_uri”:”https://login.microsoftonline.com/error?code=700016″} —> System.Net.WebException: The remote server returned an error: (400) Bad Request.
    at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
    at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
    — End of inner exception stack trace —
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecuteEnumerate(Object input)
    at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements, CommandBaseAst[] pipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext funcContext)
    at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
    at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
    at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)

    Thanks
    Sarah

  4. Hi Timothy, I am get an error when trying to run the code, the creds for the Automation Account are accepted but then an application called IntuneAutomation can’t be found. I don’t have app registered with that name. IntuneAutomation isn’t referenced in the code runbook, so I can’t see where it is getting picked up from. Any ideas?

    Any help would be much appreciated.

    FAILED – Unable to retrieve MS Graph API Authentication Token – System.Management.Automation.CmdletInvocationException: {“error”:”unauthorized_client”,”error_description”:”AADSTS700016: Application with identifier ‘IntuneAutomation’ was not found in the directory ‘mytenantname .onmicrosoft.com’. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You may have sent your authentication request to the wrong tenant.\r\nTrace ID: 89683461-74c7-40ea-9634-ea4f1f7c0000\r\nCorrelation ID: 2d43be05-294c-4c74-89ac-f2e527c71f5a\r\nTimestamp: 2019-07-24 07:54:37Z”,”error_codes”:[700016],”timestamp”:”2019-07-24 07:54:37Z”,”trace_id”:”89683461-74c7-40ea-9634-ea4f1f7c0000″,”correlation_id”:”2d43be05-294c-4c74-89ac-f2e527c71f5a”,”error_uri”:”https://login.microsoftonline.com/error?code=700016″} —> System.Net.WebException: The remote server returned an error: (400) Bad Request.
    at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.GetResponse(WebRequest request)
    at Microsoft.PowerShell.Commands.WebRequestPSCmdlet.ProcessRecord()
    — End of inner exception stack trace —
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
    at System.Management.Automation.Internal.PipelineProcessor.SynchronousExecuteEnumerate(Object input)
    at System.Management.Automation.PipelineOps.InvokePipeline(Object input, Boolean ignoreInput, CommandParameterInternal[][] pipeElements, CommandBaseAst[] pipeElementAsts, CommandRedirection[][] commandRedirections, FunctionContext funcContext)
    at System.Management.Automation.Interpreter.ActionCallInstruction`6.Run(InterpretedFrame frame)
    at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
    at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)

    Thanks
    Sarah

  5. Daniel Oakley

    This write up was extremely helpful to me. I spent about 20 hours this weekend trying to get a PowerShell Azure Automation Script working that would set the device category of uncategorized devices. I ran into a permissions error due to the lack of functionality with Client App support ManagedDevices. Your instructions helped me used Credential Manager to solve the problem. Thank you!

  6. Pingback: ICYMI: PowerShell Week of 17-May-2019 | PowerShell.org

Leave a Reply

Your email address will not be published. Required fields are marked *