How to Update User Photos in Azure AD with PowerShell

update user photos in azure ad

While I was working through learning more about the MS Graph PowerShell SDK, I realized I could update user photos with what we have learned so far. Updating user account photos is a tedious and annoying process if you ask me, and it would be great to automate it without needed a third-party tool. (Ok so some third party tools work really well, but then what would I have to write about?). This one should be a quick and easy one. I didn’t find anything obvious when I was searching the web, so I figured I’d just throw this out there (in case future me forgets how I did it). Lets examine what it takes to update user photos in Azure AD with Graph PowerShell.

DISCLAIMER

Please understand that the content herein is for informational purposes only. This existence and contents shall not create an obligation, liability or suggest a consultancy relationship. In further, such shall be considered as is without any express or implied warranties including, but not limited to express and implied warranties of merchantability, fitness for a particular purpose and non-infringement. There is no commitment about the content within the services that the specific functions of the services or its reliability, applicability or ability to meet your needs, whether unique or standard. Please be sure to test this process fully before deploying in ANY production capacity, and ensure you understand that you are doing so at your own risk.

Table of Contents

Prerequisites
App Registration API Permissions
Script to Add User Photos from a folder
Script to Remove User Photos
Conclusion

Prerequisites

So there are a few prerequisites to you being successful here. If this is your first time with MS Graph PowerShell SDK, please go check out my previous articles in this series.

Each article so far has built on the last one, so it helps to make sure you read the others. Otherwise, if you get stuck in a section, go back and review then to make sure what I have configured makes sense. You will also need to install the MS Graph PowerShell SDK, as well as PowerShell 7 (recommended for maximum compatibility).

Certificate for Application Access

The focus around this script will be around using application access rather than delegated access, so you will need a self-signed certificate (or one for a PKI) if you have one configured in your lab. The scripts will work with delegated access as well, but you will have to make sure your account has the right permissions, and you will have to modify the connection string to connect via delegate access.

Photos for Testing

Azure has specific requirements for photo size when you want to update user photos in Azure AD. The specifics are located here, but you can be sure of a few things right off:

  • Photo is square, with a size of 648 x 648 pixels or smaller
  • Photo size is no larger than 4MB per request (per user)

I recommend having a photo that is already the right size. I will also include a link to a fantastic script you can include as a function to include the resize process as part of the upload.

Additionally, I have all the photos named with the username (samAccountName) of each user being updated. This is the easiest way to link and look up each user.

IMPORTANT NOTE!

One thing I want to highlight here. This script works on the assumption that user photos in a directory are named with the username (or samAccountName) of the person for whom the photo was taken. Now, if you have a standard naming convention, that matches MOST but not ALL users, then be sure photos are named precisely. You will 100% overwrite an existing photo if the photo name matches the wrong user. Just keep that in mind as you go along, as you may accidentally cause extra work for yourself!

App Registration API Permissions

Microsoft discusses the specifics of the permissions we will need for each section here. If you are updating photos for contacts or groups, check out that article to see the specific information. However, this is what we will need for our script:

  • User.Read.All (Application) – Get user details
  • User.ReadWrite.All (Application) – Update user account photo
  • (Optional) Mail.Send (Application) – Used to send Email notifications that upload failed

Additionally, if you want to be able to remove user photos, we will need the Exchange Online App-Only access configured:

  • Exchange.ManageAsApp (Application) – Clear user account photo value

If you already went through our previous exercises to get Exchange App-Only access configured, you should already have what you need. The decision to reuse an existing App Registration is unique to each organization, but given that this is for user account photos, we are going to use the same app registration we already had configured.

Script to Add User Photos from a Folder

There are plenty of different ways to store and access photos. I plan to run this pulling photos from a local folder on my computer, but you can path it to a server or share as needed. Some modification can be made for accessing SharePoint or Blob storage as well, but that is not what I am doing here. Maybe later in a different post we can explore those options :).

Key Cmdlets

  • Set-MgUserPhotoContent – Used to update user photos for user accounts in Azure AD via Graph PowerShell SDK.
  • Remove-UserPhoto – Used to clear the photo value in Exchange Online for user account photos

Functions Included

  • Send-FailMail – This is included to allow for email alerts when a username cannot be found from a photo name.
function Send-FailMail {
    $to = "[email protected]"
    $from = "[email protected]"
    $bcc = $from
    $subject = "Employee Photo - $photo - Failed to Update"
    $type = "html"
    $template = "C:\temp\PhotoFailMail.html"
    $params = @{
	    Message = @{
		    Subject = $subject
		    Body = @{
			    ContentType = $type
			    Content = $template
		    }
		    ToRecipients = @(
			    @{
				    EmailAddress = @{
					    Address = $to
				    }
			    }
		    )
		    BccRecipients = @(
			    @{
				    EmailAddress = @{
					    Address = $bcc
				    }
			    }
		    )
	    }
	    SaveToSentItems = "true"
    }  

Send-MgUserMail -UserId $from -BodyParameter $params
}

The notification email I’m using looks like this, but can be modified or replaced to meet whatever notification needs you require.

update user photos failure messgae

Template is located here if you just want to use this exact message. Just save the code into a text file, and save it as an HTML. If you want to create your own custom message, here is my process for doing just that.

Easy HTML Email Template

  1. Type an email in whatever mail client you prefer. (Must ensure the mail format is HTML)
  2. Send it to yourself.
  3. When the email arrives, view the raw source code for the email.
    1. In Outlook, you can right-click the email and choose ‘View Source’
    2. In Gmail, you can ‘Show Original’ (though it might be a little trickier finding the right code)
  4. From here, copy the HTML from the email, paste it into your favorite code editor (which is probably VS Code, just saying…), and save it as an HTML file
  5. Open the file so you can see what it looks like. It should open in a browser, and look just like the email you sent.

That’s it! Super easy, and NO HTML coding required.

The Final Script

Finally, here is the full script. You can also find it (always updated) in my GitHub.
Graph-UploadUserPhotos.ps1 · SeeSmitty/Powershell (github.com)

Below I posted the version that is all my code. I also had a version where I included this fantastic Image Resize script I found HERE, but did not feel it was appropriate to include that in this post. However, if you are looking to resize your photos to the appropriate size as a part of the script, then check out that blog post.

Code

#Variables
$thumb = {"thumbprint"}
$client = {"clientID"}
$tenant = {"tenantID"}
$org = {"PrimaryDomain"}
$Certificate = Get-ChildItem Cert:\CurrentUser\My\$thumb

#connection Strings
Connect-ExchangeOnline -CertificateThumbPrint $thumb -AppID $client -Organization $org
Connect-Graph -TenantId $tenant -AppId $client -Certificate $Certificate

#region Email Template
function Send-FailMail {
    $to = "[email protected]"
    $from = "[email protected]"
    $bcc = $from
    $subject = "Employee Photo - $photo - Failed to Update"
    $type = "html"
    $template = "C:\temp\PhotoFailMail.html"
    $params = @{
	    Message = @{
		    Subject = $subject
		    Body = @{
			    ContentType = $type
			    Content = $template
		    }
		    ToRecipients = @(
			    @{
				    EmailAddress = @{
					    Address = $to
				    }
			    }
		    )
		    BccRecipients = @(
			    @{
				    EmailAddress = @{
					    Address = $bcc
				    }
			    }
		    )
	    }
	    SaveToSentItems = "true"
    }  

Send-MgUserMail -UserId $from -BodyParameter $params
}


#EndRegion Email Template

#establishes the file paths for initial and final photo locations
$path = "C:\Temp\PhotosToUpload"
$finalPath = "C:\Temp\PhotosUploaded"
$pathWrong = "C:\temp\PhotosWrong"


#gets all the photos that exist in the target directory
$photoName = (Get-ChildItem "$path*").Name

#Loops to resize, upload and move photos
foreach($photo in $photoName){
    try {
        #Declare variables
        $username = (Get-ChildItem "$path\$photo").basename
        $userId = get-mguser -ConsistencyLevel eventual -Filter "startsWith(Mail, '$username')" | Select-Object Id

        if ($null -ne $userId) {
            #update photo in Azure with new photo
            Set-MgUserPhotoContent -UserId $userId.Id -InFile "$Path\$username-resized.jpg"
            Write-Host "$username photo has been updated!"                       

            #move original photo to final resting place
            Move-Item -Path $path\$photo -Destination $finalPath -Force -Confirm:$false
        }else {
            #manage photos with misspelled usernames
            Move-Item -Path $path\$photo -Destination $pathWrong -Force -Confirm:$false
            Send-FailMail
        }
    }
    catch {
       Write-Host $Error
    }
}

Disconnect-Graph
Disconnect-ExchangeOnline -Confirm:$false

This is the upload script for updating user photos in Azure AD.

Script to Remove User Photos

Now because I ran into an issue where I needed to quickly remove and resync the photos I was uploading, I will also include this script to remove photos in bulk. The main reason I think it is important to include this, is because the MS Graph doesn’t include a DELETE method with the MS Graph API for user photos. So once you have uploaded the photo via MS Graph, you cannot use Graph to remove that photo. We MUST use Exchange PowerShell to remove a user photo.

I am also going to include two separate functions in this script. One function uses a list of names configured in a CSV file to remove photos from users. The other is built off the assumption that you need to remove the same photos you just uploaded in the script above. Both functions are commented out, and either can be removed. I just found that I have needed both since writing this, so It is worth including them both for you here.

Script

Graph-RemoveUserPhotos.ps1 · SeeSmitty/Powershell (github.com)

#Variables
$thumb = {"thumbprint"}
$client = {"clientID"}
$tenant = {"tenantID"}
$org = {"PrimaryDomain"}
$Certificate = Get-ChildItem Cert:\CurrentUser\My\$thumb

#connection Strings
Connect-ExchangeOnline -CertificateThumbPrint $thumb -AppID $client -Organization $org
Connect-Graph -TenantId $tenant -AppId $client -Certificate $Certificate

#Used to remove photos via the same import method used to upload photos
function Remove-PhotosDirectory {
    $finalPath = "C:\temp\PicturesFinal"
    $names = (Get-ChildItem -Path $finalPath).BaseName
    foreach($n in $names){
        $username = get-mguser -ConsistencyLevel eventual -Filter "startsWith(Mail, '$n')"
        Remove-UserPhoto -Identity $username.UserPrincipalName  -ClearMailboxPhotoRecord -Confirm:$false
        Write-Host "Removed photo for" $username.DisplayName
    }
    
}


#Used to Remove photos in bulk from a CSV List - CSV needs a header of "username" to be successful
function Remove-PhotosCSV {
    $users = Import-csv "C:\temp\removephotos.csv"  
    foreach($u in $users){
        $u2 = $u.username
        $username = get-mguser -ConsistencyLevel eventual -Filter "startsWith(Mail, '$u2')"
        Remove-UserPhoto -Identity $username.UserPrincipalName  -ClearMailboxPhotoRecord -Confirm:$false
        Write-Host "Removed photo for" $username.DisplayName
    }
}

#Uncomment the version you want to use
#Remove-PhotosCSV
#Remove-PhotosDirectory


Disconnect-Graph
Disconnect-ExchangeOnline -Confirm:$false

Maybe you might find that useful, maybe not. Either way, I know where I can find it if I need it in the future!

Conclusion

That’s all there is to it. I told you this would be short and sweet and I don’t think I missed the mark. This might be one of those things that IT Administrators might hate having to deal with, but it is a part of modern businesses. Hopefully this script makes it easier to work through the photos upload, and gives you an opportunity to possibly automate an otherwise manual process. Plus it expands on the stuff we have been doing with the PowerShell Graph SDK, so that is also a plus. I really enjoy scripting in PowerShell, so this is probably not the last of these simple PowerShell scripts that I can leverage in MS Graph.

So what do you think? Helpful script? Useful? Waste of time? I’d love to get some feedback. Let me know what you would change or add or remove.  As always, hit me up on Twitter @SeeSmittyIT to let me know what you thought of this post. Thanks for reading!

Smitty

Curtis Smith works in IT with a primary focus on Mobile Device Management, M365 Apps, and Azure AD. He has certifications from CompTIA and Microsoft, and writes as a hobby.

View all posts by Smitty →