Contents

Unused Microsoft 365 Licenses Report (PowerShell + Microsoft Graph)

PowerShell script to build report of Microsoft 365 licenses for users that haven’t signed into their account for a while.

 Intro

Use this script when you need to free up Microsoft 365 licenses. It will output a simple report of users with licenses attached that haven’t signed into their account for some period.

This script is meant to be run on a schedule and therefore requires creation of Entra ID app registration for connecting to Microsoft Graph.

 Azure/Entra ID App Registration

I’m not going to go in detail about it but you can review the Microsoft documentation if this is your first time.

The app will need the following Application permissions.

  • AuditLog.Read.All : Reading login activity
  • LicenseAssignment.Read.All : Reading tenant licenses
  • User.Read
  • User.Read.All : Reading user data

/posts/unusedmicrosoft365licensesreport/image1.png
API permissions for Entra ID app

Next, create a client secret under the app.

/posts/unusedmicrosoft365licensesreport/image2.png
Client secret for the app

Once the steps above are completed, you’ll need the following items from the app registration for the PowerShell script:

  • Tenant ID (Also referred to as Directory ID)
  • Client ID (Also referred to as Application ID)
  • Client secret (Which you created under “Certificates & Secrets” category of the app)

Continue to PowerShell script once the app registration steps above are completed.

 Script

The script is ready to run, you just need to modify the following 4 variables:

  • $InactiveDays: The number of days a user must be inactive before being flagged.
  • $tenantID: From the app registration above
  • $clientID: From the app registration above
  • $clientSecret: From the app registration above

Your output will something like this, feel free to modify it and do whatever you like with it!

/posts/unusedmicrosoft365licensesreport/image3.png
Script output

Reference for the license names: https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference

Script

<#
Using Microsoft Graph API, get users that havent signed in for specific amount of days. If they have M365 licenses attached, output report with their info attached.
#>

$InactiveDays = 90 # Filter to include only users that havent signed in in the last $InactiveDays

#App Registration details for connecting to Microsoft Graph
$tenantID = "[YOURTenantID]"
$clientID = "[YourAzureAppRegistrationAppID/ClientID]"
$clientSecret = "[YourClientSecretForTheAzureApp]"

$Body = @{
    Grant_Type    = "client_credentials"
    Scope         = "https://graph.microsoft.com/.default"
    Client_Id     = $clientID
    Client_Secret = $clientSecret
}

$Connection = Invoke-RestMethod `
    -Uri https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token `
    -Method POST `
    -Body $body
 
#Get the Access Token
$Token = $Connection.access_token

# Get organization licenses
$skuUrl = "https://graph.microsoft.com/v1.0/subscribedSkus"
$headers = @{ 
    Authorization      = "Bearer $Token"
    "ConsistencyLevel" = "eventual" 
}
$skuResponse = Invoke-RestMethod -Uri $skuUrl -Headers $headers -Method Get
$skuLookup = @{}
foreach ($sku in $skuResponse.value)
{
    $skuLookup[$sku.skuId] = $sku.skuPartNumber
}

# Get users that havent signed in for $inactiveDays along with their license info
$date = (get-date).AddDays(-$InactiveDays) | Get-Date -Format "o" # days to subtract for filter
$filter = "signInActivity/lastSuccessfulSignInDateTime le $date"
$encodedFilter = [uri]::EscapeDataString($filter)
$url = "https://graph.microsoft.com/v1.0/users?`$select=userprincipalname,displayName,signInActivity,assignedLicenses&`$filter=$encodedFilter"
$headers = @{ 
    Authorization      = "Bearer $Token"
    "ConsistencyLevel" = "eventual" 
}
$response = Invoke-RestMethod -Uri $url -Headers $headers -Method Get

# Include only users with userprincipalname
$users = $response.value | Where-Object { $_.PSObject.Properties.Name -contains "userPrincipalName" -and $_.userPrincipalName }

# Build report
$report = foreach ($user in $users)
{
    # if assignedLicenses exists
    if ($user.assignedLicenses)
    {
        $user.assignedLicenses | Select-Object `
        @{Name = 'DisplayName'; Expression = { $user.displayName } },
        @{Name = 'UserPrincipalName'; Expression = { $user.userPrincipalName } },
        @{Name = 'LicenseName'; Expression = { $skuLookup[$_.skuId] } },
        @{Name = 'lastSignInDateTime'; Expression = { $user.signInActivity.lastSignInDateTime } },
        @{Name = 'lastSuccessfulSignInDateTime'; Expression = { $user.signInActivity.lastSuccessfulSignInDateTime } }
    }
}

# Output
$report | Sort-Object DisplayName | Format-Table