Office 365 License Alerts – Send alerts to a Teams channel with adaptive cards

As of today, there are still no great automated ways to stay on top of your Office 365 Microsoft 365 licensing. However, there are a myriad of options, if you are willing to put in the time to configure this for your organization.

In this post, we will be using Azure Runbooks to pull license info from Microsoft Graph and then send it to a teams channel VIA an incoming Webhooks as an adaptive card with an actionable button.

App Registration

Thanks to JanBakker.Tech for some great instructions on completing this with Power Automate. All of the content from the app registration section is from his site.

Let’s start with creating an app registration in Azure AD. You can find your app registrations under Active Directory -> App registrations in the Azure portal. Give it a proper name, and leave the rest as is.

Next, make sure the app registration has the right permissions. Since we are using the List subscribedSkus API, you’ll need at least Organization.Read.All application permissions for Graph API. Don’t forget to configure the admin consent afterward.

Next, we’ll need a client secret for authentication. Create one and copy the secret to notepad. We need this in the next steps. Also, take note of the Application (client) ID en Directory (tenant) ID. So if you followed all the steps, you end up with:

  • An app registration with the right Graph API permissions
  • A client secret
  • The Application (client) ID
  • The Directory (tenant) ID

Write the code

If you opt to use Jan Bakker’s method, you can stop reading and continue on his site. Otherwise, bookmark his site and go back to it later for some other great resources.

Registered App Details

First, we setup the variables for the connection to our tenant using our registered app

$clientID = 'your-app-client-id'
$clientSecret = 'supersecretcode...'
$tenantID = 'your-tenant-id'

The following “Requesting a Token” and “Getting Data from Azure AD” were both taken from Emanuel Palm’s post on Pipehow.tech. Please check out his work for some more insights.

Requesting a Token

Parameters

To get an auth token using the client credentials flow we will need some information for the parameters of the request.

Tenant Id

The tenant or directory id is the id of your Azure AD tenant and can be found in the overview section of your Azure AD in the portal, among other places.

Client Id

The client or application id is the id of your registered app, found in the overview section of the registered app.

Scope

The scope parameter defines what parts that the auth token should be valid for. In our case we can use https://graph.microsoft.com/.default which will give us a token with the scope of all API permissions that the app has been granted. You can use this as a tool for more granular control, to restrict what the token should be valid for if the app has other permissions that will not be utilized by this specific token.

Client Secret

The secret that we previously created for our app.

Grant Type

The grant type decides the auth flow, we are using client_credentials to let the app work without user interaction.

We now have what we need to retrieve a token, so let’s assemble a body for our token request.

# Create a hashtable for the body, the data needed for the token request
# The variables used are explained above
$Body = @{
    'tenant' = $TenantId
    'client_id' = $ClientId
    'scope' = 'https://graph.microsoft.com/.default'
    'client_secret' = $ClientSecret
    'grant_type' = 'client_credentials'
}

# Assemble a hashtable for splatting parameters, for readability
# The tenant id is used in the uri of the request as well as the body
$Params = @{
    'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
    'Method' = 'Post'
    'Body' = $Body
    'ContentType' = 'application/x-www-form-urlencoded'
}

$AuthResponse = Invoke-RestMethod @Params

We now have a graph token! The response contains metadata such as expiration time, and the token itself consisting of a long encoded string in a format called JSON Web Token (JWT). A JWT can be decoded to see more data such as what account or application it is for and what scopes are included, for example using Microsoft’s own decoder.

Getting Data from Azure AD

Now that we have a token we can simply send the token in the headers of our API request to read or modify whichever resource the token grants us access to, in this case to read the directory. As an example we can send a request to the /subscribedskus endpoint.

$Headers = @{
    'Authorization' = "Bearer $($AuthResponse.access_token)"
}

$Result = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/subscribedskus' -Headers $Headers

$Result now contains all the subscribedskus in the specified Azure AD.

Gathering the License Info with Some IF Logic

Let’s define our variables for our report. What happens, when we gather the $Result data, is that we have a JSON which is a little hard to work with. So, we are going to create some objects, because powershell loves objects.

$obj = @()
$report = @()

$v = $Result.value

foreach($o in $v){

$obj = New-Object PSObject
$obj | Add-Member -Type NoteProperty -Name License -Value $o.skupartnumber
$obj | Add-Member -Type NoteProperty -Name Status -Value $o.CapabilityStatus
$obj | Add-Member -Type NoteProperty -Name ConsumedUnits -Value $o.ConsumedUnits
$obj | Add-Member -Type NoteProperty -Name PrepaidUnits -Value $o.PrepaidUnits.enabled

$report += $obj
}

Now we are going to loop through the report with some logic. Basically, if this will ignore any sku that is disabled, AND has more than “0” consumed licenses.

foreach($o in $report){

IF($o.status -eq "enabled" -and $o.consumedunits -gt 0){

$cu = [int]$o.consumedUnits
$ppu = [int]$o.prepaidunits
$a = $ppu-$cu
$p = [math]::Round(($cu/$ppu)*100,0)
IF($p -ge 95){
$licensename = $o.license
$licensestatus = $o.status
$Percentage = "$($p)% Utilized"
$Consumed = $o.consumedunits
$Purchased = $o.PrepaidUnits

Then, it will convert the units into integers (numbers)

foreach($o in $report){

IF($o.status -eq "enabled" -and $o.consumedunits -gt 0){
$cu = [int]$o.consumedUnits
$ppu = [int]$o.prepaidunits

$a = $ppu-$cu
$p = [math]::Round(($cu/$ppu)*100,0)
IF($p -ge 95){
$licensename = $o.license
$licensestatus = $o.status
$Percentage = "$($p)% Utilized"
$Consumed = $o.consumedunits
$Purchased = $o.PrepaidUnits

Then we do some math to get the amount of free licenses and the percentage used

foreach($o in $report){

IF($o.status -eq "enabled" -and $o.consumedunits -gt 0){
$cu = [int]$o.consumedUnits
$ppu = [int]$o.prepaidunits
$a = $ppu-$cu
$p = [math]::Round(($cu/$ppu)*100,0)

IF($p -ge 95){
$licensename = $o.license
$licensestatus = $o.status
$Percentage = "$($p)% Utilized"
$Consumed = $o.consumedunits
$Purchased = $o.PrepaidUnits

Then our IF logic will only alert us if we are at or above 95% utilization on licenses.

foreach($o in $report){

IF($o.status -eq "enabled" -and $o.consumedunits -gt 0){
$cu = [int]$o.consumedUnits
$ppu = [int]$o.prepaidunits
$a = $ppu-$cu
$p = [math]::Round(($cu/$ppu)*100,0)
IF($p -ge 95){
$licensename = $o.license
$licensestatus = $o.status
$Percentage = "$($p)% Utilized"
$Consumed = $o.consumedunits
$Purchased = $o.PrepaidUnits

Next, we configure the JSON payload. In here, we tell the connector that we would like to use the message card type payload which Teams understands. There are more modern ones, but as of this writing, Teams isn’t using them.

Then, we have a potential action. This is using deep links for Teams which allows us to call up a chat, name the chat, add the people and pre-populate with a message as well.

It also allows us to create key-value pairs to define how our card looks.

$payload = @"

 {
            "@context": "https://schema.org/extensions",
            "@type": "MessageCard",
            "potentialAction": [
                {
                    "@type": "OpenUri",
                    "name": "Request more licenses",
                    "targets": [
                        {
                            "os": "default",
                            "uri": "https://teams.microsoft.com/l/chat/0/0?users=user1@contoso.com,user2@contoso.com&topicName=licenseChat&message=We%20need%20some%20more%20licenses."
                            
                        }
                    ]
                }
            ],
            "sections": [
                {
                    "facts": [
                        {
                            "name": "License:",
                            "value": "$($licensename)"
                        },
                        {
                            "name": "License Status",
                            "value": "$($licenseStatus)"
                        },
                        {
                            "name": "License Utilization",
                            "value": "$($Percentage)"
                        },
                        {
                            "name": "Consumed Licenses",
                            "value": "$($Consumed)"
                        },
                        {
                            "name": "Purchased Licenses",
                            "value": "$($Purchased)"
                        }
                    ],
                    "text": "The following license is close to being exhausted. Please take action."
                }
            ],
            "Summary": "Summary",
            "themeColor": "f04e1f",
            "title": "Licensing Alert - $($licensename) - Less than $($a) licenses left"
        }

"@

Lastly, we run our invoke command on how to send the POST message to our incoming Webhooks

Invoke-RestMethod -Method post -ContentType 'Application/Json' -Body $payload -Uri "REPLACE_WITH_YOUR_URI_WEBHOOKURL"

}
}}

Configure your Teams Webhook

This content is taken from the great Prajwal Desai here

Add an incoming webhook to a Teams channel

Perform the below steps to add incoming webhook to Microsoft teams channel.

On the Teams app, click Apps and then click Connectors. Now search for Incoming Webhook connector. From the list, click Incoming Webhook connector.

Add Incoming Webhook to a Teams channel

On this page, you can read about Incoming Webhook before you add it to the team. Click Add to a team.

Add Incoming Webhook to a Teams channel

Select a Channel to start using the Incoming Webhook. Click Set up a connector.

Select a Channel
Select a Channel

You must specify a name to this Connector.

Setup Incoming Webhook

Optionally, upload an image avatar for your webhook and finally click Create.


The dialog window will present a unique URL that will map to the channel. Make sure that you copy and save the URL. You will need to provide it to the outside service. Select the Done button. The webhook will be available in the team channel.

Configure your Azure Runbook

Create a runbook in the Azure portal

  1. In the Azure portal, open your Automation account.
  2. From the hub, select Runbooks under Process Automation to open the list of runbooks.
  3. Click Create a runbook.
  4. Enter a name for the runbook and select its type (select Powershell). The runbook name must start with a letter and can contain letters, numbers, underscores, and dashes.
  5. Click Create to create the runbook and open the editor.
  6. Paste your powershell code into the window
  7. Click Save
  8. Click Publish when you are ready to go live with it

Schedule a runbook in the Azure portal

When your runbook has been published, you can schedule it for operation:

  1. Open the runbook in the Azure portal.
  2. Select Schedules under Resources.
  3. Select Add a schedule.
  4. In the Schedule Runbook pane, select Link a schedule to your runbook.
  5. Choose Create a new schedule in the Schedule pane.
  6. Enter a name, description, and other parameters in the New schedule pane.
  7. Once the schedule is created, highlight it and click OK. It should now be linked to your runbook.
  8. Look for an email in your mailbox to notify you of the runbook status.

That is it! Once you setup your schedule, you should now see your adaptive card populate the teams channel when the criteria is met.

Full code below (remember to substitute your variables and URLs)

$clientID = 'your-app-client-id'
$clientSecret = 'supersecretcode...'
$tenantID = 'your-tenant-id'

#Create a hashtable for the token request
$Body = @{
'tenant' = $tenantID
'client_id' = $clientID
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $clientSecret
'grant_type' = 'client_credentials'
}

#Assemble a hashtable for splatting parameters
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'Body' = $Body
'ContentType' = 'application/x-www-form-urlencoded'
}

$AuthResponse = Invoke-RestMethod @Params

$Headers = @{
'Authorization' = "Bearer $($AuthResponse.access_token)"
}

$Result = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/subscribedskus' -Headers $Headers

$obj = @()
$report = @()
$v = $Result.value
foreach($o in $v){
$obj = New-Object PSObject
$obj | Add-Member -Type NoteProperty -Name License -Value $o.skupartnumber
$obj | Add-Member -Type NoteProperty -Name Status -Value $o.CapabilityStatus
$obj | Add-Member -Type NoteProperty -Name ConsumedUnits -Value $o.ConsumedUnits
$obj | Add-Member -Type NoteProperty -Name PrepaidUnits -Value $o.PrepaidUnits.enabled
$report += $obj
}
foreach($o in $report){
IF($o.status -eq "enabled" -and $o.consumedunits -gt 0){
$cu = [int]$o.consumedUnits
$ppu = [int]$o.prepaidunits
$a = $ppu-$cu
$p = [math]::Round(($cu/$ppu)*100,0)
IF($p -ge 95){
$licensename = $o.license
$licensestatus = $o.status
$Percentage = "$($p)% Utilized"
$Consumed = $o.consumedunits
$Available = $o.PrepaidUnits
$payload = @"

 {
            "@context": "https://schema.org/extensions",
            "@type": "MessageCard",
            "potentialAction": [
                {
                    "@type": "OpenUri",
                    "name": "Request more licenses",
                    "targets": [
                        {
                            "os": "default",
                            "uri": "https://teams.microsoft.com/l/chat/0/0?users=user1@contoso.com,user2@contoso.com&topicName=LicenseChat&message=We%20need%20some%20more%20licenses."
                            
                        }
                    ]
                }
            ],
            "sections": [
                {
                    "facts": [
                        {
                            "name": "License:",
                            "value": "$($licensename)"
                        },
                        {
                            "name": "License Status",
                            "value": "$($licenseStatus)"
                        },
                        {
                            "name": "License Utilization",
                            "value": "$($Percentage)"
                        },
                        {
                            "name": "Consumed Licenses",
                            "value": "$($Consumed)"
                        },
                        {
                            "name": "Purchased Licenses",
                            "value": "$($Purchased)"
                        }
                    ],
                    "text": "The following license is close to being exhausted. Please take action."
                }
            ],
            "Summary": "Summary",
            "themeColor": "f04e1f",
            "title": "Licensing Alert - $($licensename) - Less than $($a) licenses left"
        }

"@

Invoke-RestMethod -Method post -ContentType 'Application/Json' -Body $payload -Uri "YOUR_WEBHOOK_URL"

}
}}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s