I had written a post last year for a PowerShell script that would get all the M365 License information for all users in your tenant and output the data to a CSV file that could be saved as a report. Since then, the legacy modules for AzureAD and MSOnline are being deprecated and replaced with Microsoft Graph. In this blog post, I will show how to use the Microsoft Graph SDK to update this script to use Microsoft Graph to gather and report the same information. Original Post is HERE.
Disclaimer
Note, this script can be used in all tenant types, including Government Cloud and Commercial Cloud. As will all scripts, please test the script in a test environment before placing into production and running it. Although I have successfully tested this script, I cannot guarantee functionality. LDLNET does not claim any responsibility for the use or modification of this script.
Prerequisites
You will need to create a Enterprise Application in Azure for the Microsoft Graph SDK to connect to as an API endpoint and set the proper permissions. This script requires the User.Read.All and the DIrectory.Read.All permissions as a minimum so that the API can read the User and License Information when queried by the API
Setup the Application in Azure
Step 1: Register the Application in Azure
- Navigate to the Azure portal and sign in with your account.
- Go to Azure Active Directory > App registrations > New registration.
- Enter a name for the application (like MS Graph SDK PowerShell), select the supported account types, and provide the redirect URI if necessary.
- Click on Register to create the application.
Step 2: Configure Permissions
- Once the app is registered, go to API permissions > Add a permission.
- Choose Microsoft Graph > Application permissions and select the appropriate permissions for reading and writing user and license information.
- For reading and writing user data, add permissions like
User.ReadWrite.All
. - For managing licenses, add permissions like
Directory.ReadWrite.All
. - After adding the permissions, click on Grant admin consent to apply these permissions.
Step 3: Generate Client ID and Secret
- In the application’s overview page, copy the Application (client) ID; this will be your clientID.
- Go to Certificates & secrets > New client secret.
- Add a description and set an expiry for the secret key.
- Once created, copy the Value of the client secret; this will be your secret key. You will not be able to copy this key again after this, so save the key to use in the script later or else you will have to create a new key!
Script
The script is pretty self-explanatory. I have tried to provide as much information as possible with comments to help show what is happening in the script. Please review the notes section in the script for functionality and the example on how to run the script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | <# .SYNOPSIS Get an M365 License Report of all members of a Commercial or Government Tenant M365 License Distribution. It will tell you what licneses they have based on if they have a mailbox enabled on their account. The report will be in CSV Format and will output to a predetermined directory based on the Parameters set in the script. .NOTES Name: Get-M365GraphLicenseInfo Author: Lance Lingerfelt Version: 2.0 Modify Date: 2024, May 13 Parameter Values: $ReportsPath sets the path to a default directory and will create the directory if needed later in the script. Setting a value is NOT mandatory but script will setup a directory of C:\MatlockScripts\Reports $ClientID is the Application (client) ID of an App Registration in Azure AD - Required $ClientSecret is the client secret of the App Registration in Azure AD - Required - ***DO NOT USE QUOTES AROUND THIS VALUE*** $TenantID is the Tenant ID of the Azure AD Tenant - Required $EnvType is the parameter for the Cloud Environment you are connecting to. The available choices for the -Environment parameter are: Global - This is the default environment and connects to the worldwide instance of Microsoft Graph. USGov - This connects to the United States Government instance. USGovDoD - This connects to the United States Department of Defense instance. Germany - This connects to the Microsoft Cloud Germany instance. China - This connects to the 21Vianet-operated instance in China. -EnvType Input is Required .EXAMPLE Connect to a GCCH Tenant in the Script .\Get-M365GraphLicenseInfo.ps1 -ClientID "00000000-0000-0000-0000-000000000000" -ClientSecret xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -TenantID "00000000-0000-0000-0000-000000000000" -EnvType 'USGov' #> [CmdletBinding(SupportsShouldProcess = $true)] Param( [Parameter(Mandatory = $false)] [string] $ReportsPath = "C:\LDLNETScripts\Reports", [Parameter(Mandatory = $true)] [string] $ClientID, [Parameter(Mandatory = $true)] [string] $ClientSecret, [Parameter(Mandatory = $true)] [string] $TenantID, [Parameter(Mandatory = $true)] [ValidateSet("Global","USGov", "USGovDoD", "Germany", "China")] [string] $EnvType ) # ================================================ # DO NOT MODIFY BEGIN # ================================================ $ErrorActionPreference = 'Continue' $Date = Get-Date -Format "MM/dd/yyyy" # Set Logging Configuration $Log = [PSCustomObject]@{ Path = "C:\LDLNETScripts\Logs\Get-M365GraphLicenseInfo" Name = "$($Date).log" } # ================================================ # DO NOT MODIFY END # ================================================ # ================================================ # SCRIPT BEGIN # ================================================ # Create New Logger Instance if Enabled if ($PSCmdlet.ShouldProcess("Create New Logger Instance", $Log.Path)) { # Import Logger Module try { if ( -not (Get-Module -Name PoShLog -ListAvailable) ) { Install-Module -Name PoShLog -Scope CurrentUser -Force } else { Import-Module -Name PoShLog -Force } } catch { Write-Host -Object "Unable to import logger module. Error: $($_.Exception.Message)" exit 1 } # Import Microsoft Graph Module try { if ( -not (Get-Module -Name Microsoft.Graph.Users -ListAvailable) ) { Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force } else { Import-Module -Name Microsoft.Graph.Users -Force Import-Module -Name Microsoft.Graph.DirectoryObjects -Force } } catch { Write-Host -Object "Unable to import Microsoft Graph module. Error: $($_.Exception.Message)" exit 1 } } # Create New Logger Instance. Verbose logging level. Log to file and console. Start Logger. New-Logger | ` Set-MinimumLevel -Value Verbose | ` Add-SinkFile -Path "$($Log.Path)\$($Log.Name)" -OutputTemplate ` '{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}' -RollingInterval Day | ` Add-SinkConsole | ` Start-Logger # Log Start of Script Write-VerboseLog "Start of Script." Write-Host "Start of Script." # Convert the client secret to a SecureString $secureClientSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force # Create a new PSCredential object with the client secret $ClientSecretCredential = New-Object ` -TypeName System.Management.Automation.PSCredential ` -ArgumentList $ClientID, $secureClientSecret # Connect to the Microsoft Graph SDK endpoint Connect-MgGraph -Environment $EnvType -ClientSecretCredential $ClientSecretCredential -TenantId $TenantID # Array to hold custom user license objects $LicenseInfo = @() # Make the request to the Graph API to get all users Write-VerboseLog "Getting Users" Write-Host "Getting Users" #NPlease note that the -ConsistencyLevel eventual parameter is used with the Get-MgUser cmdlet to ensure eventual consistency of the data, which is a requirement for some operations in the GCCH cloud. if ($EnvType -ne 'Global') { $users = Get-MgUser -All -ConsistencyLevel eventual -Select UserPrincipalName,DisplayName } #Just get all users if not in GCCH else { $users = Get-MgUser -All -Select UserPrincipalName,DisplayName } # Loop through each user to get their M365 license information Write-VerboseLog "Getting License Info For Each User" Write-Host "Getting License Info For Each User" foreach ($user in $users) { Write-Host "Checking [$userPrincipalName]" -ForegroundColor Green $userPrincipalName = $user.UserPrincipalName # Get the user's license details Write-Host "Getting License Information for: [$userPrincipalName]" -ForegroundColor Green $userLicenseDetails = Get-MgUserLicenseDetail -UserId $user.UserPrincipalName # Get the user's license details foreach ($SKU in $userLicenseDetails) { Write-Host "Found License [$($SKU.SkuPartNumber)]" -ForegroundColor Cyan $licenseInfo += [PSCustomObject]@{ DisplayName = $user.DisplayName UPN = $userPrincipalName License = $SKU.SkuPartNumber Services = "" } foreach ($ServicePlan in $SKU) { foreach ($Service in $ServicePlan.ServicePlans) { Write-Host "Found Service [$($Service.ServicePlanName)]" -ForegroundColor White $licenseInfo += [PSCustomObject]@{ DisplayName = $user.DisplayName UPN = $userPrincipalName License = "" Services = $Service.ServicePlanName } } } } } # Create Report Path if not there if (!(Test-Path $ReportsPath)) { New-Item -Type Directory -Path $ReportsPath } # Export the Results Write-Host "Exporting Results to Path [$ReportsPath]" -ForegroundColor Green Write-VerboseLog "Exporting Results to Path [$ReportsPath]" $LicenseInfo | Export-Csv $ReportsPath\M365_User_Licenses_$(Get-Date -Format yyyyMMddThhmmss).csv -notypeinformation # Disconnect from the Microsoft Graph SDK endpoint Write-Host "Disconnecting from Microsoft Graph SDK Endpoint" Write-VerboseLog "Disconnecting from Microsoft Graph SDK Endpoint" Disconnect-MgGraph # Log End of Script Write-Host "End of Script" Write-VerboseLog "End of Script" # ================================================ # SCRIPT END #================================================= |
Output
The Output will be in a CSV format file that will have separate columns for the main licenses (Sku) and the add-ons (Service Plans). The format should allow you to sort on the license or add-ons and get a list of UPNs that have that enabled on their account. This can be very helpful when doing a migration or an audit of your tenant.
Conclusion
As we move away from the legacy tools, we will need to be able to use the Microsoft Graph SDK to do the same tasks as those old tools did. Good luck with your scripting! Be sure to continually check for updates!
About Lance Lingerfelt
Lance Lingerfelt is an M365 Specialist and Evangelist with over 20 years of experience in the Information Technology field. Having worked in enterprise environments to small businesses, he is able to adapt and provide the best IT Training and Consultation possible. With a focus on AI, the M365 Stack, and Healthcare, he continues to give back to the community with training, public speaking events, and this blog.