← Back to articles Intune

Automate Windows Autopilot Enrollment from a USB Stick Using PowerShell and an Azure App Registration

Automate Windows Autopilot Enrollment from a USB Stick Using PowerShell and an Azure App Registration

If you've ever stood at an imaging bench manually uploading CSV files to Intune, logging into the portal for each device, and hoping the technician picked the right Group Tag — you already know the problem. It doesn't scale. The moment a pallet of 50 laptops arrives from your supplier, the process falls apart.

This article walks you through building a fully automated Autopilot enrollment tool that lives on a USB stick. It uses an Azure App Registration for silent, non-interactive authentication, presents a Group Tag selection menu to the technician, and uploads the hardware hash directly to Intune — all from within the Out-of-Box Experience, with no portal login and no CSV file. Any technician can run it in under two minutes.

🏗️ How It All Fits Together

Before writing a single line of configuration, it's worth mapping the moving parts so you can troubleshoot with confidence when something goes wrong at 11pm before a deployment wave.

  • Get-WindowsAutoPilotInfo — A community script by Michael Niehaus, published on PSGallery. It reads the device's hardware hash from WMI and, when called with the -Online flag, pushes it directly to the Microsoft Graph API instead of exporting a CSV.
  • Azure App Registration — Because there is no signed-in user during OOBE, you need an app identity using the client credentials flow to authenticate against Graph. This replaces every interactive login prompt.
  • Group Tags — In Autopilot, a Group Tag is a string label you assign to a device at enrollment time. Intune uses it to identify the device, and you can target it with a dynamic Azure AD group rule. That group then receives an Autopilot Deployment Profile, meaning the device lands in the correct deployment lane automatically. If your organisation does not use Group Tags, Autopilot will still enroll the device using the default Zero Touch Deployment (ZTDI) flow — the script handles this gracefully via the manual input option, or you can simply remove the tag selection entirely and omit the -GroupTag parameter from the call.
  • The USB Stick — Acts as a launcher only. Dependencies are pulled from PSGallery at runtime, keeping the stick lightweight and ensuring you always use the latest version of Get-WindowsAutoPilotInfo. Air-gapped environment considerations are covered below.

🔑 Step 1 — Create the Azure App Registration

This is the most critical prerequisite. A misconfigured App Registration will cause every authentication attempt to fail, often silently.

  1. In the Azure Portal, go to Azure Active Directory > App registrations > New registration.
  2. Name it something descriptive — Autopilot-Enrollment-Tool works well. Select Single tenant. No redirect URI is needed.
  3. After creation, navigate to API permissions > Add a permission > Microsoft Graph > Application permissions. Add: DeviceManagementServiceConfig.ReadWrite.All.
  4. Click Grant admin consent. Without this step the permission exists on paper but is not active.
  5. Go to Certificates & secrets > New client secret. Set an expiry aligned with your rotation policy — 12 or 24 months is common. Copy the secret value immediately; you cannot retrieve it after leaving the page.
  6. Record your Tenant ID, Application (client) ID, and Client Secret. These go into the configuration block at the top of the script.
Worth knowing: DeviceManagementServiceConfig.ReadWrite.All is a broad scope covering Autopilot configuration at a service level. There is currently no narrower Graph permission that permits Autopilot device import via app-only auth. If your security team raises this, document the justification in your change record and compensate with the controls described in the Security Considerations section below.

🔧 Step 2 — The Enrollment Script

Drop the following script onto your USB stick as Enroll-Autopilot.ps1. The only section you need to edit before deployment is the configuration block at the top. Everything else is ready for production use.

#Requires -RunAsAdministrator
<#
.SYNOPSIS
    OOBE Autopilot Enrollment Automator
    Author: Souhaiel MORHAG
    Focus:  Silent Import via Azure App Registration
    Usage:  Run from USB during Windows OOBE (Shift+F10 > PowerShell)
#>

# ==========================================
#      CONFIGURATION (EDIT THIS SECTION)
# ==========================================
$TenantId  = "YOUR-TENANT-ID"
$AppId     = "YOUR-APP-CLIENT-ID"
$AppSecret = "YOUR-CLIENT-SECRET"
# ==========================================

Set-ExecutionPolicy Bypass -Scope Process -Force
$ErrorActionPreference = "Stop"

function Show-Header {
    Clear-Host
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host "    AUTOPILOT ENROLLMENT TOOL"             -ForegroundColor Cyan
    Write-Host "    Author: Souhaiel MORHAG"               -ForegroundColor Cyan
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host ""
}

function Check-Internet {
    Write-Host " [?] Checking Internet Connection..." -NoNewline
    try {
        $request = [System.Net.WebRequest]::Create("https://www.google.com")
        $request.Timeout = 3000
        $response = $request.GetResponse()
        $response.Close()
        Write-Host " OK" -ForegroundColor Green
        return $true
    } catch {
        Write-Host " FAIL" -ForegroundColor Red
        return $false
    }
}

function Install-Dependencies {
    Write-Host " [?] Preparing Environment..." -ForegroundColor Yellow
    try {
        Set-ExecutionPolicy Bypass -Scope Process -Force

        if (-not (Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue)) {
            Write-Host "     - Installing NuGet Provider..."
            Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope CurrentUser | Out-Null
        }

        if (-not (Get-InstalledScript -Name Get-WindowsAutoPilotInfo -ErrorAction SilentlyContinue)) {
            Write-Host "     - Installing Get-WindowsAutoPilotInfo..."
            Install-Script -Name Get-WindowsAutoPilotInfo -Force -Scope CurrentUser | Out-Null
        }
        Write-Host " [OK] Environment Ready." -ForegroundColor Green
    } catch {
        Write-Host " [!] Failed to install dependencies." -ForegroundColor Red
        Write-Host "     Error: $($_.Exception.Message)" -ForegroundColor Red
        Read-Host "Press Enter to Exit..."
        exit
    }
}

# --- MAIN EXECUTION FLOW ---

Show-Header

# 1. Internet Check
if (-not (Check-Internet)) {
    Write-Host "`n [!] Internet required. Connect via Ethernet or Wi-Fi." -ForegroundColor Red
    Write-Host "     Tip: Press Shift+F10 in OOBE to open CMD, then start PowerShell."
    Read-Host "Press Enter to Exit..."
    exit
}

# 2. Group Tag Selection Menu
$selectedTag = ""

while ($true) {
    Show-Header
    Write-Host "Select the Group Tag for this device:" -ForegroundColor Yellow
    Write-Host ""
    Write-Host "  1. WIN-AAD-FRA"
    Write-Host "  2. WIN-AAD-CORP"
    Write-Host "  3. Manual Input (Custom)"
    Write-Host ""

    $choice = Read-Host "Enter selection (1-3)"

    switch ($choice) {
        "1" { $selectedTag = "WIN-AAD-FRA"; break }
        "2" { $selectedTag = "WIN-AAD-CORP"; break }
        "3" {
            $selectedTag = Read-Host "Type the custom Group Tag"
            if ([string]::IsNullOrWhiteSpace($selectedTag)) {
                Write-Host "Tag cannot be empty." -ForegroundColor Red
                Start-Sleep -Seconds 1
                continue
            }
            break
        }
        default {
            Write-Host "Invalid selection." -ForegroundColor Red
            Start-Sleep -Seconds 1
        }
    }
    if ($selectedTag) { break }
}

# 3. Confirm and Execute
Show-Header
Write-Host "Target Group Tag : " -NoNewline
Write-Host $selectedTag -ForegroundColor Green
Write-Host "Installing dependencies..."

Install-Dependencies

Write-Host "`n [>>>] STARTING UPLOAD TO INTUNE..." -ForegroundColor Cyan
Write-Host "       Please wait. This usually takes 30-60 seconds."

try {
    # Resolve installed script path (CurrentUser then AllUsers fallback)
    $ScriptPath = "$env:USERPROFILE\Documents\WindowsPowerShell\Scripts\Get-WindowsAutoPilotInfo.ps1"
    if (-not (Test-Path $ScriptPath)) {
        $ScriptPath = "$env:ProgramFiles\WindowsPowerShell\Scripts\Get-WindowsAutoPilotInfo.ps1"
    }

    & $ScriptPath `
        -Online `
        -TenantId  $TenantId `
        -AppId     $AppId `
        -AppSecret $AppSecret `
        -GroupTag  $selectedTag `
        -Assign `
        -InformationAction Continue

    Write-Host ""
    Write-Host "==========================================" -ForegroundColor Green
    Write-Host "   SUCCESS! DEVICE REGISTERED"             -ForegroundColor Green
    Write-Host "==========================================" -ForegroundColor Green
    Write-Host " 1. Hardware hash uploaded to Intune."
    Write-Host " 2. Group Tag '$selectedTag' assigned."
    Write-Host " 3. Intune is generating the Autopilot profile."
    Write-Host ""
    Write-Host " RECOMMENDATION: Wait 5 minutes before rebooting." -ForegroundColor Yellow

} catch {
    Write-Host ""
    Write-Host " [!] ERROR: Registration Failed" -ForegroundColor Red
    Write-Host "     $($_.Exception.Message)"    -ForegroundColor Gray
}

Write-Host ""
$reboot = Read-Host "Do you want to Reboot now? (Y/N)"
if ($reboot -eq "Y") {
    Restart-Computer -Force
}
Pro tip — the -Assign flag: This tells Get-WindowsAutoPilotInfo to poll until an Autopilot profile has been assigned to the device before the script returns. Without it, the script exits as soon as the hash is uploaded. If the technician reboots immediately, the device may not yet have a profile and will present a generic OOBE instead of your branded Autopilot experience. Always use -Assign in production.

💾 Step 3 — Prepare the USB Stick

USB preparation is deliberately minimal. The script pulls its dependencies at runtime, keeping the stick lightweight and ensuring you always run the latest version of Get-WindowsAutoPilotInfo.

  1. Format a USB stick as FAT32 or NTFS. Capacity is irrelevant — the script itself is kilobytes.
  2. Create a folder on the root: D:\AutopilotTool\
  3. Save the script as Enroll-Autopilot.ps1 in that folder.
  4. Optionally add a README.txt with the technician steps below for your helpdesk team.
Worth knowing — air-gapped environments: If PSGallery is blocked on your network, pre-bundle the dependencies. Download Get-WindowsAutoPilotInfo.ps1 and the NuGet provider binaries onto the USB, then modify Install-Dependencies to copy them from a path relative to $PSScriptRoot instead of pulling from the internet. This adds complexity but makes the tool fully offline-capable.

🚀 Step 4 — Running the Tool During OOBE

This is the workflow your technicians follow at the bench. It requires no Intune knowledge on their part.

  1. Power on the device and let it reach the first OOBE screen. The language/region picker is sufficient — you don't need to progress further.
  2. Press Shift + F10 to open a Command Prompt.
  3. Type powershell and press Enter.
  4. Plug in the USB stick and identify the drive letter (e.g. D:).
  5. Run the script:
D:\AutopilotTool\Enroll-Autopilot.ps1
  1. Select the correct Group Tag from the menu (or accept no tag for the default ZTDI flow).
  2. Wait for the SUCCESS! DEVICE REGISTERED banner.
  3. When prompted, choose Y to reboot. The device will restart directly into the branded Autopilot experience.
Pro tip — restricted OOBE context: On some OEM devices, Shift+F10 opens CMD in a restricted context. If PowerShell throws execution policy or access denied errors, launch it explicitly with the bypass flag — this overrides any system-level policy before the script even loads:powershell -ExecutionPolicy Bypass -File D:\AutopilotTool\Enroll-Autopilot.ps1
The script sets -Scope Process bypass internally, but passing it at launch handles restrictions that fire before the script body runs.

✅ Step 5 — Verify the Enrollment in Intune

Always confirm the device landed correctly before it leaves the bench.

  1. In the Microsoft Intune admin center, navigate to Devices > Enrollment > Windows > Windows Autopilot devices.
  2. Search by serial number. The device should appear within 1–2 minutes of a successful upload.
  3. Confirm the Group Tag column shows the expected value.
  4. Check the Profile status column. It should transition from Not assigned to Assigned once your dynamic group membership has been evaluated — typically 2–5 minutes.
Worth knowing — dynamic group evaluation: If Profile status stays at Not assigned for more than 10 minutes, check your dynamic group rule syntax. A common mistake is using device.enrollmentProfileName instead of device.devicePhysicalIds to match Group Tags. The correct rule syntax for a Group Tag match is:
(device.devicePhysicalIds -any _ -eq "[OrderID]:WIN-AAD-CORP")

🔐 Security Considerations — Don't Skip This

Embedding credentials in a script on a USB stick is a conscious trade-off. Acknowledge the risk and compensate with these controls.

  • Rotate the secret regularly. Set a calendar reminder. An expired secret discovered during a deployment is a painful problem. Align rotation with your organisation's credential lifecycle policy.
  • Scope the App Registration tightly. The app should have DeviceManagementServiceConfig.ReadWrite.All and nothing else. Review permissions quarterly.
  • Encrypt the USB with BitLocker To Go. If the stick is lost, the plain-text credential is not exposed. Store the recovery key in your password manager, not on a sticky note.
  • Disable the App Registration when not in use. If bulk enrollments happen quarterly, disable the app between campaigns and re-enable it the day before. The overhead is seconds; the risk reduction is significant.
  • Monitor sign-in logs. In Azure AD > App registrations > your app > Sign-in logs, every use of the app credential is recorded. Unexpected authentication events are an immediate red flag.
Pro tip — Azure Key Vault: If you're running this script from a managed workstation rather than raw OOBE, you can refactor it to retrieve $AppSecret from Key Vault at runtime using Get-AzKeyVaultSecret. This eliminates the static credential from the USB entirely. For raw OOBE scenarios without a network identity, the embedded secret approach remains the most practical option — just layer the mitigations above around it.

🛠️ Extending the Script

The script as written is production-ready for most environments. These extensions are worth considering as your deployment practice matures.

  • Centralised logging. Add a Write-Log function that appends serial number, timestamp, technician name, and Group Tag to a CSV on a network share or Azure Blob Storage. Invaluable for audits and tracking down missing devices.
  • Group Tag whitelist validation. Prevent typos in the custom input option by comparing $selectedTag against an approved list loaded from a JSON config file on the USB root. Fail fast with a clear error message rather than silently enrolling into the wrong profile.
  • Multi-tenant support. Add a tenant selection menu to support MSP scenarios where one USB stick serves multiple customer tenants. Store credentials as a hashtable keyed by tenant name and load the correct set based on the technician's selection.
  • Pre-populate device naming. Prompt the technician for a device name prefix and pass it as an additional parameter. Some environments combine this with Autopilot device naming templates defined in the deployment profile.

📋 Conclusion

What you've built is a meaningful quality-of-life improvement for your deployment workflow. Any technician with a USB stick can now register a device into the correct Autopilot deployment lane in under two minutes — no portal access, no CSV, no Intune knowledge required on their part.

The three things that must be right for this to work reliably: an App Registration with DeviceManagementServiceConfig.ReadWrite.All and admin consent granted, the -Assign flag ensuring profile propagation before the device reboots, and Group Tag values that map cleanly to your dynamic AAD group rules. Validate those three things with a single test device before you roll this out to a full deployment batch.

Key Takeaways

  • Use an Azure App Registration with client credentials for non-interactive Autopilot uploads — no user account, no MFA prompts, no expiring passwords to manage.
  • The -Assign flag in Get-WindowsAutoPilotInfo is critical — without it the device may reboot before a profile has been assigned, resulting in a generic OOBE.
  • Group Tags drive dynamic group membership which drives profile assignment. If you don't use Group Tags, the default ZTDI flow still works — just omit the -GroupTag parameter.
  • Validate your dynamic group rule syntax (device.devicePhysicalIds -any _ -eq "[OrderID]:YOUR-TAG") with a test device before production use.
  • Mitigate the embedded credential risk with BitLocker To Go on the USB, short secret rotation cycles, app disable-when-idle policy, and Azure AD sign-in log monitoring.
  • The script is straightforwardly extensible — centralised logging, multi-tenant support, and whitelist validation are natural next steps for a mature deployment operation.

🎓 Ready to go deeper?

Practice real MD-102 exam questions, get AI feedback on your weak areas, and fast-track your Intune certification.

Start Free Practice → Book a Session
Souhaiel Morhag
Souhaiel Morhag
Microsoft Endpoint & Modern Workplace Engineer

Souhaiel is a Microsoft Intune and endpoint management specialist with hands-on experience deploying and securing enterprise environments across Microsoft 365. He founded MSEndpoint.com to share practical, real-world guides for IT admins navigating Microsoft technologies — and built the MSEndpoint Academy at app.msendpoint.com/academy, a dedicated learning platform for professionals preparing for the MD-102 (Microsoft 365 Endpoint Administrator) certification. Through in-depth articles and AI-powered practice exams, Souhaiel helps IT teams move faster and certify with confidence.

Related Articles