Get started with PowerShell to run MS Graph API queries – Part 1

Get started with PowerShell to run MS Graph API queries - Part 1

Welcome to 3rd post of the series Learn How to Use Microsoft Graph API with Joy. Today in this blog post, I will try to show how easily you can get started with PowerShell to make MS Graph API calls.

The previous posts of this series is listed below for your convenience.

In the previous posts of this series, we talked about how AUTH plays an important role, discussing the different permissions types and deciding the most suitable MSAL auth flow method to use to work with Microsoft Graph.

Today’s blog post basically serves the purpose of an implementation guide to whatever we have discussed in the previous post, with the context of PowerShell as the client app to make API calls to Microsoft Graph.

But before we start, let’s quickly check out the requirements.

Requirements: The post assumes that your Azure AD user account is assigned any of the Admin roles as mentioned below to be able to perform the portal-end activities.
  
      Application Administrator
      Cloud Application Administrator
      Global Administrator 

Register an Application in Azure AD to connect to Microsoft Graph

Head over to the Azure AD portal and navigate to App registrations and create a New registration.

Get started with PowerShell to run MS Graph API queries - Register App in Azure to establish Client identity with Microsoft Identity platform.
Get started with PowerShell to run MS Graph API queries – Register App in Azure to establish Client identity with Microsoft Identity platform.

Next you have to

  • give a Name to the app
  • specify who can use the app as per your environment,
  • provide a redirect URI,  and finally
  • click on Register.
Note: Though Redirect URI is optional, certain auth scenario requires a Redirect URI to be specified to return auth responses. We need not worry about name resolution as the URL does not needs to be resolved! As such, you may choose to enter in any random URL as the value. In our case, I have put http://localhost as per MS suggestion.
Get started with PowerShell to run MS Graph API queries - Register App in Azure to establish Client identity with Microsoft Identity platform.
Get started with PowerShell to run MS Graph API queries – Register App in Azure to establish Client identity with Microsoft Identity platform.

Once the application gets created, for now, take a note of the following details which you will require later

  • Application (client) ID
  • Directory (tenant) ID

Generate Client Secret for the Application

In the Azure AD portal and from within the App you just created, navigate to Certificates and secrets.

Get started with PowerShell to run MS Graph API queries - Register App in Azure to establish Client identity with Microsoft Identity platform.
Get started with PowerShell to run MS Graph API queries – Register App in Azure to establish Client identity with Microsoft Identity platform.

Notice that you have two options – New Client secret or Upload certificate. I will show you both the methods.

#1 – Generate Client Secret

  • Click on the New client secret button. From the pop-up window that appears as shown,
Generate Client secret for your registered App
Generate Client secret for your registered App
  • Provide a description,
  • Set the expiration term, and
  • Click on Add.

You would be returned to the same screen, but it will show you the Value for the Client secret you just generated. Make a note of it, as we will need it later.

Generate Client secret for your registered App - Note the value of the generated Client Secret as the value gets masked on later and cannot be retrieved without creating a new one.
Generate Client secret for your registered App – Note the value of the generated Client Secret as the value gets masked on later and cannot be retrieved without creating a new one.
IMPORTANT: Make a note of the Client secret Value before changing screens, as this is displayed one-time only. Later, the value will be masked and you would not be able to retrieve it. Only option would be to delete and create a new one. 
Generate Client secret for your registered App - Note the value of the generated Client Secret as the value gets masked on later and cannot be retrieved without creating a new one.
Generate Client secret for your registered App – Note the value of the generated Client Secret as the value gets masked on later and cannot be retrieved without creating a new one.

#2 – Generate Client Secret based on Certificate

This is considered more secured and is mostly used with Application-level authorization scenarios. You can create a self-signed cert using the below sample PS code snippet.

# Your tenant name
$TenantName = "<tenantName>.onmicrosoft.com"
# Cert store you want the created certificate to be stored
$StoreLocation = "Cert:\CurrentUser\My"
# Expiration date of the new certificate (set to X=5 years from present date)
$ExpirationDate    = (Get-Date).AddYears(5)
#Details with which to prepare the certificate 
$CreateCert = @{
    FriendlyName = "<Name_of_App_created_in_Azure>"
    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 @CreateCert
# Set certificate path to store created cert
$CertificatePath = Join-Path -Path $StoreLocation -ChildPath $Certificate.Thumbprint
# Export the certificate without the private key to the export path
$ExportPathCheck = "C:\Temp"
If (!(Test-Path -Path $ExportPathCheck)){
mkdir  $ExportPathCheck
$CerOutPath = "C:\Temp\PowerShellGraphCert.cer"
Export-Certificate -Cert $CertificatePath -FilePath $CerOutPath | Out-Null
}
else
{
$CerOutPath = "C:\Temp\PowerShellGraphCert.cer"
Export-Certificate -Cert $CertificatePath -FilePath $CerOutPath | Out-Null
}

You will find the self-signed cert (.CER) generated in the path as defined in the variable $ExportPathCheck.

All you would need to do is

  • head back to Azure AD portal > App Registrations and from within the App you just created,
  • navigate to Certificates and secrets, and
  • use the Upload certificate to upload the self-signed certificate you just generated.

You should be able to see the cert details such as thumbprint and cert validity period on the screen post upload.

Generate Client secret for your registered App - You can also use a self-signed Certificate for your app to securely authenticate to Microsoft Identity platform.
Generate Client secret for your registered App – You can also use a self-signed Certificate for your app to securely authenticate to Microsoft Identity platform.

Add API Permissions to the Application

  • Next move on to API permissions within the Azure AD Application you created and click on Add a permission.
Add API Permissions to the Application - Authorization is very important to work with Microsoft Graph.
Add API Permissions to the Application – Authorization is very important to work with Microsoft Graph.
  • Next you will be asked to choose the API that your app will work with. Choose Microsoft Graph.
Add API Permissions to the Application - Authorization is very important to work with Microsoft Graph.
Add API Permissions to the Application – Authorization is very important to work with Microsoft Graph.
  • Next you will be prompted to choose the permission typeDelegated or Application
Add API Permissions to the Application - Choose the appropriate permission type for your app.
Add API Permissions to the Application – Choose the appropriate permission type for your app.
Use the Microsoft Graph beta and v1 reference documentation to identify the permissions required for the particular resources that your application (program/script) would be working with. Again, a security best practice is to try use the permission with least privilege.

Once you have identified all the permissions required for your application to work,

  • select the Permission type and then select all the permission required for your application. You can use the search option.
Most of the Graph API requires permissions to be consented for by the Admin before they can be used.
Most of the Graph API requires permissions to be consented for by the Admin before they can be used.
Notice that some (majority) permissions will state that it requires Admin consent, meaning an Admin needs to approve before the permission is added to the application. As such, when you add the permission, you will see the status of the permission as Not Granted for Admin consent.
Admin can grant consent for required permissions for all users of the tenant so that the users will not have to consent to the permission individually upon sign-in.
Admin can grant consent for required permissions for all users of the tenant so that the users will not have to consent to the permission individually upon sign-in.
  • Click on the Grant admin consent button. The status for each permission will change to Granted state.
Admin can grant consent for required permissions for all users of the tenant so that the users will not have to consent to the permission individually upon sign-in.
Admin can grant consent for required permissions for all users of the tenant so that the users will not have to consent to the permission individually upon sign-in.
NOTE #1: If your user account does not have the Application Admin/Cloud Application Admin/Global Admin role assigned, and you only work on script/programming end (likely a Developer), you would need to furnish the above details to someone whose account has any of the mentioned roles assigned, so that they can review and complete the steps till this.
NOTE #2: If your user account does not have the Application Admin/Cloud Application Admin/Global Admin role assigned, and the application will be running as on-behalf of your account with delegated permission, make sure you mention the same to the Admin so that your account is added as a user to the Application.

[OPT] Modify Application manifest in Azure to support Public Client connection

This step is only required if your app would be using either ROPC auth flow, Device code auth flow or Windows Integrated auth flow. Since we will be dealing with Authorization Code and Client Credential auth flows, this is not required for the purpose of this blog. 

In the Azure portal, from the App Registrations screen, select your App and then go to Authentication and EnableAllow public client flows.

Allow public client flow is required to be enabled for your app only if it uses the specified MSAL auth flow methods.
Allow public client flow is required to be enabled for your app only if it uses the specified MSAL auth flow methods.

You can also do this by making a change in the application manifest – set “allowPublicClient” to true.

Allow public client flow is required to be enabled for your app only if it uses the specified MSAL auth flow methods.
Allow public client flow is required to be enabled for your app only if it uses the specified MSAL auth flow methods.

This is all required to be configured from the portal to register your app/script with the Microsoft Identity platform. With this, let’s now move on to the client-side.

MSAL Auth implementation in PowerShell to work with Microsoft GRAPH

In the previous post, we talked about the two most suitable MSAL supported Oauth2 auth flow methods for working with Microsoft GraphAuthorization Code and Client Credentials auth flow methods.

Here, let’s see how we can implement the two auth flow methods in PowerShell to obtain an Access Token from the Microsoft Identity platform to work with Microsoft Graph.

PowerShell code sample to implement Authorization Code Auth flow

#Define Client Variables Here
#############################
$clientId = "b3fac597-e86a-49a6-a42f-25bb250c7aa2"
$clientSecret = "X32-_Cpl8~_dH-0MeK8U6hvCQZs.H2DOHv"
$resource = "https://graph.microsoft.com"
$scope = "https://graph.microsoft.com/.default"
$redirectUri = "https://localhost"

#UrlEncode variables for special characters
###########################################
Add-Type -AssemblyName System.Web
$clientSecretEncoded = [System.Web.HttpUtility]::UrlEncode($clientSecret)
$redirectUriEncoded =  [System.Web.HttpUtility]::UrlEncode($redirectUri)
$resourceEncoded = [System.Web.HttpUtility]::UrlEncode($resource)
$scopeEncoded = [System.Web.HttpUtility]::UrlEncode($scope)

#Obtain Authorization Code
##########################
Add-Type -AssemblyName System.Windows.Forms
$form = New-Object -TypeName System.Windows.Forms.Form -Property @{Width=440;Height=640}
$web  = New-Object -TypeName System.Windows.Forms.WebBrowser -Property @{Width=420;Height=600;Url=($url -f ($Scope -join "%20")) }
$url = "https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&redirect_uri=$redirectUriEncoded&client_id=$clientID&resource=$resourceEncoded&prompt=admin_consent&scope=$scopeEncoded"
$DocComp  = {
        $Global:uri = $web.Url.AbsoluteUri        
        if ($Global:uri -match "error=[^&]*|code=[^&]*") {$form.Close() }
    }
$web.ScriptErrorsSuppressed = $true
$web.Add_DocumentCompleted($DocComp)
$form.Controls.Add($web)
$form.Add_Shown({$form.Activate()})
$form.ShowDialog() | Out-Null
$queryOutput = [System.Web.HttpUtility]::ParseQueryString($web.Url.Query)
$output = @{}
foreach($key in $queryOutput.Keys){
    $output["$key"] = $queryOutput[$key]
}
$regex = '(?<=code=)(.*)(?=&)'
$authCode  = ($uri | Select-string -pattern $regex).Matches[0].Value

#Get Access Token with obtained Auth Code
#########################################
$body = "grant_type=authorization_code&redirect_uri=$redirectUri&client_id=$clientId&client_secret=$clientSecretEncoded&code=$authCode&resource=$resource"
$authUri = "https://login.microsoftonline.com/common/oauth2/token"
$tokenResponse = Invoke-RestMethod -Uri $authUri -Method Post -Body $body -ErrorAction STOP

PowerShell code sample to implement Client Credential auth using Self-Signed Certificate

#Define Client Variables Here
########################
$TenantName = "blueear.onmicrosoft.com"
$AppId = "b3fac597-e86a-49a6-a42f-25bb250c7aa2"
$Certificate = Get-Item Cert:\CurrentUser\My\9c2abfb10dc868e1eaf2e5c2bc7fb7f7ec496e93
$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"
    x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}

#Create JWT payload
#################
$JWTPayLoad = @{
       aud = "https://login.microsoftonline.com/$TenantName/oauth2/token"
       exp = $JWTExpiration
       iss = $AppId
       jti = [guid]::NewGuid()
       nbf = $NotBefore
       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

# Use the self-generated JWT as Authorization to get the Access Token
##########################################################
$Header = @{
    Authorization = "Bearer $JWT"
}

$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"
}

$authUri = "https://login.microsoftonline.com/common/oauth2/token"

$TokenResponse = Invoke-RestMethod -Header $Header -Uri $authUri -Method POST -Body $Body

PowerShell code sample to implement Client Credential auth using Client Secret

#Define Client Variables Here
#############################
$TenantName = "blueear.onmicrosoft.com"
$clientID = "b3fac597-e86a-49a6-a42f-25bb250c7aa2"
$clientSecret = "X32-_Cpl8~_dH-0MeK8U6hvCQZs.H2DOHv"
$Scope = "https://graph.microsoft.com/.default"

#Construct Auth call to get Access Token 
#########################################
$Body = @{
    Grant_Type = "client_credentials"
    Scope = $Scope
    client_Id = $clientID
    Client_Secret = $clientSecret
}
$authUri = "https://login.microsoftonline.com/$TenantName/oauth2/v2.0/token"
 
$TokenResponse = Invoke-RestMethod -Uri $authUri -Method POST -Body $Body
Client Credential Auth flow with Client Secret requires the shortest code and hence easier to implement than the rest.

Verify the Access Token from Auth Response

Irrespective of the Auth flow method used, the Auth response is stored in the variable $TokenResponse with which you can check the access token acquired.

Verify the Access Token obtained from the Microsoft Identity platform. Note that this Access Token is obtained for a Delegated interactive user sign-in.
Verify the Access Token obtained from the Microsoft Identity platform. Note that this Access Token is obtained for a Delegated interactive user sign-in.

Further, if you have the JWTDetails PS module installed, you can see the claims contained by the Access Token as shown below.

You can further see the Access Token claims within PowerShell by using the JWTDetails PS Module.
You can further see the Access Token claims within PowerShell by using the JWTDetails PS Module.

Now that your app has an access token, you can start using to build the actual part of your script.

To be Contd…

Note that the PowerShell code samples as shown above are derived from the examples already available in the internet and are not something not written by me. The purpose of reuse is to showcase the MSAL Auth implementation with PowerShell.

That was all for today. In the next post of this series, I will be showing you how to construct Graph API calls in PowerShell. Stay tuned!