Improve the process of generating an AAD bulk enrollment provisioning package

I’ve been working a lot with Windows Configuration Designer recently, and have been experiencing issues whereby the Bulk Token is expiring, or even worse, the WCD GUI gives an error and fails to retrieve the token at all.

This led me down the path of finding (better) ways to (re)create the bulk token for a provisioning package. As a result, I stumbled across Sir Michael Niehaus’ blog post, in which he explains how you can simplify the process of generating an AAD bulk enrollment provisioning package.

Seeing that Michaels script simply builds the required XML on the fly, I thought I could be clever and simply add in the XML code from customisations.xml which was generated by the WCD GUI.

In doing so, this returned errors shown below.

ICD.exe : ERROR: The following errors were detected in customizations:
ICD.exe : ERROR: Settings were not found in the store file.
ICD.exe : ERROR: Parse error (0x80070490): ‘Users’ is not a valid child node for /Accounts
ICD.exe : ERROR: C:\Users\jvincen2\OneDrive\Documents\Windows Imaging and Configuration Designer (WICD)\Common\ICDCommon_20231229-131705-570_60472.log

Reviewing the generated log file referenced in the error, hinted at the Store File being the issue, as you can see below… ICD is referencing a “StoreFile” at C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\ called Microsoft-Common-Provisioning.dat.

12/29/2023 1:17:59 PM Info StoreFile path: C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\Microsoft-Common-Provisioning.dat
12/29/2023 1:17:59 PM Info Loaded Knobs schema hive at C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\Microsoft-Common-Provisioning.dat
12/29/2023 1:17:59 PM Error WpxGetFileEdition (onecore\base\ntsetup\wpx\core\store.cpp:103) - 0x80070002:
12/29/2023 1:17:59 PM Error     No SKU information file C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\Microsoft-Common-Provisioning.sku.xml available for store file C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\Microsoft-Common-Provisioning.dat
12/29/2023 1:17:59 PM Info Loaded settings from Windows Unknown Unknown
12/29/2023 1:17:59 PM Error WpxConfig::FindSchema (onecore\base\ntsetup\wpx\core\config.cpp:162) - 0x80070490:
12/29/2023 1:17:59 PM Error     Couldn't find schema path for DevDetail under /
12/29/2023 1:17:59 PM Error WpxSetting::GetChild (onecore\base\ntsetup\wpx\core\setting.cpp:249) - 0x80070490:
12/29/2023 1:17:59 PM Error     Couldn't find schema for answer path /DevDetail
12/29/2023 1:17:59 PM Error WpxWpafReader::ReadSettingsTree (onecore\base\ntsetup\wpx\file\wpafreader.cpp:628) - 0x8007000d:
12/29/2023 1:17:59 PM Error     Unrecognized child DevDetail specified for tree setting /
12/29/2023 1:17:59 PM Error WpxWpafReader::ReadCommonSettings (onecore\base\ntsetup\wpx\file\wpafreader.cpp:197) - 0x8007000d:
12/29/2023 1:17:59 PM Error     Couldn't read common settings
12/29/2023 1:17:59 PM Error WpxWpafReader::Read (onecore\base\ntsetup\wpx\file\wpafreader.cpp:78) - 0x8007000d:
12/29/2023 1:17:59 PM Error     Failed to read common settings node
12/29/2023 1:17:59 PM Error WpxConfig::ReadAnswerFile (onecore\base\ntsetup\wpx\core\config.cpp:598) - 0x8007000d:
12/29/2023 1:17:59 PM Error     Failed to load answer file
12/29/2023 1:17:59 PM Error The following errors were detected in customizations:
Settings were not found in the store file.
Parse error (0x80070490): 'DevDetail' is not a valid child node for /

12/29/2023 1:17:59 PM Error Microsoft.Windows.ICD.Common.Exceptions.CustomizationsException: The following errors were detected in customizations:
Settings were not found in the store file.
Parse error (0x80070490): 'DevDetail' is not a valid child node for /

   at Microsoft.Windows.ICD.Provisioning.ImageCustomization.ImageCustomizations.ReadAndValidateWPXAnswerFile(Config wpxConfig, XDocument wpxDocument, Boolean ignoreWpxParseErrors)
   at Microsoft.Windows.ICD.Provisioning.ImageCustomization.ImageCustomizations.Load(XDocument customizationsXDoc, Boolean onlySettings, Boolean ignoreWpxParseErrors)
   at Microsoft.Windows.ICD.Provisioning.ImageCustomization.ImageCustomizations.CreateAndLoadAnswers(XDocument customizationsXDoc, IStoreConfig storeConfig, Boolean ignoreWpxParseErrors, Boolean isMobile)
   at Microsoft.Windows.ICD.Core.Common.Project.LoadCustomizations(XDocument customizationsXDoc)
   at Microsoft.Windows.ICD.Core.Common.Project.CreateFromStoreFile(IEnumerable`1 storeFilePaths, String location, XDocument customizationsXDoc, InvocationMode invocationMode)
   at Microsoft.Windows.ICD.Core.CLI.BuildPackageCmdHandler.Execute()

Being (fairly) familiar with Windows Configuration Designer and Answer Files and what not, I am aware of the different layers and levels of customisations. As such, I explored the directory in which the referenced StoreFile lived, and found there is a variety of Provisioning DAT files. (HoloLens, Desktop, Team, Mobile etc)… on the basis the Common provisioning file was smaller than the Desktop, and as an attempt at a quick fix, I simply renamed Microsoft-Desktop-Provisioning.dat to Microsoft-Common-Provisioning.dat, backing up the old file in the process, and attempted to generate the PPKG again using Michaels script.

This time it was a success. Suggesting that the Microsoft-Common-Provisioning.dat being called by default doesn’t include all the variables and references that a Desktop devices would require.

Further reading of the ICD help articles on the Microsoft site revealed that you can indeed call the specific Store file, so appending /StoreFile:”$kitsRoot\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\Microsoft-Desktop-Provisioning.dat” to the last line which generates the package, should work, but it seems like there’s a bug in ICD preventing more than two params being called.

As such, I created a hacky song and dance in the script that renames the existing Microsoft-Common-Provisioning file, creates a copy of the Desktop file in its place, creates the package, and then returns things to normal.

This is how the modified script looks;

[Be aware, this includes some personal commands that are relevant to my project. Treat the code shown below as an example..!]

# -----------------------------------------------------------------------------
# File: Generate-AAD-PPKG.ps1
# Author: Michael Niehaus
#
# Description:
# A sample script to generate a provisioning package that can be used to join
# one or more devices to an Azure AD tenant (AAD join). This uses the
# AADInternals module, available on the PowerShell Gallery, as well as the 
# ICD.EXE tool from the Windows 10/11 ADK.
#
# Provided as-is with no support. See https://oofhours.com for related 
# information.
# -----------------------------------------------------------------------------

[CmdletBinding()]
param(
    [Parameter(Mandatory=$False,ValueFromPipelineByPropertyName=$True,Position=0)] [String] $FileName = "BulkEnrollment-Expires-$((Get-Date).AddDays(179).ToString("yyyy-MM-dd-hh-mm-ss"))"
)

# Make sure NuGet is installed
$provider = Get-PackageProvider NuGet -ErrorAction Ignore
if (-not $provider) {
    Find-PackageProvider -Name NuGet -ForceBootstrap -IncludeDependencies
}

# Import the AADInternals module, installing if necessary
$module = Import-Module AADInternals -PassThru -ErrorAction Ignore
if (-not $module) {
    Install-Module AADInternals -Force
    Import-Module AADInternals -Force
}

# Prompt the user to enter a Prefix for the hostname.
Write-Host "Enter the desired prefix for the Computer Hostname, a 3 Character shortcode is the recommendation."
Write-Host "The Computer will be renamed PFX-84SAD32J, where PFX = the shortcode entered, followed by the Serial number."
$Prefix = Read-Host -Prompt 'Enter prefix now'

# Get the access token
$user = Get-AADIntAccessTokenForAADGraph -Resource urn:ms-drs:enterpriseregistration.windows.net -SaveToCache

# Create a new BPRT (bulk token/bulk PRT)
$bprt = New-AADIntBulkPRTToken -Expires ((Get-Date).AddDays(179))

# Generate the customizations xml file
$xml = @"
<?xml version="1.0" encoding="utf-8"?>
<WindowsCustomizations>
  <PackageConfig xmlns="urn:schemas-Microsoft-com:Windows-ICD-Package-Config.v1.0">
    <ID>{$((New-Guid).Guid)}</ID>
    <Name>$FileName</Name>
    <Version>1.0</Version>
    <OwnerType>ITAdmin</OwnerType>
    <Rank>0</Rank>
    <Notes></Notes>
  </PackageConfig>
  <Settings xmlns="urn:schemas-microsoft-com:windows-provisioning">
    <Customizations>
      <Common>
        <Accounts>
          <Azure>
            <Authority>https://login.microsoftonline.com/common</Authority>
            <BPRT>$bprt</BPRT>
          </Azure>
          <Users>
            <User UserName="LocAdm">
              <Password>SuperDuperPassword619</Password>
              <UserGroup>Administrators</UserGroup>
            </User>
          </Users>
        </Accounts>
        <DevDetail>
          <DNSComputerName>$Prefix-%SERIAL%</DNSComputerName>
        </DevDetail>
        <OOBE>
          <Desktop>
            <HideOobe>True</HideOobe>
          </Desktop>
        </OOBE>
        <Policies>
          <ApplicationManagement>
            <AllowAllTrustedApps>Yes</AllowAllTrustedApps>
          </ApplicationManagement>
          <WindowsLogon>
            <HideFastUserSwitching>No</HideFastUserSwitching>
          </WindowsLogon>
        </Policies>
        <ProvisioningCommands>
          <PrimaryContext>
            <Command>
              <CommandConfig Name="DontDisplayLastUserName">
                <CommandLine>cmd.exe /C reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" /v dontdisplaylastusername /t REG_DWORD /d 1 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="HideFastUserSwitching">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V HideFastUserSwitching /t REG_DWORD /d 1 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="LastLoggedOnDisplayName">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V LastLoggedOnDisplayName /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="LastLoggedOnSAMUser">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V LastLoggedOnSAMUser /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="LastLoggedOnUserSID">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V LastLoggedOnUserSID /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="LastLoggedOnUser">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V LastLoggedOnUser /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="SelectedUserSID">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" /V SelectedUserSID /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="DisablePassportForWork">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PassportForWork" /v Enabled /t REG_DWORD /d 0 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
              <CommandConfig Name="DisablePostLogonProvisioning">
                <CommandLine>cmd.exe /C REG add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\PassportForWork" /v DisablePostLogonProvisioning /t REG_DWORD /d 1 /f</CommandLine>
                <ContinueInstall>True</ContinueInstall>
                <RestartRequired>False</RestartRequired>
              </CommandConfig>
            </Command>
          </PrimaryContext>
        </ProvisioningCommands>        
      </Common>
    </Customizations>
  </Settings>
</WindowsCustomizations>
"@
$xml | Out-File "$Prefix-$FileName.xml" -Encoding UTF8 -Force

# Find the ADK and ICD.exe
if (Test-Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows Kits\Installed Roots") {
    $kitsRoot = Get-ItemPropertyValue -Path "HKLM:\Software\WOW6432Node\Microsoft\Windows Kits\Installed Roots" -Name KitsRoot10
} elseif (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots") {
    $kitsRoot = Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Windows Kits\Installed Roots" -Name KitsRoot10
} else {
    Write-Error "ADK is not installed."
    return
}
$icdPath = "$kitsRoot\Assessment and Deployment Kit\Imaging and Configuration Designer\x86"
$icdExe = "$kitsRoot\Assessment and Deployment Kit\Imaging and Configuration Designer\x86\ICD.exe"
if (-not (Test-Path $icdExe)) {
    Write-Error "ICD.exe not found."
    return
}

# ICD doesn't like more than two parameters (BUG?) so perform a song and dance to work around this. 
# Backup Existing Common Store File
ren $icdPath\Microsoft-Common-Provisioning.dat Microsoft-Common-Provisioning.bak

# Copy Desktop Store File to Common Store
cp $icdPath\Microsoft-Desktop-Provisioning.dat $icdPath\Microsoft-Common-Provisioning.dat

# Generate the PPKG
& "$icdExe" /Build-ProvisioningPackage /CustomizationXML:$Prefix-$FileName.xml /PackagePath:$Prefix-$FileName.ppkg

# Reset the Store Files back to normal
del $icdPath\Microsoft-Common-Provisioning.dat
ren $icdPath\Microsoft-Common-Provisioning.bak Microsoft-Common-Provisioning.dat 
James avatar

One response to “Improve the process of generating an AAD bulk enrollment provisioning package”

  1. Zeb Wainwright

    I owe you a beer! This has been on my to-do list for quite a while and I never had the time to dig into why it was giving me errors. Made the modifications you suggested (with some tweaks in my script) and it creates the PPKG file! Now I can re-create all six packages on the fly as needed.

Leave a Reply

Your email address will not be published. Required fields are marked *