If you ever need to set the local Windows user account profile pictures from Azure AD, you can use the following script.
The script leverages the Graph API through a service principal (app) in Azure AD. There is some requirements before running the script:
- An Azure AD App with “read all users’ full profiles” Graph API permission. More information, see https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal
- You must be able to get the object id (client id), key (client secret) and token endpoint (OAuth 2.0 Token Endpoint) from the Azure AD app.
- The OS must be Windows 10.
- The client must be able to contact the Azure AD.
You can run the script “manually” or deploy it with Azure Intune. You can run the script under your own or with the “nt authority\system” account. Just be sure that the account have access to write to the following registry path “HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users” and it child objects.
The only thing you need to change in the script is three variables (line 43, 44 and 45) for the Azure AD app information.
Here is a basic walk through of what the script actually does:
- Create folder structure in “C:\Scripts\ProfilePicture” to store pictures, script and logs. The folder path can be changed to your liking on line 33, 34 and 39.
- Start transcript logs to “C:\Scripts\ProfilePicture\Logs\”.
- Get the access token for Graph API.
- Get user information (UPN, Username and SID) that have already logged in to the local device.
- Download user profile photo for each user in “C:\Scripts\ProfilePicture\Data\”.
- Sets registry keys to use the downloaded photo for each user.
- Create a task schedule (if it doesn’t exist) so it updates any picture change in Azure AD.
- Copy the script to location “C:\Scripts\ProfilePicture”.
You may need to compile the code into an executable, this will disguise the client secret used to retrieve the profile pictures. One way of turning a PowerShell script into an executable is to use this script, but remember to change the schedule task in the code to point to the .exe file instead of the .ps1 before compiling.
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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 |
#requires -version 3 <# .SYNOPSIS . .DESCRIPTION . .NOTES Version: 1.0 Author: Alex Ø. T. Hansen (ath@tofte-it.dk) Creation Date: 01-12-2018 Purpose/Change: Initial script development #> #region begin boostrap ############### Bootstrap - Start ############### #Clear the screen. Clear-Host; #Assemblies. Add-Type -AssemblyName System.Web; ############### Bootstrap - End ############### #endregion #region begin input ############### Input - Start ############### #Folders: $Folders = @{ Logs = ("C:\Scripts\ProfilePicture\Logs\"); Data = ("C:\Scripts\ProfilePicture\Data\"); }; #Files: $Files = @{ Script = ("C:\Scripts\ProfilePicture\Set-AzureADProfilePicture.ps1"); }; #Azure AD Application. $OauthTokenEndpoint = "https://login.microsoftonline.com/<GUID>/oauth2/token"; $ClientID = "<Client ID>"; $ClientSecret = "<Client Secret>"; ############### Input - End ############### #endregion #region begin functions ############### Functions - Start ############### #Write to the console. Function Write-Console { [cmdletbinding()] Param ( [Parameter(Mandatory=$false)][string]$Category, [Parameter(Mandatory=$false)][string]$Text ) #If the input is empty. If([string]::IsNullOrEmpty($Text)) { $Text = " "; } #If category is not present. If([string]::IsNullOrEmpty($Category)) { #Write to the console. Write-Output("[" + (Get-Date).ToString("dd/MM-yyyy HH:mm:ss") + "]: " + $Text + "."); } Else { #Write to the console. Write-Output("[" + (Get-Date).ToString("dd/MM-yyyy HH:mm:ss") + "][" + $Category + "]: " + $Text + "."); } } #Sets a user profile picture. Function Set-UserProfilePicture { [cmdletbinding()] Param ( [Parameter(Mandatory=$true)]$SID, [Parameter(Mandatory=$true)]$FilePath ) #Check if the profile picture exist. If(Test-Path -Path $FilePath) { #Create new image folder. $ImageBase = ($env:public + "\AccountPictures\" + $SID); $ImageBaseFolder = (New-Item -Path $ImageBase -ItemType Directory -Force) | Out-Null; #Create registry path. $RegistryPath = ("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users\" + $SID); New-Item -Path $RegistryPath -Force | Out-Null; #Array of image sizes. $ImageSizes = @(32, 40, 48, 96, 192, 200, 240, 448); #Foreach image size. Foreach($ImageSize in $ImageSizes) { #Set photo filename. $PhotoFileName = ("Image" + $ImageSize + ".jpg"); #Save the photo. Copy-Item -Path $FilePath -Destination ($ImageBase + "\" + $PhotoFileName) -Force; #Create new registry key. New-ItemProperty -Path $RegistryPath -Name ("Image" + $ImageSize) -Value ($ImageBase + "\" + $PhotoFileName) -Force | Out-Null; } } } #Get cache user information from the registry. Function Get-CacheUserInformation { #Array to store user information. $Users = @(); #Get all identities except system users. $Identities = Get-ChildItem -Path "HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache" | Where-Object {$_.Name -notlike "*S-1-5*"}; #Foreach identity found. Foreach($Identity in $Identities) { #Clear variables. $UPN = $null; $SID = $null; $SamAccountName = $null; $Domain = $null; #Set SID. $SID = $Identity.PSChildName; #Get key values. $KeyValues = Get-ItemProperty -Path ("HKLM:\SOFTWARE\Microsoft\IdentityStore\Cache\" + $SID + "\IdentityCache\" + $SID); #Set variables. $UPN = $KeyValues.UserName; $SamAccountName = $KeyValues.SAMName; $Domain = $KeyValues.ProviderName; #Create a new object. $User = New-Object -TypeName psobject; #Add data to the object. Add-Member -InputObject $User -MemberType NoteProperty -Name "UPN" -Value $UPN; Add-Member -InputObject $User -MemberType NoteProperty -Name "SID" -Value $SID; Add-Member -InputObject $User -MemberType NoteProperty -Name "SamAccountName" -Value $SamAccountName; Add-Member -InputObject $User -MemberType NoteProperty -Name "Domain" -Value $Domain; #Add object to array. $Users += $User; } #Return object array. Return $Users; } #Get access token from Graph API. Function Get-GraphAccessToken { [cmdletbinding()] Param ( [Parameter(Mandatory=$true)][string]$TokenEndpoint, [Parameter(Mandatory=$true)][string]$ClientID, [Parameter(Mandatory=$true)][string]$ClientSecret ) #Encode the client secret so it removes any special characters. $ClientSecretEncoded = [System.Web.HttpUtility]::UrlEncode($ClientSecret); #Construct request body to Graph API. $AuthBody = ("grant_type=client_credentials" + "&client_id=$ClientID" + "&client_secret=$ClientSecretEncoded" + "&resource=https://graph.microsoft.com/"); #Call the Graph API to get bearer token. $AuthReponse = Invoke-RestMethod -Method Post -Uri $OauthTokenEndpoint -body $AuthBody -ContentType "application/x-www-form-urlencoded"; #Return access token. Return ($AuthReponse.access_token); } #Get the user profile picture from Azure AD. Function Get-UserProfilePicture { [cmdletbinding()] Param ( [Parameter(Mandatory=$true)][string]$AccessToken, [Parameter(Mandatory=$true)][string]$UPN, [Parameter(Mandatory=$true)][string]$Destination ) #Invoke rest method to Graph API with access token. Invoke-RestMethod -Method Get -Uri ("https://graph.microsoft.com/v1.0/users/" + $UPN + '/photo/$value') -Headers @{"Authorization"="Bearer $($AccessToken)"} -OutFile $Destination; } #Create a schedule task. Function New-ProfilePictureScheduleTask { [cmdletbinding()] Param ( [Parameter(Mandatory=$true)][string]$Script ) #Template to create the schedule task. $XML = @" <?xml version="1.0" encoding="UTF-16"?> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2018-12-01T00:00:00.0000000</Date> <Author>blog.tofte-it.dk</Author> <URI>\Set Profile Account Pictures</URI> </RegistrationInfo> <Triggers> <CalendarTrigger> <StartBoundary>2018-12-01T10:00:00+01:00</StartBoundary> <ExecutionTimeLimit>P1D</ExecutionTimeLimit> <Enabled>true</Enabled> <ScheduleByDay> <DaysInterval>1</DaysInterval> </ScheduleByDay> </CalendarTrigger> </Triggers> <Principals> <Principal id="Author"> <UserId>S-1-5-18</UserId> <RunLevel>HighestAvailable</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries> <AllowHardTerminate>true</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>P1D</ExecutionTimeLimit> <Priority>7</Priority> <RestartOnFailure> <Interval>PT15M</Interval> <Count>3</Count> </RestartOnFailure> </Settings> <Actions Context="Author"> <Exec> <Command>C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe</Command> <Arguments>-ExecutionPolicy Bypass -File "$($Script)"</Arguments> </Exec> </Actions> </Task> "@; #Add schedule task. Register-ScheduledTask -Xml $XML -TaskName "Set Profile Account Pictures" | Out-Null; } #Check if the schedule task already exist. Function Test-ScheduleTask { [CmdletBinding()] Param ( [parameter(Mandatory=$true)][string]$Name ) #Create a new schedule object. $Schedule = New-Object -com Schedule.Service; #Connect to the store. $Schedule.Connect(); #Get schedule tak folders. $Task = $Schedule.GetFolder("\").GetTasks(0) | Where-Object {$_.Name -eq $Name -and $_.Enabled -eq $true}; #If the task exists and is enabled. If($Task) { #Return true. Return $true; } #If the task doesn't exist. Else { #Return false. Return $false; } } ############### Functions - End ############### #endregion #region begin main ############### Main - Start ############### #Create folders. New-Item -Path $Folders.Logs -ItemType Directory -Force | Out-Null; New-Item -Path $Folders.Data -ItemType Directory -Force | Out-Null; #Start transcript. Start-Transcript -Path ($Folders.Logs + "\" + (Get-Date).ToString("ddMMyyyy") + ".log") -Append -Force; #Get access token to access Graph API. Write-Console -Category "Access Token" -Text "Getting Graph API access token"; $AccessToken = Get-GraphAccessToken -TokenEndpoint $OauthTokenEndpoint -ClientID $ClientID -ClientSecret $ClientSecret; #Get all cached users. Write-Console -Category "Registry" -Text "Getting cached users from registry"; $Users = Get-CacheUserInformation; #Foreach user cached. Write-Console -Category "Users" -Text "Enumerating cached users"; Foreach($User in $Users) { #Write to log. Write-Output ""; Write-Console -Category $User.UPN -Text $User.UPN; Write-Console -Category $User.UPN -Text $User.SID; Write-Console -Category $User.UPN -Text $User.SamAccountName; Write-Console -Category $User.UPN -Text $User.Domain; #Download the profile picture. Write-Console -Category $User.UPN -Text ("Downloading profile picture to '" + ($Folders.Data + $User.UPN + ".jpeg") + "'"); Get-UserProfilePicture -AccessToken $AccessToken -UPN $User.UPN -Destination ($Folders.Data + $User.UPN + ".jpeg"); #Set the user profile picture. Write-Console -Category $User.UPN -Text ("Setting registry keys"); Set-UserProfilePicture -SID $User.SID -FilePath ($Folders.Data + $User.UPN + ".jpeg"); } #Output empty line. Write-Output ""; #Check if the schedule task doesn't exist. If(!(Test-ScheduleTask -Name "Set Profile Account Pictures")) { #Create the schedule task. Write-Console -Category "Schedule Task" -Text ("Creating schedule task"); New-ProfilePictureScheduleTask -Script $Files.Script; #Copy running script. Write-Console -Category "Schedule Task" -Text ("Copying running script to '" + $Files.Script + "'"); Copy-Item -Path $PSCommandPath -Destination $Files.Script; } ############### Main - End ############### #endregion #region begin finalize ############### Finalize - Start ############### #Stop transcript. Stop-Transcript; ############### Finalize - End ############### #endregion |
I can’t get it to work, it always returns the same error.
[Users]: Enumerating cached users.