Over the recent months, Paul Winstanley and I have been working on a series of blogs about using Microsoft Graph with your Windows 365 Cloud PC devices. We published part 3 at the end of 2024, with a couple more parts due soon to wrap up the series. Part 3 of the blog focused on provisioning your Windows 365 devices. The scripts we developed for the series are aimed at those getting started with MS Graph, with simple fire and forget commands, which got the job done.
We decided that we would create some more advanced scripts for admins, which would include some error handling, prompts for input, and more. You will be able to use these scripts to provision your Entra ID joined Cloud PCs for various scenarios, be it using a Microsoft Hosted network or an Azure Network Connection, Enterprise or Frontline builds, or choosing your Cloud PC image, language, or region etc.
The blog post will follow the format of part 3 of the previous blog, by stepping though the provisioning in the following manner:
- Install PowerShell 7
- Getting the Licence SKU
- Assigning the Licence to Users
- Adding licenced users to an Entra ID group
- Network choice for your Cloud PCs
- Create the Networking Components (optional)
- Find your Autopatch Group ID (optional)
- Create a Provisioning Policy
Install PowerShell 7
To avoid errors later on, and in order to get the full benefit of PowerShell, we’ll install PowerShell version 7. You can determine your PowerShell version using the following code, in Visual Studio Code.
$PSVersionTable
As you can see here, our PC is running an older version of PowerShell so it’s time to update it.
When installing the PowerShell extension in Visual Studio Code, you’ll be presented with an option to install the lastest release of version 7. Clicking the button will redirect to the Microsoft site to download the relevant install package. The actual URL link is here.
Once installed, restart Visual Studio Code and check the version again and it should reflect PowerShell version 7.4.6 as below.
WHAT RIGHTS ARE NEEDED???
Getting the Licence SKU
You need to assign a Windows 365 license to your users in order for them to use the service, much as you would with any Microsoft 365 product.
Using the following code, we can list all of those SKUs via Graph, this uses the following cmdlet Get-MgSubscribedSku.
The code is available from GitHub here.
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser -Force -AllowClobber Connect-MgGraph -Scopes "Organization.Read.All" Get-MgSubscribedSku | Select-Object SkuId, SkuPartNumber
What exactly is this code doing?
It starts by installing the Microsoft.Graph.Identity.DirectoryManagement module, a subset of the Microsoft Graph PowerShell SDK. This module provides cmdlets to interact with Microsoft Graph APIs specifically related to directory and identity management.
Note, if you get the following in Visual Studio Code when running the Install-Module command ‘Install-Package: No match was found for the specified search criteria and module name ‘Microsoft.Graph.Identity.DirectoryManagement’. Try Get-PSRepository to see all available registered module repositories.’ then run the command Register-PSRepository -Default and then retry.
How do we know that we need to use this specific module? Well we can refer to the Microsoft documentation for the cmdlet Get-MgSubscribedSku to answer that question. We can see the module is referenced on that page, therefore we do not need to install all the Microsoft Graph modules on our device to be able to query using that cmdlet, but just that particular module.
Next, the command Connect-MgGraph authenticates the session to Microsoft Graph. The scope parameter Organization.Read.All permission grants the script access to read organisation-level data in Entra ID. The user will be prompted for authentication, if not already signed in, and may have to consent to be able to access, if this has not been performed previously in the tenant. Note, since we are only querying for data here, we only need Read permissions on this data and we are ensuring least privilege, where possible when using our scripts.
Finally, the script retrieves a list of subscribed SKUs (licences) from the organisation’s tenant in Entra ID and reports back:
- SkuId: The unique identifier for the SKU.
- SkuPartNumber: The friendly name or part number of the SKU (e.g., Microsoft_Intune_Suite).
Now that we know the SKU id of our available licenses, we can assign users to the Windows 365 Enterprise 4 vCPU, 16 GB, 128 GB license shown as d201f153-d3b2-4057-be2f-fe25c8983e6f CPC_E_4C_16GB_128GB in the PowerShell output.
Assigning the Licence to Users
OK, so let’s take the SKU and run some code to assign this to our users. The full code required to do this is located here.
This PowerShell script automates the process of assigning the specific licence to users in Entra ID using Microsoft Graph. It ensures that each user exists, checks if the license is already assigned, and assigns it if necessary.
The script starts by checking for the existence of the modules that we wish to use to be able to assign the licence to the users. The PowerShell cmdlets we are going to use in this script are Get-MgSubscribedSku and Get-MgUser. We know from previous that we need to install the Microsoft.Graph.Identity.DirectoryManagement module for the Get-MgSubscribedSku cmdlet, but for the Get-MgUser module we need to install Microsoft.Graph.Users. Refer to the documentation for the Get-MgUser module to confirm this, here.
As you can see from the code, we are checking if the modules are installed using the Get-Module command and if not then installing. Note, the parameters for the Install-Module command. We are using:
- –Scope CurrentUser: When set to CurrentUser, the module is installed only for the current user, in the user’s profile directory (e.g., ~\Documents\PowerShell\Modules). Using this parameter means we can install the module without the need for administrative rights on the device.
- -Force: Using this parameter overwrites an existing module with the same name, even if it’s already installed. This will bypass warnings or errors for conflicting versions.
- -AllowClobber: This parameter allows the installation of a module that may overwrite commands provided by another module. For example, both Microsoft Graph and AzureAD modules may have a cmdlet called Get-User. Using -AllowClobber ensures that the new module’s commands overwrite existing ones.
# Check if Microsoft.Graph.Users module is installed, install if not if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) { Install-Module Microsoft.Graph.Users -Scope CurrentUser -Force -AllowClobber } # Check if Microsoft.Graph.Identity.DirectoryManagement module is installed, install if not if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Identity.DirectoryManagement)) { Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser -Force -AllowClobber }
With the modules installed we then connect to Graph, this time we need to increase our permissions when connecting. Since we need to add users to licences we need to be able to write, therefore our scope is set to Group.ReadWrite.All.
Our SkuId is added to a variable and then we check if the SKU exists in our tenant (it should do since we retrieved this earlier). If not then we will write back that The specified SKU ID does not exist in the tenant and exit the script.
You’ll also see the Import-Module PowerShell commands being used in the code snippet below. Why does this differ from the Install-Modules we have used earlier? Well even after installing a module, you still need to load it into your current PowerShell session if you want to use its commands. Think of Install-Module as putting the tools in your shed, and Import-Module as taking them out when you need them.
Import-Module Microsoft.Graph.Users Import-Module Microsoft.Graph.Identity.DirectoryManagement Connect-MgGraph -Scopes "Group.ReadWrite.All" # The SKU ID for the license you want to assign $skuId = "d201f153-d3b2-4057-be2f-fe25c8983e6f" # Check if the SKU exists $skuList = Get-MgSubscribedSku | Where-Object { $_.SkuId -eq $skuId } if (-not $skuList) { Write-Host "The specified SKU ID '$skuId' does not exist in the tenant. Exiting." exit }
To add users to the licence, we are going to utilise a text file containing a list of all our users that we wish to add. We are using a variable $userIdsFilePath to state the location of the txt file, userID.txt. The file should contain the UPN for each user. We haven’t added any error checking for the existence of the file path, but as you start to build up your PowerShell knowledge, you could add some simple error checking here also.
# Path to the text file containing UPNs (one per line) $userIdsFilePath = "C:\temp\userId.txt" # Read UPNs from the text file $userIds = Get-Content -Path $userIdsFilePath
When this section of the script is executed, the $userIds variable will hopefully be populated with your users that you wish to assign the licence to.
Now we will loop through each user in the $userIds variable (foreach ($userId in $userIDs) and perform the following:
- Check if the user exists in the tenant, if not write to the screen that the user does not exist and therefore is skipping.
- If the user does exist, then retrieve their licence details, if the licence is already assigned then skip, otherwise add the user to the licence.
# Loop through each UPN and check if the user exists and if the license is already assigned foreach ($userId in $userIds) { # Check if the user exists $user = Get-MgUser -UserId $userId -ErrorAction SilentlyContinue if (-not $user) { Write-Host "User $userId does not exist. Skipping." continue } if ($user) { # Retrieve user license details $userLicenses = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$userId/licenseDetails" # Check if the SKU ID is already assigned $licenseAssigned = $userLicenses.value | Where-Object { $_.skuId -eq $skuId } if ($licenseAssigned) { Write-Host "License is already assigned to $userId. Skipping." } else { # Assign the license if not already assigned Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$userId/assignLicense" -Body (@{ addLicenses = @(@{ skuId = $skuId }) removeLicenses = @() } | ConvertTo-Json) > Null Write-Host "License assigned to $userId" } } }
You can read the rest of this blog post here.