Add browser auth for headless runs
This commit is contained in:
Binary file not shown.
Binary file not shown.
10
Core.psm1
10
Core.psm1
@@ -63,6 +63,16 @@ function Start-CoreApp
|
||||
|
||||
Invoke-ModuleFunction "Invoke-InitializeModule"
|
||||
|
||||
if($View)
|
||||
{
|
||||
$global:currentViewObject = $global:viewObjects | Where-Object { $_.ViewInfo.Id -eq $View } | Select-Object -First 1
|
||||
}
|
||||
|
||||
if(-not $global:currentViewObject)
|
||||
{
|
||||
$global:currentViewObject = $global:viewObjects | Select-Object -First 1
|
||||
}
|
||||
|
||||
if(-not $global:SilentBatchFile)
|
||||
{
|
||||
Write-Log "SilentBatchFile must be specified" 3
|
||||
|
||||
@@ -563,6 +563,14 @@ function Add-MSALPrereq
|
||||
}
|
||||
$RequiredAssemblies.Add('System.Security.dll')
|
||||
$RequiredAssemblies.Add('mscorlib.dll')
|
||||
if($PSVersionTable.PSVersion.Major -ge 7)
|
||||
{
|
||||
$netStandardAssembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq "netstandard" } | Select-Object -First 1
|
||||
if($netStandardAssembly -and $netStandardAssembly.Location)
|
||||
{
|
||||
$RequiredAssemblies.Add($netStandardAssembly.Location)
|
||||
}
|
||||
}
|
||||
if($PSVersionTable.PSVersion.Major -ge 7) {
|
||||
$RequiredAssemblies.Add('System.Security.Cryptography.ProtectedData.dll')
|
||||
}
|
||||
@@ -574,6 +582,7 @@ function Add-MSALPrereq
|
||||
}
|
||||
catch
|
||||
{
|
||||
$global:SkipTokenCacheHelperEx = $true
|
||||
Write-LogError "Failed to compile TokenCacheHelperEx. The access token will not be cached. Check write access to the CS folder and ASR policies" $_.Exception
|
||||
}
|
||||
}
|
||||
@@ -732,7 +741,10 @@ function Add-MSALPrereq_old
|
||||
$global:SkipTokenCacheHelperEx = $true
|
||||
Write-LogError "Failed to compile TokenCacheHelperEx. The access token will not be cached. Check write access to the CS folder and ASR policies" $_.Exception
|
||||
}
|
||||
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
|
||||
if(Test-IsWindowsPlatform)
|
||||
{
|
||||
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
|
||||
}
|
||||
}
|
||||
|
||||
function Connect-MSALClientApp
|
||||
@@ -931,7 +943,14 @@ function Get-MSALApp
|
||||
|
||||
[void]$appBuilder.WithAuthority($authority)
|
||||
|
||||
if($appInfo.RedirectUri) { [void]$appBuilder.WithRedirectUri($appInfo.RedirectUri) }
|
||||
if($appInfo.RedirectUri)
|
||||
{
|
||||
[void]$appBuilder.WithRedirectUri($appInfo.RedirectUri)
|
||||
}
|
||||
else
|
||||
{
|
||||
[void]$appBuilder.WithDefaultRedirectUri()
|
||||
}
|
||||
|
||||
[void] $appBuilder.WithClientName("CloudAPIPowerShellManagement")
|
||||
[void] $appBuilder.WithClientVersion($PSVersionTable.PSVersion)
|
||||
@@ -952,7 +971,7 @@ function Get-MSALApp
|
||||
|
||||
$msalApp = $appBuilder.Build()
|
||||
|
||||
if($global:SkipTokenCacheHelperEx -ne $true -and (Get-SettingValue "CacheMSALToken"))
|
||||
if($global:SkipTokenCacheHelperEx -ne $true -and ("TokenCacheHelperEx" -as [type]) -and (Get-SettingValue "CacheMSALToken"))
|
||||
{
|
||||
[TokenCacheHelperEx]::EnableSerialization($msalApp.UserTokenCache, (Join-Path (Get-CloudApiDataFolder) "msalcahce.bin3"))
|
||||
}
|
||||
@@ -961,6 +980,53 @@ function Get-MSALApp
|
||||
return $msalApp
|
||||
}
|
||||
|
||||
function Get-HeadlessPublicClientAppInfo
|
||||
{
|
||||
$appObj = $null
|
||||
|
||||
if($global:MSGraphGlobalApps)
|
||||
{
|
||||
$templateApp = $global:MSGraphGlobalApps | Where-Object ClientId -eq $global:AzureAppId | Select-Object -First 1
|
||||
if($templateApp)
|
||||
{
|
||||
$appObj = [PSCustomObject]@{
|
||||
Name = $templateApp.Name
|
||||
ClientId = $templateApp.ClientId
|
||||
RedirectUri = $templateApp.RedirectUri
|
||||
Authority = $templateApp.Authority
|
||||
TenantId = $global:TenantId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(-not $appObj)
|
||||
{
|
||||
$appObj = [PSCustomObject]@{
|
||||
Name = "Headless Browser Login"
|
||||
ClientId = $global:AzureAppId
|
||||
RedirectUri = $null
|
||||
Authority = $null
|
||||
TenantId = $global:TenantId
|
||||
}
|
||||
}
|
||||
elseif($appObj.RedirectUri -and $appObj.RedirectUri -notmatch '^http://localhost(:\d+)?/?$')
|
||||
{
|
||||
$appObj.RedirectUri = $null
|
||||
}
|
||||
|
||||
if($global:MSALRedirectUri)
|
||||
{
|
||||
$appObj.RedirectUri = $global:MSALRedirectUri
|
||||
}
|
||||
|
||||
if($global:TenantId)
|
||||
{
|
||||
$appObj.TenantId = $global:TenantId
|
||||
}
|
||||
|
||||
$appObj
|
||||
}
|
||||
|
||||
function Get-MSALAppAuthority
|
||||
{
|
||||
try
|
||||
@@ -1001,20 +1067,33 @@ function Connect-MSALUser
|
||||
|
||||
if($global:hideUI -eq $true)
|
||||
{
|
||||
if($global:AzureAppId -and $global:ClientSecret -and $global:TenantId)
|
||||
$headlessAuthMode = ?? $global:HeadlessAuthMode "AppOnly"
|
||||
|
||||
if($headlessAuthMode -eq "Browser")
|
||||
{
|
||||
if(-not $global:AzureAppId -or -not $global:TenantId)
|
||||
{
|
||||
Write-Log "Azure AppId and Tenant Id must be specified for browser auth" 3
|
||||
return
|
||||
}
|
||||
|
||||
$global:appObj = Get-HeadlessPublicClientAppInfo
|
||||
}
|
||||
elseif($global:AzureAppId -and $global:ClientSecret -and $global:TenantId)
|
||||
{
|
||||
Connect-MSALClientApp $global:AzureAppId $global:TenantId -secret $global:ClientSecret
|
||||
return
|
||||
}
|
||||
elseif($global:AzureAppId -and $global:ClientCert -and $global:TenantId)
|
||||
{
|
||||
Connect-MSALClientApp $global:AzureAppId $global:TenantId -certificate $global:ClientCert
|
||||
return
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Log "Azure AppId, Tenant Id and Sercret/Cert must be specified for batch jobs" 3
|
||||
Write-Log "Azure AppId, Tenant Id and Secret/Cert must be specified for AppOnly auth, or use browser auth" 3
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
# No login during first time the app is started
|
||||
@@ -1348,7 +1427,7 @@ function Connect-MSALUser
|
||||
|
||||
$app = $appBuilder.Build()
|
||||
|
||||
if((Get-SettingValue "CacheMSALToken"))
|
||||
if($global:SkipTokenCacheHelperEx -ne $true -and ("TokenCacheHelperEx" -as [type]) -and (Get-SettingValue "CacheMSALToken"))
|
||||
{
|
||||
[TokenCacheHelperEx]::EnableSerialization($app.UserTokenCache, (Join-Path (Get-CloudApiDataFolder) "msalcahce.bin3"))
|
||||
}
|
||||
|
||||
@@ -1313,6 +1313,7 @@ function Show-GraphExportForm
|
||||
function Invoke-InitSilentBatchJob
|
||||
{
|
||||
$global:MSALToken = $null
|
||||
$headlessAuthMode = ?? $global:HeadlessAuthMode "AppOnly"
|
||||
|
||||
if(-not $global:TenantId)
|
||||
{
|
||||
@@ -1320,12 +1321,15 @@ function Invoke-InitSilentBatchJob
|
||||
return
|
||||
}
|
||||
|
||||
if(-not $global:AzureAppId -or (-not $global:ClientSecret -and -not $global:ClientCert))
|
||||
if(-not $global:AzureAppId -or ($headlessAuthMode -eq "AppOnly" -and (-not $global:ClientSecret -and -not $global:ClientCert)))
|
||||
{
|
||||
# Get login info for silent job from settings
|
||||
$global:AzureAppId = Get-SettingValue "GraphAzureAppId" -TenantID $global:TenantId
|
||||
$global:ClientSecret = Get-SettingValue "GraphAzureAppSecret" -TenantID $global:TenantId
|
||||
$global:ClientCert = Get-SettingValue "GraphAzureAppCert" -TenantID $global:TenantId
|
||||
if($headlessAuthMode -eq "AppOnly")
|
||||
{
|
||||
$global:ClientSecret = Get-SettingValue "GraphAzureAppSecret" -TenantID $global:TenantId
|
||||
$global:ClientCert = Get-SettingValue "GraphAzureAppCert" -TenantID $global:TenantId
|
||||
}
|
||||
}
|
||||
|
||||
if(-not $global:AzureAppId)
|
||||
@@ -1334,7 +1338,7 @@ function Invoke-InitSilentBatchJob
|
||||
return
|
||||
}
|
||||
|
||||
if(-not $global:ClientSecret -and -not $global:ClientCert)
|
||||
if($headlessAuthMode -eq "AppOnly" -and -not $global:ClientSecret -and -not $global:ClientCert)
|
||||
{
|
||||
Write-Log "Secret or Certificate must be specified. Either specify Secret/Certificate in Settings or Command Line" 3
|
||||
return
|
||||
|
||||
@@ -37,13 +37,19 @@ function New-TemporaryBatchFile
|
||||
function Test-AuthParameters
|
||||
{
|
||||
param(
|
||||
[string]$AuthMode,
|
||||
[string]$Secret,
|
||||
[string]$Certificate
|
||||
)
|
||||
|
||||
if($AuthMode -eq "Browser")
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
if((-not $Secret) -and (-not $Certificate))
|
||||
{
|
||||
throw "Specify -Secret or -Certificate."
|
||||
throw "Specify -Secret or -Certificate for AppOnly auth, or use -AuthMode Browser."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +66,11 @@ function Invoke-IntuneHeadlessBatch
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[psobject]$BatchConfig,
|
||||
|
||||
@@ -68,7 +79,7 @@ function Invoke-IntuneHeadlessBatch
|
||||
[string]$BatchFile
|
||||
)
|
||||
|
||||
Test-AuthParameters -Secret $Secret -Certificate $Certificate
|
||||
Test-AuthParameters -AuthMode $AuthMode -Secret $Secret -Certificate $Certificate
|
||||
|
||||
$projectRoot = Get-IntuneManagementProjectRoot
|
||||
$runtimeModule = Join-Path $projectRoot "Runtime/IntuneManagement.Runtime.psd1"
|
||||
@@ -98,13 +109,19 @@ function Invoke-IntuneHeadlessBatch
|
||||
TenantId = $TenantId
|
||||
AppId = $AppId
|
||||
SilentBatchFile = $BatchFile
|
||||
AuthMode = $AuthMode
|
||||
}
|
||||
|
||||
if($Secret)
|
||||
if($RedirectUri)
|
||||
{
|
||||
$invokeParams.RedirectUri = $RedirectUri
|
||||
}
|
||||
|
||||
if($AuthMode -eq "AppOnly" -and $Secret)
|
||||
{
|
||||
$invokeParams.Secret = $Secret
|
||||
}
|
||||
else
|
||||
elseif($AuthMode -eq "AppOnly")
|
||||
{
|
||||
$invokeParams.Certificate = $Certificate
|
||||
}
|
||||
@@ -135,6 +152,11 @@ function Export-IntunePolicies
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExportPath,
|
||||
|
||||
@@ -167,6 +189,8 @@ function Export-IntunePolicies
|
||||
-AppId $AppId `
|
||||
-Secret $Secret `
|
||||
-Certificate $Certificate `
|
||||
-AuthMode $AuthMode `
|
||||
-RedirectUri $RedirectUri `
|
||||
-BatchConfig $batchConfig `
|
||||
-SettingsFile $SettingsFile `
|
||||
-BatchFile $BatchFile
|
||||
@@ -186,6 +210,11 @@ function Import-IntunePolicies
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ImportPath,
|
||||
|
||||
@@ -225,6 +254,8 @@ function Import-IntunePolicies
|
||||
-AppId $AppId `
|
||||
-Secret $Secret `
|
||||
-Certificate $Certificate `
|
||||
-AuthMode $AuthMode `
|
||||
-RedirectUri $RedirectUri `
|
||||
-BatchConfig $batchConfig `
|
||||
-SettingsFile $SettingsFile `
|
||||
-BatchFile $BatchFile
|
||||
@@ -248,6 +279,11 @@ function Invoke-IntunePolicyAction
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[string]$SettingsFile,
|
||||
|
||||
[string]$BatchFile,
|
||||
@@ -282,6 +318,8 @@ function Invoke-IntunePolicyAction
|
||||
-AppId $AppId `
|
||||
-Secret $Secret `
|
||||
-Certificate $Certificate `
|
||||
-AuthMode $AuthMode `
|
||||
-RedirectUri $RedirectUri `
|
||||
-ExportPath $ExportPath `
|
||||
-SettingsFile $SettingsFile `
|
||||
-BatchFile $BatchFile `
|
||||
@@ -298,6 +336,8 @@ function Invoke-IntunePolicyAction
|
||||
-AppId $AppId `
|
||||
-Secret $Secret `
|
||||
-Certificate $Certificate `
|
||||
-AuthMode $AuthMode `
|
||||
-RedirectUri $RedirectUri `
|
||||
-ImportPath $ImportPath `
|
||||
-SettingsFile $SettingsFile `
|
||||
-BatchFile $BatchFile `
|
||||
|
||||
@@ -25,3 +25,12 @@ Export-IntunePolicies `
|
||||
-Secret "<client-secret>" `
|
||||
-ExportPath "/tmp/intune-export"
|
||||
```
|
||||
|
||||
```powershell
|
||||
Export-IntunePolicies `
|
||||
-TenantId "<source-tenant-id>" `
|
||||
-AppId "<public-client-app-id>" `
|
||||
-AuthMode Browser `
|
||||
-RedirectUri "http://localhost" `
|
||||
-ExportPath "/tmp/intune-export"
|
||||
```
|
||||
|
||||
37
README.md
37
README.md
@@ -6,7 +6,7 @@ This repository is now CLI-first. The old WPF application surface has been remov
|
||||
|
||||
1. export policies from a source tenant
|
||||
2. store the exported JSON and migration table
|
||||
3. import into a target tenant with app-only authentication
|
||||
3. import into a target tenant with app-only or browser authentication
|
||||
|
||||
## Entry points
|
||||
|
||||
@@ -18,8 +18,8 @@ This repository is now CLI-first. The old WPF application surface has been remov
|
||||
## Runtime
|
||||
|
||||
* `pwsh` 7+
|
||||
* Microsoft Graph app registration with app-only access
|
||||
* Client secret or certificate
|
||||
* Microsoft Graph app registration
|
||||
* App-only auth with client secret or certificate, or browser auth with a public client redirect URI
|
||||
|
||||
## Default object types
|
||||
|
||||
@@ -45,6 +45,16 @@ pwsh ./Scripts/Export-Policies.ps1 `
|
||||
-IncludeAssignments
|
||||
```
|
||||
|
||||
## Export with browser auth
|
||||
|
||||
```powershell
|
||||
pwsh ./Scripts/Export-Policies.ps1 `
|
||||
-TenantId "<source-tenant-id>" `
|
||||
-AppId "<public-client-app-id>" `
|
||||
-AuthMode Browser `
|
||||
-ExportPath "/tmp/intune-export"
|
||||
```
|
||||
|
||||
## Import
|
||||
|
||||
```powershell
|
||||
@@ -59,6 +69,16 @@ pwsh ./Scripts/Import-Policies.ps1 `
|
||||
-ReplaceDependencyIds
|
||||
```
|
||||
|
||||
## Import with browser auth
|
||||
|
||||
```powershell
|
||||
pwsh ./Scripts/Import-Policies.ps1 `
|
||||
-TenantId "<target-tenant-id>" `
|
||||
-AppId "<public-client-app-id>" `
|
||||
-AuthMode Browser `
|
||||
-ImportPath "/tmp/intune-export/SourceTenantName"
|
||||
```
|
||||
|
||||
## Single entrypoint
|
||||
|
||||
```powershell
|
||||
@@ -80,8 +100,19 @@ pwsh ./Start-HeadlessIntune.ps1 `
|
||||
-ImportType alwaysImport
|
||||
```
|
||||
|
||||
```powershell
|
||||
pwsh ./Start-HeadlessIntune.ps1 `
|
||||
-Action Export `
|
||||
-TenantId "<source-tenant-id>" `
|
||||
-AppId "<public-client-app-id>" `
|
||||
-AuthMode Browser `
|
||||
-RedirectUri "http://localhost" `
|
||||
-ExportPath "/tmp/intune-export"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
* Export writes a migration table used during cross-tenant import.
|
||||
* Import can translate dependency IDs and recreate missing assignment groups.
|
||||
* This repo intentionally does not preserve the old Windows UI launch flow.
|
||||
* Browser auth uses the system browser and a loopback redirect. If your app registration does not allow loopback redirects, pass `-RedirectUri "http://localhost"` and configure the same redirect URI in Entra ID.
|
||||
|
||||
@@ -17,6 +17,9 @@ function Initialize-IntuneManagementRuntime
|
||||
[string]$AppId,
|
||||
[string]$Secret,
|
||||
[string]$Certificate,
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
[string]$RedirectUri,
|
||||
[string]$GraphEnvironment,
|
||||
[string]$GCCType
|
||||
)
|
||||
@@ -30,6 +33,8 @@ function Initialize-IntuneManagementRuntime
|
||||
$global:AzureAppId = $AppId
|
||||
$global:ClientSecret = $Secret
|
||||
$global:ClientCert = $Certificate
|
||||
$global:HeadlessAuthMode = $AuthMode
|
||||
$global:MSALRedirectUri = $RedirectUri
|
||||
$global:UseGraphEnvironment = $GraphEnvironment
|
||||
$global:UseGCCType = $GCCType
|
||||
$global:UseJSonSettings = ($JSonSettings -eq $true)
|
||||
@@ -68,6 +73,10 @@ function Initialize-IntuneManagementRuntime
|
||||
{
|
||||
Write-Host "Using Azure App Certificate"
|
||||
}
|
||||
elseif($global:HeadlessAuthMode -eq "Browser")
|
||||
{
|
||||
Write-Host "Using browser authentication"
|
||||
}
|
||||
else
|
||||
{
|
||||
Write-Warning "Azure App Secret or Certificate is missing. Use -Secret <Secret> or -Certificate <Certificate>."
|
||||
|
||||
@@ -14,6 +14,11 @@ param(
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ExportPath,
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ param(
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ImportPath,
|
||||
|
||||
|
||||
@@ -14,6 +14,11 @@ param(
|
||||
|
||||
[string]$Certificate,
|
||||
|
||||
[ValidateSet("AppOnly","Browser")]
|
||||
[string]$AuthMode = "AppOnly",
|
||||
|
||||
[string]$RedirectUri,
|
||||
|
||||
[string]$SettingsFile,
|
||||
|
||||
[string]$BatchFile,
|
||||
@@ -45,6 +50,7 @@ $invokeParams = @{
|
||||
Action = $Action
|
||||
TenantId = $TenantId
|
||||
AppId = $AppId
|
||||
AuthMode = $AuthMode
|
||||
SettingsFile = $SettingsFile
|
||||
BatchFile = $BatchFile
|
||||
NameFilter = $NameFilter
|
||||
@@ -71,4 +77,9 @@ elseif($Certificate)
|
||||
$invokeParams.Certificate = $Certificate
|
||||
}
|
||||
|
||||
if($RedirectUri)
|
||||
{
|
||||
$invokeParams.RedirectUri = $RedirectUri
|
||||
}
|
||||
|
||||
Invoke-IntunePolicyAction @invokeParams
|
||||
|
||||
Reference in New Issue
Block a user