The Microsoft Graph API is a service that allows you to read, modify and manage almost every aspect of Azure AD and Office 365 under a single REST API endpoint. Being able to leverage it is an incredibly powerful tool to have when you can manage and automate almost every aspect of Azure AD users, Sharepoint, Microsoft Teams, security, auditing and more!

In this article, you’ll learn the basics in accessing and reading the Microsoft Graph API with PowerShell.

Prerequisites

If you’d like to follow along with me in this article, be sure you first meet the following criteria:

Running Windows PowerShell 5.1 (This is the version I tested with. Others version may work but are not guaranteed)

An Azure tenant

Authenticated to Azure with an account with global admin permissions or app registration permissions on the subscription and a global admin to accept your app registration requests.

Creating an Application Identity for Microsoft Graph API

To access the Microsoft Graph API you first need an identity to get an OAuth token. This is primarily done with an application identity that you can create in the Azure Portal. You can create an application identity via the Azure portal. To do so:

Head over to the Azure Portal and go to Azure Active Directory.

Click on App Registrations under Manage on the left menu and click on the New registration button.

New app registration

Enter a name for your application and click Register.

Copy the Application Id guid for later use.

Registering an application

Creating Secrets for Microsoft Graph API

You can authenticate to the Graph API with two primary methods: AppId/Secret and certificate based authentication. Let’s cover how to authenticate with both methods.

AppId/Secret

An application ID/secret is just like a regular username/password. The application id consists of a GUID instead of a username and the password is just a randomized string.

To create a secret – click on Certificates & secrets in the left menu and press on New client secret.

Creating a client secret

Enter a description for the secret and select when you want it to expire. Now it’s just a matter of asking for permission so that you can access the data that you want.

Certificate

There’s the possibility to create a self-signed certificate and upload it’s public key to Azure. This is the preferred and more secure way of authenticating.

You’ll first need to generate a self-signed certificate. Luckily, this done easily with PowerShell.

# Your tenant name (can something more descriptive as well) $TenantName = "contoso.onmicrosoft.com" # Where to export the certificate without the private key $CerOutputPath = "C:\Temp\PowerShellGraphCert.cer" # What cert store you want it to be in $StoreLocation = "Cert:\CurrentUser\My" # Expiration date of the new certificate $ExpirationDate = (Get-Date).AddYears(2) # Splat for readability $CreateCertificateSplat = @{ FriendlyName = "AzureApp" DnsName = $TenantName CertStoreLocation = $StoreLocation NotAfter = $ExpirationDate KeyExportPolicy = "Exportable" KeySpec = "Signature" Provider = "Microsoft Enhanced RSA and AES Cryptographic Provider" HashAlgorithm = "SHA256" } # Create certificate $Certificate = New-SelfSignedCertificate @CreateCertificateSplat # Get certificate path $CertificatePath = Join-Path -Path $StoreLocation -ChildPath $Certificate.Thumbprint # Export certificate without private key Export-Certificate -Cert $CertificatePath -FilePath $CerOutputPath | Out-Null

Now, upload the self-signed certificate that you exported to $CerOutputPath to your Azure Application by clicking on Certificates & secrets in the left menu and pressing on Upload certificate.

Uploading a certificate

Adding permissions to the application

Giving the application proper permissions is important – not only for the functionality of your app but also for the security. Knowledge of this and (almost) everything else in the Microsoft Graph API can be found in the documentation.

Once I get this set up, I’m going to gather all the security events from my tenant. To be allowed do that I need SecurityEvents.Read.All as a minimum permission. With this I can gather and take action on Impossible Travel events, users connecting via VPN/TOR and such.

To add the SecurityEvents.Read.All to your application – click on API Permissions and then Add Permission. This will present you with not just the Graph API but a ton of other applications in the Azure as well. Most of these applications are easy to connect to once you know how to connect to the Microsoft Graph API.

Click on Microsoft Graph > Application Permissions > Security Events and check the SecurityEvents.Read.All. After that press the Add Permission button.

Did you see that the Admin consent required column was set to Yes on that permission? This means that a tenant admin needs to approve before the permission is added to the application.

Tenant admin permission approval

If you’re a global admin, press the Grant admin consent for or ask a Global Admin to approve it. Asking for permission from the user instead of an admin just setting read/write permission is a big part of OAuth authentication. But this allows us to bypass it for most permissions in Microsoft Graph.

You’ve probably seen this on Facebook or Google already: “Do you allow application X to access your profile?”

Granting admin consent

Now you’re all set – let’s log in get some data!

Acquire an Access Token (Application Id and Secret)

For this we will need to post a request to get an access token from a Microsoft Graph OAuth endpoint. And in the body of that request we need to supply:

client_id – Your Application Id – url encoded

– Your Application Id – url encoded client_secret – Your application secret – url encoded

– Your application secret – url encoded scope – A url-encoded url specifying what you want to access

– A url-encoded url specifying what you want to access grant_type – What authentication method you are using

The URL for the endpoint is https://login.microsoftonline.com/<tenantname>/oauth2/v2.0/token. You can request an access token with PowerShell using the code snippet below.

# Define AppId, secret and scope, your tenant name and endpoint URL $AppId = '2d10909e-0396-49f2-ba2f-854b77c1e45b' $AppSecret = 'abcdefghijklmnopqrstuv12345' $Scope = "https://graph.microsoft.com/.default" $TenantName = "contoso.onmicrosoft.com" $Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" # Add System.Web for urlencode Add-Type -AssemblyName System.Web # Create body $Body = @{ client_id = $AppId client_secret = $AppSecret scope = $Scope grant_type = 'client_credentials' } # Splat the parameters for Invoke-Restmethod for cleaner code $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' # Create string by joining bodylist with '&' Body = $Body Uri = $Url } # Request the token! $Request = Invoke-RestMethod @PostSplat

Acquire an Access Token (Using a Certificate)

The authenticate to the Microsoft Graph API with a certificate is a bit different from the normal AppId/Secret flow. To get an access token using a certificate you have to:

Create a Java Web Token (JWT) header. Create a JWT payload. Sign the JWT header AND payload with the previously created self-signed certificate. This will create a self made access token used for requesting a Microsoft Graph access token. Create a request body containing:

client_id=<application id>

client_assertion=<the JWT>

client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer

scope=<URLEncoded scope>

grant_type=client_credentials

Make a post request with body to the oauth endpoint with Authorization=<JWT> in it’s header.

How to do this wasn’t obvious in Microsoft’s documentation but here’s the PowerShell script for making this happen:

$TenantName = "<your tenant name>.onmicrosoft.com" $AppId = "<your application id" $Certificate = Get-Item Cert:\CurrentUser\My\<self signed and uploaded cert thumbprint> $Scope = "https://graph.microsoft.com/.default" # Create base64 hash of certificate $CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash()) # Create JWT timestamp for expiration $StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime() $JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds $JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0) # Create JWT validity start timestamp $NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds $NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0) # Create JWT header $JWTHeader = @{ alg = "RS256" typ = "JWT" # Use the CertificateBase64Hash and replace/strip to match web encoding of base64 x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '=' } # Create JWT payload $JWTPayLoad = @{ # What endpoint is allowed to use this JWT aud = "https://login.microsoftonline.com/$TenantName/oauth2/token" # Expiration timestamp exp = $JWTExpiration # Issuer = your application iss = $AppId # JWT ID: random guid jti = [guid]::NewGuid() # Not to be used before nbf = $NotBefore # JWT Subject sub = $AppId } # Convert header and payload to base64 $JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json)) $EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte) $JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json)) $EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte) # Join header and Payload with "." to create a valid (unsigned) JWT $JWT = $EncodedHeader + "." + $EncodedPayload # Get the private key object of your certificate $PrivateKey = $Certificate.PrivateKey # Define RSA signature and hashing algorithm $RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1 $HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256 # Create a signature of the JWT $Signature = [Convert]::ToBase64String( $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding) ) -replace '\+','-' -replace '/','_' -replace '=' # Join the signature to the JWT with "." $JWT = $JWT + "." + $Signature # Create a hash with body parameters $Body = @{ client_id = $AppId client_assertion = $JWT client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" scope = $Scope grant_type = "client_credentials" } $Url = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token" # Use the self-generated JWT as Authorization $Header = @{ Authorization = "Bearer $JWT" } # Splat the parameters for Invoke-Restmethod for cleaner code $PostSplat = @{ ContentType = 'application/x-www-form-urlencoded' Method = 'POST' Body = $Body Uri = $Url Headers = $Header } $Request = Invoke-RestMethod @PostSplat

Understanding the Request Access Token Output

Once you’ve obtained an access token either via the application ID/secret or via certificate, should see an object with four properties.

token_type – What kind of token it is

– What kind of token it is expires_in – Time in seconds that the access token is valid

– Time in seconds that the access token is valid ext_expires_in – Like expires_in but for resiliency in case of a token service outage

– Like but for resiliency in case of a token service outage access_token – What we came for

Next up you will create a header using token_type and access_token and start making requests to the Microsoft Graph API.

Making Requests to the Microsoft Graph API

Now start making some requests to the API.

Going with our example, you’ll first need the URL for listing security alerts. Remember to use the Microsoft Graph API documentation to see what’s needed.

In this case, you need a header with Authorization=Bearer <access_token> and a GET request towards the Graph API Alerts endpoint. Here’s how to do that with PowerShell.

# Create header $Header = @{ Authorization = "$($Request.token_type) $($Request.access_token)" } $Uri = "https://graph.microsoft.com/v1.0/security/alerts" # Fetch all security alerts $SecurityAlertsRequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json" $SecurityAlerts = $SecurityAlertsRequest.Value

Now if you have any security alerts in the $SecurityAlerts variable, it should look something like this:

$SecurityAlerts | select eventDateTime,Title eventDateTime title ------------- ----- 2019-08-05T17:59:47.6271981Z Atypical travel 2019-08-05T08:23:01.7325708Z Anonymous IP address 2019-08-05T08:23:55.5000456Z Anonymous IP address 2019-08-04T22:06:51.063797Z Anonymous IP address 2019-08-04T21:56:10.981437Z Anonymous IP address 2019-08-08T09:30:00Z Creation of forwarding/redirect rule 2019-07-19T13:30:00Z eDiscovery search started or exported 2019-07-19T08:00:00Z eDiscovery search started or exported

Inspecting a single security alert as JSON will look like this:

"id": "censored", "azureTenantId": "censored", "azureSubscriptionId": "censored", "riskScore": null, "tags": [ ], "activityGroupName": null, "assignedTo": null, "category": "AnonymousLogin", "closedDateTime": null, "comments": [ ], "confidence": null, "createdDateTime": "2019-08-08T09:46:59.65722253Z", "description": "Sign-in from an anonymous IP address (e.g. Tor browser, anonymizer VPNs)", "detectionIds": [ ], "eventDateTime": "2019-08-08T09:46:59.65722253Z", "feedback": null, "lastModifiedDateTime": "2019-08-08T09:54:30.7256251Z", "recommendedActions": [ ], "severity": "medium", "sourceMaterials": [ ], "status": "newAlert", "title": "Anonymous IP address", "vendorInformation": { "provider": "IPC", "providerVersion": null, "subProvider": null, "vendor": "Microsoft" }, "cloudAppStates": [ ], "fileStates": [ ], "hostStates": [ ], "historyStates": [ ], "malwareStates": [ ], "networkConnections": [ ], "processes": [ ], "registryKeyStates": [ ], "triggers": [ ], "userStates": [ { "aadUserId": "censored", "accountName": "john.doe", "domainName": "contoso.com", "emailRole": "unknown", "isVpn": null, "logonDateTime": "2019-08-08T09:45:59.6174156Z", "logonId": null, "logonIp": "censored", "logonLocation": "Denver, Colorado, US", "logonType": null, "onPremisesSecurityIdentifier": null, "riskScore": null, "userAccountType": null, "userPrincipalName": "[email protected]" } ], "vulnerabilityStates": [ ] }

Understanding and Managing API Output Paging

The Microsoft Graph API has a limit per function on how many items it will return. This limit is per function but let’s say it’s 1000 items. That means that you can only get a max of 1000 items in your request.

When this limit is reached it will use paging to deliver the rest of the items. It does this by adding the @odata.nextLink property to the answer of your request. @odata.nextLink contains a URL that you can call to get the next page of your request.

You can read through all the items by checking for this property and using a loop:

$Uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns" # Fetch all security alerts $AuditLogRequest = Invoke-RestMethod -Uri $Uri -Headers $Header -Method Get -ContentType "application/json" $AuditLogs = @() $AuditLogs+=$AuditLogRequest.value while($AuditLogRequest.'@odata.nextLink' -ne $null) { $AuditLogRequest += Invoke-RestMethod -Uri $AuditLogRequest.'@odata.nextLink' -Headers $Header -Method Get -ContentType "application/json" }

Conclusion

After you learn how to authenticate to the Graph API it’s pretty easy to gather data from it. It’s a powerful service that’s used way less than it should.

Luckily there are many modules out there already to utilize the Microsoft Graph API but to meet your own needs you might need to create your own module for it. Using the skills you’ve learned in this article, you should be well on your way.

For more information about controlling guest access in Office 365, I wrote an in-depth article on my blog I encourage you to check it out.