Script repository

Copy user photo from Microsoft 365 to AD account

Updated on: Jan 18, 2026, Views: 3423

Microsoft 365, User accounts

The script gets a user photo from Microsoft 365 and sets it into the AD account of the user. To execute the script, create a business rule, custom command or scheduled task configured for the User object type.

# Get user ID in Microsoft 365.
$azureID = $Context.TargetObject.AzureID
if ($NULL -eq $azureID)
{
    $Context.LogMessage("The user does not have a Microsoft 365 account", "Warning")
    return
}

# Connect to Microsoft Graph PowerShell.
$accessToken = $Context.CloudServices.GetAzureAuthAccessToken()
Connect-MgGraph -AccessToken ($accessToken | ConvertTo-SecureString -AsPlainText -Force)

# Get temp file path.
$tempFodlerPath = [System.IO.path]::GetTempPath()
$tempFilePath = "$tempFodlerPath%sAMAccountName%.tmp"

# Download user photo.
try
{
    Get-MgUserPhotoContent -UserId $azureID -OutFile $tempFilePath -ErrorAction Stop
}
catch
{
    $Context.LogMessage("The user has no photo", "Warning")
    return
}

$photoBytes = [System.IO.File]::ReadAllBytes($tempFilePath)
if ($NULL -ne $photoBytes)
{
    # Update user photo in AD.
    $user = $Context.BindToObjectEx($Context.TargetObject.AdsPath, $True)
    $user.Put("thumbnailPhoto", $photoBytes)
    $user.SetInfo()
}

# Remove temp file.
Remove-Item $tempFilePath -Force -Confirm:$False

Comments 13

You must be signed in to comment.

  • A

    A

    I have this setup but is there a way to do a bulk import from 365 for all users

    • Support

      Support

      Hello,

      What exactly do you mean? Could you, please, describe the desired behavior in all the possible details with live examples?

      • Trent

        Trent

        We would like to do this for all users, is that possible?

        • Support

          Support

          Hello Trent,

          As it is mentioned in the script description, it can be executed in a scheduled task or custom command. As such, the easiest option is to create a one-time task executing the script and add All objects to the Activity Scope.

  • Kevin

    Kevin

    Hi,

    Microsoft annouched they will stop the get-userphoto cmdlet from Exchange Online. Will you be upgrading the script to the Microsoft Graph PowerShell ((Updated) ExchangePowerShell: retirement of tenant admin cmdlets to Get, Set, and Remove UserPhotos)?

    • Support

      Support

      Hello Kevin,

      The script uses the Get-MgUserPhotoContent cmdlet from the Microsoft.Graph PowerShell module to get the photo. Please, note that for the script to work, the module must be installed on the computer where the Adaxes service runs. If you have multiple Adaxes services sharing a common configuration, the module must be installed on each computer where the Adaxes service runs.

      • Kevin

        Kevin

        Awesome then we don't have to fix anything for the upcomming update of the powershell cmdlets for Exchange.

  • Andrew Baker

    Andrew Baker

    Hi,

    I've just tried to run this on my tenant and got the following error?

    Is there a particular version of the Graph modules required?

    ---------------------------
    Run Script - User McUser
    ---------------------------
    [UnknownError] : Stack trace: at Get-MgUserPhotoContent, C:\Program Files\WindowsPowerShell\Modules\Microsoft.Graph.Users\2.26.1\exports\ProxyCmdletDefinitions.ps1: line 12253
    at , : line 21
    ---------------------------

    • Support

      Support

      Hello Andrew,

      > I've just tried to run this on my tenant and got the following error?

      How exactly did you execute the script? Was it in Windows PowerShell?

      For troubleshooting purposes, please, send us (support@adaxes.com) a screenshot of the error you are facing.

      >Is there a particular version of the Graph modules required?

      It is only required to have a version of the module compatible with PowerShell 5. As of now, Adaxes does not support PowerShell 7.

  • Marcel

    Marcel

    Hi, we have changed this line in the script:

    $tempFilePath = "$tempFodlerPath\\photo.tmp" 
    

    to

    $tempFilePath = "$tempFodlerPath\\%sAMAccountName%.tmp"  
    

    This because we had some issues where the photo.tmp was not correct downloaded or deleted. And then the wrong photo (of a different user) was added to AD (and syned to the MS365 account).

    Please make this change to avoid this problem.

  • Support

    Support

    Hello Marcel,

    Thank you for the suggestion. We updated the script accordingly.

  • Ilia Goldenberg

    Ilia Goldenberg

    AD attribute "thumbnailPhoto" has hard limits and size recommendations. The maximum size limit of this attribute is 100KB, while recommended size is ~10KB. This attribute is also has direct impact on AD replication (total size of replicated data)...

    On the other hand, the photo downloaded by Get-MgUserPhotoContent might be larger.
    Current version of script do not performs size validation.

    I would suggest to update the script to something like this (tested only on single user):

    <#
    .SYNOPSIS
    Downloads a user's photo from Microsoft 365 (Entra ID/Azure AD) and updates their Active Directory thumbnailPhoto attribute, resizing and compressing the image to meet AD requirements.
    
    .REQUIREMENTS
    - Adaxes context (script must run within an Adaxes custom command or business rule)
    - Microsoft.Graph PowerShell module (for Get-MgUserPhotoContent and Connect-MgGraph)
    Install-Module Microsoft.Graph -Scope AllUsers
    - System.Drawing.Common .NET package (for image resizing/compression in PowerShell 7+)
    System.Drawing.Common is included with PowerShell 7+ on Windows.
    If you encounter errors, ensure your .NET runtime includes System.Drawing.Common.
    dotnet tool install --global System.Drawing.Common
    
    .NOTES
    - The script resizes the photo to 96x96 pixels and compresses it to target ~10KB (AD recommended), max 100KB (AD limit).
    - The script will not set the photo if the final image is still over 100KB after processing.
    - The script must run on Windows or on a platform where System.Drawing.Common is supported.
    #>
    
    # Get user ID in Microsoft 365
    $azureID = $Context.TargetObject.AzureID
    if ($NULL -eq $azureID) {
      $Context.LogMessage("The user does not have a Microsoft 365 account", "Warning")
      return
    }
    
    # Connect to Microsoft Graph PowerShell
    $accessToken = $Context.CloudServices.GetAzureAuthAccessToken()
    Connect-MgGraph -AccessToken ($accessToken | ConvertTo-SecureString -AsPlainText -Force)
    
    # Get temp file path
    $tempFodlerPath = [System.IO.path]::GetTempPath()
    $tempFilePath = "$tempFodlerPath\%sAMAccountName%.tmp"
    
    # Download user photo
    try {
      Get-MgUserPhotoContent -UserId $azureID -OutFile $tempFilePath -ErrorAction Stop
    }
    catch {
      $Context.LogMessage("The user has no photo", "Warning")
      return
    }
    
    $photoBytes = [System.IO.File]::ReadAllBytes($tempFilePath)
    
    # Check and resize image if needed (requires System.Drawing.Common in PowerShell 7+)
    try {
      Add-Type -AssemblyName System.Drawing
    
      $image = [System.Drawing.Image]::FromFile($tempFilePath)
    
      # Resize if not 96x96
      if ($image.Width -ne 96 -or $image.Height -ne 96) {
        $Context.LogMessage("Resizing photo to 96x96 pixels for AD compliance.", "Information")
        $resized = New-Object System.Drawing.Bitmap 96, 96
        $graphics = [System.Drawing.Graphics]::FromImage($resized)
        $graphics.DrawImage($image, 0, 0, 96, 96)
        $graphics.Dispose()
        $image.Dispose()
    
        # Save resized image as JPEG with quality to target ~10KB
        $jpegCodec = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq "image/jpeg" }
        $encoder = [System.Drawing.Imaging.Encoder]::Quality
        $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters 1
    
        # Try different quality levels to get close to 10KB
        $targetSize = 10240 # 10 KB
        $quality = 80
        $step = 10
        $minQuality = 30
        $finalBytes = $null
    
        do {
          $ms = New-Object System.IO.MemoryStream
          $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter $encoder, $quality
          $resized.Save($ms, $jpegCodec, $encoderParams)
          $bytes = $ms.ToArray()
          $ms.Dispose()
    
          if ($bytes.Length -le $targetSize -or $quality -le $minQuality) {
            $finalBytes = $bytes
            break
          }
          $quality -= $step
        } while ($quality -ge $minQuality)
    
        $resized.Dispose()
        $photoBytes = $finalBytes
      }
      else {
        $image.Dispose()
      }
    }
    catch {
      $Context.LogMessage("Could not resize or compress image. Error: " + $_.Exception.Message, "Warning")
    }
    
    # Final size check (AD limit is 100KB)
    if ($photoBytes.Length -gt 102400) {
      $Context.LogMessage("Photo is still larger than 100 KB after resizing/compression and will not be set in AD.", "Warning")
      Remove-Item $tempFilePath -Force -Confirm:$False
      return
    }
    
    # Update user photo in AD
    $user = $Context.BindToObjectEx($Context.TargetObject.AdsPath, $True)
    $user.Put("thumbnailPhoto", $photoBytes)
    $user.SetInfo()
    
    # Remove temp file
    Remove-Item $tempFilePath -Force -Confirm:$False
    
    • Support

      Support

      Hello Ilia,

      Thank you for the suggestion. We will consider updating the script to include resizing.

Got questions?

Support Questions & Answers

We use cookies to improve your experience.
By your continued use of this site you accept such use.
For more details please see our privacy policy and cookies policy.