Managing user profile photos within Microsoft Entra ID (formerly Azure AD) can be tedious if done manually, especially for large organizations. In this post, I will walk you through automating two key tasks using PowerShell and the Microsoft Graph API:
- Identifying users without profile photos.
- Bulk updating user profile photos.
By the end of this guide, you’ll be able to retrieve all users, identify those without profile photos, and upload photos for them—all through automated scripts.
Prerequisites
Before we dive into the scripts, you need to have:
- Microsoft Entra ID Tenant ID
- Client ID and Client Secret from an Azure AD App registration with permissions to manage users.
- PowerShell installed on your machine.
- Microsoft Graph API permissions for user read/write access (
User.ReadWrite.All
).
Script 1: Finding Users Without Profile Photos
This first script authenticates to Microsoft Graph, retrieves all users, and checks if each user has a profile photo. If no photo exists, the user’s information is logged both on the console and in a log file. Finally, it exports the list of users without photos to a CSV file.
The Script
$TenantId = "f91b2525--44e6-a17c"
$ClientId = "dc784ec7----a453ac06bedc"
$ClientSecret = "RC58Q~aK4"
$LogPath = "C:\Images\logs.log"
$CsvPath = "C:\Images\NoPhotoUsers.csv"
$Body = @{
'tenant' = $TenantId
'client_id' = $ClientId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $ClientSecret
'grant_type' = 'client_credentials'
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'Body' = $Body
'ContentType' = 'application/x-www-form-urlencoded'
}
$NoPhotoUsers = @()
try {
# Authenticate and get the token
$AuthResponse = Invoke-RestMethod @Params
$Headers = @{
'Authorization' = "Bearer $($AuthResponse.access_token)"
}
# Graph API endpoint for users
$UsersUri = "https://graph.microsoft.com/v1.0/users"
do {
# Get users in batches
$Users = Invoke-RestMethod -Uri $UsersUri -Headers $Headers -Method Get
foreach ($User in $Users.value) {
try {
# Check if the user has a photo
$PhotoResponse = Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$($User.id)/photo/\$value" -Headers $Headers -Method Get -ErrorAction Stop
Write-Host "Photo exists for user: $($User.displayName)"
} catch {
# Log if no photo exists
Write-Host "No photo for user: $($User.displayName)"
Add-Content -Path $LogPath -Value "[$(Get-Date)] No photo for user: $($User.displayName) ($($User.mail))`n"
$NoPhotoUsers += [PSCustomObject]@{
DisplayName = $User.displayName
Email = $User.mail
}
}
}
# Check for next page of users
$UsersUri = $Users.'@odata.nextLink'
} while ($UsersUri -ne $null)
# Save users without photos to a CSV
$NoPhotoUsers | Export-Csv -Path $CsvPath -NoTypeInformation
} catch {
Write-Host "Authentication failed: $_"
Add-Content -Path $LogPath -Value "[$(Get-Date)] Authentication failed: $_`n"
}
Script Breakdown
- Authentication: We authenticate to Microsoft Graph using OAuth 2.0 by sending the tenant ID, client ID, client secret, and scope. This returns an access token.
- Retrieve Users: We use the
/users
endpoint to fetch all users. We paginate through all the results using@odata.nextLink
. - Check Photos: For each user, we attempt to retrieve their profile photo. If no photo is found, we log the user details.
- Export Results: Users without photos are exported to a CSV file for further processing.
Script 2: Bulk Uploading User Profile Photos
Once we have identified users without profile photos, the next step is to upload their photos in bulk. This second script reads a CSV file containing email addresses and file paths to photos, and then uploads the images to the respective user profiles.
The Script
$TenantId = "f91b2525-a478--a17c-"
$ClientId = "dc784ec7-a453ac06bedc"
$ClientSecret = "~~aK4"
$CsvPath = "C:\Images\userpic.csv"
$LogPath = "C:\Images\logs.log"
$Body = @{
'tenant' = $TenantId
'client_id' = $ClientId
'scope' = 'https://graph.microsoft.com/.default'
'client_secret' = $ClientSecret
'grant_type' = 'client_credentials'
}
$Params = @{
'Uri' = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token"
'Method' = 'Post'
'Body' = $Body
'ContentType' = 'application/x-www-form-urlencoded'
}
try {
# Authenticate and get the token
$AuthResponse = Invoke-RestMethod @Params
$Headers = @{
'Authorization' = "Bearer $($AuthResponse.access_token)"
}
# Read the CSV file and update photos
Import-Csv -Path $CsvPath | ForEach-Object {
try {
Invoke-RestMethod -Method Put -Uri "https://graph.microsoft.com/v1.0/users/$($_.mail)/photo/`$value" -Headers $Headers -ContentType "image/jpeg" -InFile $_.path
Write-Host "Successfully updated photo for $($_.mail)"
Add-Content -Path $LogPath -Value "[$(Get-Date)] Successfully updated photo for $($_.mail)`n"
} catch {
Write-Host "Failed to update photo for $($_.mail): $_"
Add-Content -Path $LogPath -Value "[$(Get-Date)] Failed to update photo for $($_.mail): $_`n"
}
}
} catch {
Write-Host "Authentication failed: $_"
Add-Content -Path $LogPath -Value "[$(Get-Date)] Authentication failed: $_`n"
}
Script Breakdown
- CSV Input: The script reads a CSV file with two columns:
mail
(user email) andpath
(file path to the image). - Photo Upload: For each user, it uploads the specified image as their profile picture using a
PUT
request to the Graph API/photo/$value
endpoint. - Error Logging: If the upload fails for any user, the error is logged for review.
CSV File Format
The CSV file for bulk uploading photos should look like this:
[email protected],C:\Images\user1.jpg
[email protected],C:\Images\user2.jpg
Conclusion
By automating these tasks, you can efficiently manage user profile photos across your organization. This approach saves time, especially when dealing with hundreds or thousands of users. The scripts can be modified to handle more complex scenarios, such as resizing images or handling different file formats.
Potential Enhancements
- Automated Image Processing: Before uploading, images can be resized or compressed.
- Enhanced Logging: Consider logging to a centralized system like Azure Log Analytics for easier tracking.
- Error Handling: Adding retries or more granular error reporting can make the script more resilient.