Created
March 28, 2022 08:02
-
-
Save machv/dcab5de5957c2b32d39c33e329b15d65 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<# 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