Skip to content

Instantly share code, notes, and snippets.

@machv
Created March 28, 2022 08:02
Show Gist options
  • Save machv/dcab5de5957c2b32d39c33e329b15d65 to your computer and use it in GitHub Desktop.
Save machv/dcab5de5957c2b32d39c33e329b15d65 to your computer and use it in GitHub Desktop.
<# setup
Connect-AzureAD
$PasswordProfile = New-Object -TypeName Microsoft.Open.AzureAD.Model.PasswordProfile
$PasswordProfile.Password = "<SecretPassword>"
$PasswordProfile.ForceChangePasswordNextLogin = $false
$user = New-AzureADUser `
-DisplayName "Photo Syncer" `
-PasswordProfile $PasswordProfile `
-UserPrincipalName "<UserName>" `
-AccountEnabled $true `
-MailNickName "photo-syncer"
Connect-ExchangeOnline
Add-RoleGroupMember "Recipient Management" -Member "<UserName>"
$cert = New-SelfSignedCertificate -Subject "CN=PhotoSyncerCert" -CertStoreLocation "Cert:\CurrentUser\My" -KeyExportPolicy Exportable -KeySpec Signature -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath aad-photo-syncer.crt
#>
#region Prereq
$photo = [byte[]](Get-Content test.jpg -Encoding byte)
Set-ADUser test.test -Replace @{thumbnailPhoto=$photo}
Start-ADSyncSyncCycle -PolicyType Delta
$u = Get-ADUser test -Properties thumbnailPhoto
$u.thumbnailPhoto | Set-Content preview.jpg -Encoding byte
Invoke-Item preview.jpg
Connect-AzureAD
Get-AzureADUserThumbnailPhoto -ObjectId "[email protected]" -View $true
#endregion
#region Credentials
$ecpUser = "<UserName>"
$ecpPassword = "<SecretPassword>"
$Tenant = "<TenantName>"
$ClientId = "<ClientId>"
$ClientSecret = "<ClientSecret>"
$CertificateThumbprint = "<CertificateThumbprint>"
<#
(https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/add-in-permissions-in-sharepoint)
create app at
https://m365x906820-admin.sharepoint.com/_layouts/15/appregnew.aspx
and then approve at
https://m365x906820-admin.sharepoint.com/_layouts/15/appinv.aspx
with this metadata:
<AppPermissionRequests AllowAppOnlyPolicy="true">
<AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
<AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" />
</AppPermissionRequests>
#>
$spTenantHandle = "m365x906820"
$spClientId = "70e53759-612c-46e5-b086-c23046ec62b1"
$spClientSecret = "<SharePointClientSecret>"
#endregion
#region Functions
function Invoke-OnBehalfOfFlowcertificate {
# https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-on-behalf-of-flow
param(
[Parameter(Mandatory = $true)]
[string]$Tenant,
[Parameter(Mandatory = $true)]
[string]$ClientId,
[Parameter(Mandatory = $true)]
[string]$CertificateThumbprint,
[Parameter(Mandatory = $true)]
[string]$AccessToken,
[Parameter()]
[string]$Resource = "https://graph.microsoft.com"
)
$certificate = Get-ChildItem Cert:\CurrentUser\My\$CertificateThumbprint
$jwt = New-Jwt -Subject $ClientId -Issuer $ClientId -ValidforSeconds 180 -Certificate $certificate -Audience "https://login.microsoftonline.com/$($Tenant)/oauth2/token"
$payload = @{
grant_type = "urn:ietf:params:oauth:grant-type:jwt-bearer"
requested_token_use = "on_behalf_of"
scope = "openid"
assertion = $AccessToken
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
client_assertion = $jwt
resource = $Resource
client_id = $ClientId
}
$response = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/$Tenant/oauth2/token" -Body $payload
$response
}
function Get-ReadableSize($Size) {
$suffix = "B", "kB", "MB", "GB", "TB"
$index = 0
while ($Size -gt 1kb) {
$Size = $Size / 1kb
$index++
}
"{0:N1} {1}" -f $Size, $suffix[$index]
}
function ConvertFrom-Timestamp {
param(
[Parameter(Mandatory = $true)]
[int]$Timestamp
)
$utc = (Get-Date 01.01.1970) + ([System.TimeSpan]::fromseconds($Timestamp))
$datetime = [datetime]::SpecifyKind($utc, 'Utc').ToLocalTime()
$datetime
}
function New-Jwt {
param(
$Type = "JWT",
[Parameter(Mandatory = $True)]
[string]$Issuer = $null,
[int]$ValidforSeconds = 180,
[Parameter(Mandatory=$true)][System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
$Audience = $null,
$Subject = $null
)
$Algorithm = "RS256"
$exp = [int][double]::parse((Get-Date -Date $((Get-Date).addseconds($ValidforSeconds).ToUniversalTime()) -UFormat %s)) # Grab Unix Epoch Timestamp and add desired expiration.
[hashtable]$header = @{
alg = $Algorithm
typ = $type
x5t = [System.Convert]::ToBase64String($Certificate.GetCertHash())
}
[hashtable]$payload = @{
iss = $Issuer
exp = $exp
aud = $Audience
sub = $Subject
}
$headerjson = $header | ConvertTo-Json -Compress
$payloadjson = $payload | ConvertTo-Json -Compress
$headerjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($headerjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$payloadjsonbase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payloadjson)).Split('=')[0].Replace('+', '-').Replace('/', '_')
$jwt = $headerjsonbase64 + "." + $payloadjsonbase64
$toSign = [System.Text.Encoding]::UTF8.GetBytes($jwt)
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
if ($null -eq $rsa) { # Requiring the private key to be present; else cannot sign!
throw "There's no private key in the supplied certificate - cannot sign"
}
else {
try {
$signed = $rsa.SignData($toSign, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
$sig = [Convert]::ToBase64String($signed) -replace '\+','-' -replace '/','_' -replace '='
} catch { throw "Signing with SHA256 and Pkcs1 padding failed using private key $rsa" }
}
$jwt = $jwt + '.' + $sig
return $jwt
}
# https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
function Invoke-ClientCredentalsCertificateFlow {
param(
[Parameter(Mandatory = $true)]
[string]$Tenant,
[string]$ClientId,
[string]$CertificateThumbprint,
[string]$Resource = "https://graph.microsoft.com"
)
$certificate = Get-ChildItem Cert:\CurrentUser\My\$CertificateThumbprint
$jwt = New-Jwt -Subject $ClientId -Issuer $ClientId -ValidforSeconds 180 -Certificate $certificate -Audience "https://login.microsoftonline.com/$($Tenant)/oauth2/token"
$authUrl = "https://login.microsoftonline.com/{0}/oauth2/token" -f $Tenant
$parameters = @{
grant_type = "client_credentials"
resource = $Resource
client_id = $ClientId
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
client_assertion = $jwt
}
$response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters
$expires = ConvertFrom-Timestamp -Timestamp $response.expires_on
$result = [PSCustomObject]@{
Expires = $expires
AccessToken = $response.access_token
}
$result
}
function Invoke-ClientCredentialsFlow {
param(
[string]$Tenant,
[Parameter(ParameterSetName='ClientCredential')]
[pscredential]$Client,
[Parameter(ParameterSetName='ClientExplicit')]
[string]$ClientId,
[Parameter(ParameterSetName='ClientExplicit')]
[string]$ClientSecret,
[string]$Resource = "https://graph.microsoft.com",
[string]$AuthorizationEndpoint = "https://login.microsoftonline.com/{0}/oauth2/token"
)
$authUrl = $AuthorizationEndpoint -f $Tenant
$parameters = @{
grant_type = "client_credentials"
client_secret = $ClientSecret
resource = $Resource
client_id = $ClientId
}
$response = Invoke-RestMethod -Uri $authUrl -Method Post -Body $parameters
$expires = ConvertFrom-Timestamp -Timestamp $response.expires_on
$result = [PSCustomObject]@{
Expires = $expires
AccessToken = $response.access_token
}
$result
}
function Update-SpoUserProperty {
param(
$Endpoint,
$Headers,
$User,
$Property,
$Value
)
$payload = @{
accountName = ("i:0#.f|membership|" + $User)
propertyName = $Property
propertyValue = $Value
}
$url = "https://$($Endpoint)/_api/SP.UserProfiles.PeopleManager/SetSingleValueProfileProperty"
Invoke-WebRequest -Uri $url -Method Post -Headers $Headers -Body ($payload | ConvertTo-Json) -ContentType "application/json;odata=nometadata"
}
#endregion
#region Option 1: On-Prem AD -> ExO
$startTime = Get-Date
[securestring]$secStringPassword = ConvertTo-SecureString $ecpPassword -AsPlainText -Force
[pscredential]$credentials = New-Object System.Management.Automation.PSCredential ($ecpUser, $secStringPassword)
Connect-ExchangeOnline -Credential $credentials -ShowBanner:$false -ConnectionUri "https://outlook.office365.com/powershell-liveid/?proxyMethod=RPS"
$allMailboxes = Get-EXOMailbox -ResultSize Unlimited
$usersWithPhoto = Get-ADUser -Properties thumbnailPhoto -Filter "thumbnailPhoto -like '*'" # -and ms-ds-consistencyguid -like '*'"
$user = $usersWithPhoto | Select -First 1
foreach($user in $usersWithPhoto) {
$photo = $user.thumbnailPhoto
$mailbox = $allMailboxes | Where-Object UserPrincipalName -EQ $user.UserPrincipalName
if(-not $mailbox) {
Write-Host -ForegroundColor Yellow " - skipping $($user.UserPrincipalName) as no mailbox detected"
continue
}
Write-Host -ForegroundColor Gray " - trying to update a picture of $($user.UserPrincipalName) [$(Get-ReadableSize -Size $photo.Length)]"
Set-UserPhoto -Identity $user.UserPrincipalName -PictureData $photo -Confirm:$false
}
$endTime = Get-Date
$duration = $endTime - $startTime
Write-Host ("Total sync duration: {0}" -f $duration)
#endregion
#region Option 2: On-Prem -> Microsoft Graph API
$startTime = Get-Date
$token = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>"
$headers = @{
"Authorization" = "Bearer $($token.AccessToken)"
"Content-Type" = "image/jpeg"
}
$usersWithPhoto = Get-ADUser -Properties thumbnailPhoto,Mail -Filter "thumbnailPhoto -like '*'" # -and ms-ds-consistencyguid -like '*'"
$user = $usersWithPhoto | Select -First 1
foreach($user in $usersWithPhoto) {
$photo = $user.thumbnailPhoto
if(-not $user.Mail) {
Write-Host -ForegroundColor Yellow " - skipping $($user.UserPrincipalName) as no e-mail address is set on the user"
continue
}
Write-Host -ForegroundColor Gray " - trying to update a picture of $($user.UserPrincipalName) [$(Get-ReadableSize -Size $photo.Length)]"
Invoke-WebRequest -Method Patch -Uri "https://graph.microsoft.com/v1.0/users/$($user.UserPrincipalName)/photo/`$value" -Headers $headers -Body $photo
}
$endTime = Get-Date
$duration = $endTime - $startTime
Write-Host ("Total sync duration: {0}" -f $duration)
#endregion
#region Option 3: AAD Graph API -> Microsoft Graph API (which goes to ExO mailbox)
$startTime = Get-Date
# MS Graph
$msgToken = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>" -Resource "https://graph.microsoft.com"
$msgHeaders = @{
"Authorization" = "Bearer $($msgToken.AccessToken)"
"Content-Type" = "image/jpeg"
}
# AAD Graph API
$aadToken = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>" -Resource "https://graph.windows.net"
$aadHeaders = @{
"Authorization" = "Bearer $($aadToken.AccessToken)"
"Content-Type" = "image/jpeg"
}
$url = "https://graph.windows.net/myorganization/users/?`$filter=dirSyncEnabled eq true&`$top=500&api-version=1.6"
$r = Invoke-RestMethod -Method Get -Uri $url -Headers $aadHeaders
$users = $r.value
while($r.'odata.nextLink')
{
$nextLink = $r.'odata.nextLink'+'&api-version=1.6'
$r = Invoke-RestMethod -Uri "https://graph.windows.net/myorganization/$($nextLink)" -Headers $aadHeaders -Method Get
$users += $r.value
}
$user = $users | ? UserPrincipalNAme -eq "[email protected]"
foreach($user in $users) {
$user.userPrincipalName
$exch = $user.assignedPlans | Where-Object service -EQ "exchange"
if(-not $exch) {
Write-Host -ForegroundColor Yellow " - user without Exchange license -> skipping"
continue
}
$photo = $null
$url = "https://graph.windows.net/myorganization/users/$($user.userPrincipalName)/thumbnailPhoto?api-version=1.6"
try {
$r = Invoke-WebRequest -Method Get -Uri $url -Headers $aadHeaders
$photo = $r.Content
} catch {
if($_.Exception.Response.StatusCode.value__ -eq 404) {
Write-Host -ForegroundColor Yellow " - no photo available -> skipping"
}
else {
Write-Host -ForegroundColor Red "Error while loading pic: $($_.Exception)"
}
continue
}
if($photo) {
Write-Host " - updating a picture [$(Get-ReadableSize -Size $photo.Length)]... " -NoNewline
try {
$r = Invoke-WebRequest -Method Patch -Uri "https://graph.microsoft.com/v1.0/users/$($user.UserPrincipalName)/photo/`$value" -Headers $msgHeaders -Body $photo
if($r.StatusCode -eq 200) {
Write-Host "OK"
}
} catch {
Write-Host ""
Write-Host -ForegroundColor Red (" - server returned {0} -> probably user does not have an ExO mailbox?" -f $_.Exception.Response.StatusCode)
}
}
}
$endTime = Get-Date
$duration = $endTime - $startTime
Write-Host ("Total sync duration: {0}" -f $duration)
#endregion
#region Option 4: AAD Graph API -> SPO
$spPhotosDocumentLibrary = "User Photos"
$spPhotosFolder = "Profile Pictures"
# Connect to AAD Graph
$aadToken = Invoke-ClientCredentalsCertificateFlow -Tenant $Tenant -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -Resource "https://graph.windows.net"
$aadHeaders = @{
"Authorization" = "Bearer $($aadToken.AccessToken)"
"Content-Type" = "image/jpeg"
}
$url = "https://graph.windows.net/myorganization/tenantDetails?api-version=1.6"
$r = Invoke-RestMethod -Method Get -Uri $url -Headers $aadHeaders
$tenantId = ($r.value | Select -First 1).objectId
# Load all AAD users
$url = "https://graph.windows.net/myorganization/users/?`$filter=dirSyncEnabled eq true&`$top=500&api-version=1.6"
$r = Invoke-RestMethod -Method Get -Uri $url -Headers $aadHeaders
$users = $r.value
while($r.'odata.nextLink')
{
$nextLink = $r.'odata.nextLink'+'&api-version=1.6'
$r = Invoke-RestMethod -Uri "https://graph.windows.net/myorganization/$($nextLink)" -Headers $aadHeaders -Method Get
$users += $r.value
}
$AuthorizationEndpoint = "https://accounts.accesscontrol.windows.net/$($tenantId)/tokens/OAuth/2"
$spoAdminToken = Invoke-ClientCredentialsFlow -AuthorizationEndpoint $AuthorizationEndpoint -ClientId "$($spClientId)@$($tenantId)" -ClientSecret $spClientSecret -Resource "00000003-0000-0ff1-ce00-000000000000/$($spTenantHandle)-admin.sharepoint.com@$($tenantId)"
$spoAdminHeaders = @{
"Authorization" = "Bearer $($spoAdminToken.AccessToken)"
"Accept" = "application/json;odata=nometadata"
}
$spoMyToken = Invoke-ClientCredentialsFlow -AuthorizationEndpoint $AuthorizationEndpoint -ClientId "$($spClientId)@$($tenantId)" -ClientSecret $spClientSecret -Resource "00000003-0000-0ff1-ce00-000000000000/$($spTenantHandle)-my.sharepoint.com@$($tenantId)"
$spoMyHeaders = @{
"Authorization" = "Bearer $($spoMyToken.AccessToken)"
"Accept" = "application/json;odata=nometadata"
}
$spoPictures = @{
"_SThumb" = "48"
"_MThumb" = "72"
"_LThumb" = "200"
}
$user = $users | ? UserPrincipalNAme -eq "[email protected]"
foreach($user in $users) {
$user.userPrincipalName
$photo = $null
$url = "https://graph.windows.net/myorganization/users/$($user.userPrincipalName)/thumbnailPhoto?api-version=1.6"
try {
$r = Invoke-WebRequest -Method Get -Uri $url -Headers $aadHeaders
$photo = $r.Content
} catch {
if($_.Exception.Response.StatusCode.value__ -eq 404) {
Write-Host -ForegroundColor Yellow " - no photo available -> skipping"
}
else {
Write-Host -ForegroundColor Red "Error while loading pic: $($_.Exception)"
}
continue
}
# https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/working-with-folders-and-files-with-rest
#$url = "https://m365x906820-my.sharepoint.com/_api/web/GetFolderByServerRelativeUrl('/User%20Photos/Profile%20Pictures')/Files('lisa_test_litware_ml_LThumb.jpg')/`$value"
#$r = Invoke-WebRequest -Uri $url -Method Get -Headers $spoMyHeaders
# check if user is present in SPO
$url = "https://$($spTenantHandle)-admin.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetPropertiesFor(accountName=@v)?@v='i:0%23.f|membership|$($user.userPrincipalName)'"
$r = Invoke-RestMethod -Uri $url -Method Get -Headers $spoAdminHeaders -ContentType "application/json;odata=nometadata"
if(-not $r.AccountName) {
Write-Host -ForegroundColor Yellow " - user not present in SPO (probably newly created account?) -> skipping"
continue
}
<#
$exchState = ($r.UserProfileProperties | Where-Object Key -EQ "SPS-PictureExchangeSyncState").Value
if($exchState -eq 1) {
Write-Host -ForegroundColor Yellow " - already in sync with ExO -> skipping"
continue
}
#>
$userNamePrefix = $user.userPrincipalName.Replace("@", "_").Replace(".", "_")
#$size = @{Name = "_MThumb";Value = 200}
foreach($size in $spoPictures.GetEnumerator()) {
# setup path
$fileName = "$($userNamePrefix)$($size.Name).jpg"
Write-Host " - resizing $fileName"
# Covert image into different size of image
[System.IO.MemoryStream]$stream = [System.IO.MemoryStream]::new($photo)
$img = [System.Drawing.Image]::FromStream($stream)
[int32]$new_width = $size.Value
[int32]$new_height = $size.Value
$img2 = New-Object System.Drawing.Bitmap($new_width, $new_height)
$graph = [System.Drawing.Graphics]::FromImage($img2)
$graph.DrawImage($img, 0, 0, $new_width, $new_height)
#Covert image into memory stream
$stream = New-Object -TypeName System.IO.MemoryStream
$format = [System.Drawing.Imaging.ImageFormat]::Jpeg
$img2.Save($stream, $format)
$streamseek = $stream.Seek(0, [System.IO.SeekOrigin]::Begin)
$resizedPhoto = $stream.ToArray()
# https://docs.microsoft.com/en-us/sharepoint/dev/sp-add-ins/working-with-folders-and-files-with-rest
$url = "https://$($spTenantHandle)-my.sharepoint.com/_api/web/GetFolderByServerRelativeUrl('/$($spPhotosDocumentLibrary)/$($spPhotosFolder)')/Files/add(url='$($fileName)',overwrite=true)"
$r = Invoke-WebRequest -Uri $url -Headers $spoMyHeaders -Method Post -Body $resizedPhoto
}
# Update properties
Write-Host " - Updating SPO properties for user"
$pictureUrl = "https://$($spTenantHandle)-my.sharepoint.com:443/$($spPhotosDocumentLibrary)/$($spPhotosFolder)/$($userNamePrefix)_MThumb.jpg"
$x = Update-SpoUserProperty -Endpoint "$($spTenantHandle)-admin.sharepoint.com" -Headers $spoAdminHeaders -User $user.userPrincipalName -Property "PictureURL" -Value $pictureUrl
$x = Update-SpoUserProperty -Endpoint "$($spTenantHandle)-admin.sharepoint.com" -Headers $spoAdminHeaders -User $user.userPrincipalName -Property "SPS-PicturePlaceholderState" -Value "0"
}
<#
https://techcommunity.microsoft.com/t5/sharepoint/profile-photo-sync-spo/m-p/78585
https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/upload-user-profile-pictures-sample-app-for-sharepoint
- https://github.com/pnp/PnP/tree/master/Samples/Core.ProfilePictureUploader
$spoMyToken = Invoke-ClientCredentalsCertificateFlow -Tenant $Tenant -ClientId $ClientId -CertificateThumbprint $certThumbprint -Resource "https://m365x906820-my.sharepoint.com"
$spoMyHeaders = @{
"Authorization" = "Bearer $($spoMyToken.AccessToken)"
#"accept" = "application/json;odata=verbose"
}
$spoAdminToken = Invoke-ClientCredentalsCertificateFlow -Tenant $Tenant -ClientId $ClientId -CertificateThumbprint $certThumbprint -Resource "https://m365x906820-admin.sharepoint.com"
$spoAdminHeaders = @{
"Authorization" = "Bearer $($spoAdminToken.AccessToken)"
#"accept" = "application/json;odata=verbose"
}
# Get Refresh Token for the service account
#$tokenResponse = Invoke-CodeGrantFlow -RedirectUrl "https://localhost:15484/auth" -ClientId $ClientId -ClientSecret $ClientSecret -Tenant $Tenant -Resource $ClientId -AlwaysPrompt $true
#$spgTok = Invoke-OnBehalfOfFlowcertificate -Tenant $Tenant -ClientId $clientId -CertificateThumbprint $CertificateThumbprint -AccessToken $tokenResponse.access_token -Resource "https://$($spTenantHandle)-admin.sharepoint.com" | ConvertTo-AuthorizationHeaders
$url = "https://m365x906820-my.sharepoint.com/_api/web/GetFolderByServerRelativeUrl('/User%20Photos/Profile%20Pictures')/Files('lisa_test_litware_ml_LThumb.jpg')"
$url = "https://m365x906820-my.sharepoint.com/_api/web/GetFolderByServerRelativeUrl('/User%20Photos/Profile%20Pictures')/Files('lisa_test_litware_ml_LThumb.jpg')/`$value"
$r = Invoke-WebRequest -Uri $url -Method Get -Headers $spoMyHeaders
#https://m365x906820-my.sharepoint.com/_api/web/GetFolderByServerRelativeUrl('/User Photos/Profile Pictures')/Files('lisa_test_litware_ml_LThumb.jpg')/$value
# properties
# Install-Module -Name SharePointOnline.CSOM
Load-SPOnlineCSOMAssemblies
$uri = New-Object System.Uri -ArgumentList "https://m365x906820-admin.sharepoint.com/"
$context = New-Object Microsoft.SharePoint.Client.ClientContext($uri)
$context.AuthenticationMode = [Microsoft.SharePoint.Client.ClientAuthenticationMode]::Anonymous
$context.FormDigestHandlingEnabled = $false
$context.add_ExecutingWebRequest({
$EventArgs.WebRequestExecutor.RequestHeaders["Authorization"] = "Bearer " + $spoAdminToken.AccessToken;
})
$context.Load()
$url = "https://m365x906820-admin.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetUserProfilePropertyFor(accountName=@v,propertyName='PictureURL')?@v=%27i:0%23.f|membership|[email protected]%27"
$response = Invoke-WebRequest -Uri $url -Method Get -Headers $spoAdminHeaders # "https://m365x906820.sharepoint.com/_layouts/15/[email protected]&size=S" -Headers $headers
$PictureURL = $SiteURL + $DocLibName + "/" + $foldername + "/" + $username + "_MThumb" + $Extension
$payload = @{
accountName = ("i:0#.f|membership|" + "[email protected]")
propertyName = "PictureURL"
propertyValue = "https://m365x906820-my.sharepoint.com:443/User%20Photos/Profile%20Pictures/bart_test_litware_ml_MThumb.jpg"
}
$url = "https://m365x906820-admin.sharepoint.com/_api/SP.UserProfiles.PeopleManager/SetSingleValueProfileProperty"
Invoke-WebRequest -Uri $url -Method Post -Headers $spoAdminHeaders -Body ($payload | ConvertTo-Json) -ContentType "application/json;odata=nometadata"
###
# https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly#what-are-the-limitations-when-using-app-only
# User Profile CSOM write operations do not work with Azure AD application - read operations work. Both read and write operations work through SharePoint App-Only principal
###
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($WebUrl)
$ctx.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username, $password)
$ctx.add_ExecutingWebRequest({
param($Source, $EventArgs)
$request = $EventArgs.WebRequestExecutor.WebRequest
$request.UserAgent = "NONISV|CsomPs|TestDecorate/1.0"
})
$ctx.ExecuteQuery()
$peopleManager = New-Object Microsoft.SharePoint.Client.UserProfiles.PeopleManager($ctx)
$targetAccount = ("i:0#.f|membership|" + $targetAcc)
$peopleManager.SetSingleValueProfileProperty($targetAccount, $PropertyName, $Value)
UpdateUserProfile -targetAcc $Identity -PropertyName SPS-PictureExchangeSyncState -Value 0 -SPOAdminPortalUrl $SPOAdminPortalUrl -Creds $Creds
UpdateUserProfile -targetAcc $Identity -PropertyName SPS-PictureTimestamp -Value 63605901091 -SPOAdminPortalUrl $SPOAdminPortalUrl -Creds $Creds
Connect-PnPOnline -Url "https://m365x906820-admin.sharepoint.com/" -ClientId $ClientId -Thumbprint $CertificateThumbprint -Tenant $Tenant
Connect-PnPOnline -Url "https://m365x906820-admin.sharepoint.com/" -AppId $spClientId -AppSecret $spClientSecret
Set-PnPUserProfileProperty -Account [email protected] -PropertyName PictureURL -Value "https://m365x906820-my.sharepoint.com:443/User%20Photos/Profile%20Pictures/bart_test_litware_ml_MThumb.jpg"
Set-PnPUserProfileProperty -Account [email protected] -PropertyName "SPS-PictureExchangeSyncState" -Value "1"
https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly#what-are-the-limitations-when-using-app-only
#>
#endregion
#region Tests
$user = "[email protected]"
$filePattern = "pic_{0}_{{0}}.jpg" -f ($user -split "@")[0]
$show = $true
# AAD Graph API
$token = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>" -Resource "https://graph.windows.net"
$headers = @{
"Authorization" = "Bearer $($token.AccessToken)"
"Content-Type" = "image/jpeg"
}
$file = $filePattern -f "aadgraph"
$url = "https://graph.windows.net/myorganization/users/$($user)/thumbnailPhoto?api-version=1.6"
$r = Invoke-WebRequest -Method Get -Uri $url -Headers $headers
$r.Content | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
# Microsoft Graph API v1
$file = $filePattern -f "msgraph_v1"
$token = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>"
$headers = @{
"Authorization" = "Bearer $($token.AccessToken)"
"Content-Type" = "image/jpeg"
}
$r = Invoke-WebRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/users/$($user)/photo/`$value" -Headers $headers
$r.Content | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
# MS Graph beta
$file = $filePattern -f "msgraph_beta"
$r = Invoke-WebRequest -Method Get -Uri "https://graph.microsoft.com/beta/users/$($user)/photo/`$value" -Headers $headers
$r.Content | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
# Local AD
$file = $filePattern -f "ad"
$u = Get-ADUser (($user -split "@")[0]) -Properties thumbnailPhoto
$u.thumbnailPhoto | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
# EWS
$file = $filePattern -f "ews"
[securestring]$secStringPassword = ConvertTo-SecureString $ecpPassword -AsPlainText -Force
[pscredential]$credentials = New-Object System.Management.Automation.PSCredential ($ecpUser, $secStringPassword)
Connect-ExchangeOnline -Credential $credentials -ShowBanner:$false -ConnectionUri "https://outlook.office365.com/powershell-liveid/?proxyMethod=RPS"
$r = Get-UserPhoto -Identity $user
$r.PictureData | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
# SPO
$AuthorizationEndpoint = "https://accounts.accesscontrol.windows.net/$($tenantId)/tokens/OAuth/2"
$spoAdminToken = Invoke-ClientCredentialsFlow -AuthorizationEndpoint $AuthorizationEndpoint -ClientId "$($spClientId)@$($tenantId)" -ClientSecret $spClientSecret -Resource "00000003-0000-0ff1-ce00-000000000000/$($spTenantHandle).sharepoint.com@$($tenantId)"
$spoAdminHeaders = @{
"Authorization" = "Bearer $($spoAdminToken.AccessToken)"
# "Accept" = "application/json;odata=nometadata"
}
$url = "https://$($spTenantHandle).sharepoint.com/_layouts/15/UserPhoto.aspx?size=L&AccountName=$($user)"
$r = Invoke-WebRequest -Uri $url -Method Get -Headers $spoAdminHeaders
$file = $filePattern -f "spo"
$r.Content | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
$token = Invoke-ClientCredentalsCertificateFlow -Tenant $Tenant -ClientId $ClientId -CertificateThumbprint $certThumbprint -Resource "https://m365x906820.sharepoint.com"
#$token = Invoke-ClientCredentialsFlow -Tenant "<TenantName>" -ClientId "<ClientId>" -ClientSecret "<ClientSecret>" -Resource "https://m365x906820.sharepoint.com"
$headers = @{
"Authorization" = "Bearer $($token.AccessToken)"
#"accept" = "application/json;odata=verbose"
}
$url = "https://m365x906820.sharepoint.com/_api/SP.UserProfiles.PeopleManager/GetUserProfilePropertyFor(accountName=@v,propertyName='PictureURL')?@v=%27i:0%23.f|membership|[email protected]%27"
$response = Invoke-WebRequest -Uri $url -Method Get -Headers $headers # "https://m365x906820.sharepoint.com/_layouts/15/[email protected]&size=S" -Headers $headers
$url = "https://m365x906820.sharepoint.com/_layouts/15/UserPhoto.aspx?size=L&accountname=$($user)"
$r = Invoke-WebRequest -Uri $url -Method Get -Headers $headers
$file = $filePattern -f "spo"
$r.Content | Set-Content $file -Encoding byte
if($show) { Invoke-Item $file }
Get-FileHash pic_* | Sort-Object Hash | Select Hash, Path
#endregion
# Get All mailboxes with picture
Get-Mailbox -ResultSize Unlimited | where-object HasPicture -EQ $true
<#
Note The thumbnailPhoto attribute can store a user photo as large as 100 kilobytes (KB).
The thumbnailPhoto attribute is synced only one time between Azure AD and Exchange Online. Any later changes to the attribute from the on-premises environment are not synced to the Exchange Online mailbox.
Exchange Online accepts only a photo that's no larger than 10 KB from Azure AD.
SP:
https://docs.microsoft.com/en-us/sharepoint/sharepoint-admin-role
https://docs.microsoft.com/en-us/sharepoint/dev/general-development/work-with-user-profiles-in-sharepoint
https://docs.microsoft.com/en-us/sharepoint/dev/solution-guidance/security-apponly-azuread
set current user photo: https://docs.microsoft.com/en-us/previous-versions/office/developer/sharepoint-rest-reference/dn790354(v=office.15)?redirectedfrom=MSDN#setmyprofilepicture-method
The Picture Timestamp, Picture Placeholder State, and Picture Exchange Sync State profile properties for the user are set or updated to reflect the profile picture synchronization state.
http://www.techrobbers.com/2019/07/sharepoint-user-profile-fields-with.html
Picture PictureURL URL
Picture Timestamp SPS-PictureTimestamp string single value
Picture Placeholder State SPS-PicturePlaceholderState integer
Picture Exchange Sync State SPS-PictureExchangeSyncState integer
Get specific size of MSG photo:
Sizes: 48x48, 64x64, 96x96, 120x120, 240x240, 360x360, 432x432, 504x504, and 648x648
- https://graph.microsoft.com/v1.0/users/[email protected]/photos/64x64/$value
references:
- https://support.microsoft.com/en-gb/help/3062745/user-photos-aren-t-synced-from-the-on-premises-environment-to-exchange
- https://support.microsoft.com/en-us/office/information-about-profile-picture-synchronization-in-microsoft-365-20594d76-d054-4af4-a660-401133e3d48a?ui=en-us&rs=en-us&ad=us
- https://docs.microsoft.com/en-us/skypeforbusiness/deploy/integrate-with-exchange-server/high-resolution-photos
- https://outlook.office.com/owa/service.svc/s/[email protected]&UA=0&size=HR648x648
Multiple locations of photos in 365:
- AAD directly (synced with AD [thumbnailPhoto attribute] via AAD Connect
- Exchange Online mailbox (only for users with ExO mailbox) - synced once with AAD
- SharePoint Online - synced in 24h intevals from ExO mailbox and stored in 3 sizes in Document Library https://m365x906820-my.sharepoint.com/User%20Photos/Forms/Thumbnails.aspx?id=%2FUser%20Photos%2FProfile%20Pictures
Delve, Teams respects Ex0 policy and fetch avatars from there
Microsoft Graph API (both 1.0 and beta) reads ExO mailbox photo
Azure AD Graph API reads directly from AAD storage
Other APIs:
- Outlook rest api:
* https://docs.microsoft.com/en-us/previous-versions/office/office-365-api/api/version-2.0/photo-rest-operations
* https://docs.microsoft.com/en-us/previous-versions/office/office-365-api/api/beta/use-outlook-rest-api-beta#DefineOutlookRESTAPI
#>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment