1919import com .google .common .base .Suppliers ;
2020import com .microsoft .graph .http .GraphServiceException ;
2121import com .microsoft .graph .models .Group ;
22+ import com .microsoft .graph .models .ProfilePhoto ;
2223import com .microsoft .graph .options .Option ;
2324import com .microsoft .graph .options .QueryOption ;
2425import com .microsoft .graph .requests .GraphServiceClient ;
2526import com .microsoft .graph .requests .GroupCollectionPage ;
27+ import com .microsoft .graph .requests .ProfilePhotoRequestBuilder ;
28+ import com .microsoft .jenkins .azuread .avatar .EntraAvatarProperty ;
2629import com .microsoft .jenkins .azuread .scribe .AzureAdApi ;
2730import com .microsoft .jenkins .azuread .utils .UUIDValidator ;
2831import com .thoughtworks .xstream .converters .Converter ;
4750import hudson .util .Secret ;
4851import io .jenkins .plugins .azuresdk .HttpClientRetriever ;
4952
53+ import java .io .File ;
54+ import java .nio .file .Files ;
55+ import java .nio .file .StandardCopyOption ;
5056import javax .servlet .http .HttpSession ;
5157
5258import jenkins .model .Jenkins ;
5359import jenkins .security .SecurityListener ;
60+ import jenkins .util .SystemProperties ;
5461import okhttp3 .Request ;
5562import org .apache .commons .lang3 .RandomStringUtils ;
5663import org .apache .commons .lang3 .StringUtils ;
@@ -146,8 +153,8 @@ public AccessToken getAccessToken() {
146153 tokenRequestContext .setScopes (singletonList (graphResource + ".default" ));
147154
148155 AccessToken accessToken = ("Certificate" .equals (credentialType ) ? getClientCertificateCredential () : getClientSecretCredential ())
149- .getToken (tokenRequestContext )
150- .block ();
156+ .getToken (tokenRequestContext )
157+ .block ();
151158
152159 if (accessToken == null ) {
153160 throw new IllegalStateException ("Access token null when it is required" );
@@ -185,6 +192,7 @@ ClientCertificateCredential getClientCertificateCredential() {
185192 .httpClient (HttpClientRetriever .get ())
186193 .build ();
187194 }
195+
188196 public boolean isPromptAccount () {
189197 return promptAccount ;
190198 }
@@ -230,6 +238,7 @@ public String getClientCertificateSecret() {
230238 public String getCredentialType () {
231239 return credentialType ;
232240 }
241+
233242 public String getTenantSecret () {
234243 return tenant .getEncryptedValue ();
235244 }
@@ -465,9 +474,14 @@ public HttpResponse doFinishLogin(StaplerRequest request)
465474
466475 // Enforce updating current identity
467476 SecurityContextHolder .getContext ().setAuthentication (auth );
468- updateIdentity (auth .getAzureAdUser (), User .current ());
477+ User currentUser = User .current ();
478+ updateIdentity (auth .getAzureAdUser (), currentUser );
469479
470480 SecurityListener .fireAuthenticated2 (userDetails );
481+
482+ if (!isDisableGraphIntegration ()) {
483+ updateAvatar (userDetails , currentUser );
484+ }
471485 } catch (Exception ex ) {
472486 LOGGER .log (Level .SEVERE , "error" , ex );
473487 throw ex ;
@@ -480,6 +494,50 @@ public HttpResponse doFinishLogin(StaplerRequest request)
480494 }
481495 }
482496
497+ private void updateAvatar (AzureAdUser userDetails , User currentUser ) {
498+ if (currentUser == null ) {
499+ return ;
500+ }
501+ try {
502+ if (SystemProperties .getBoolean (AzureSecurityRealm .class .getName () + ".disableAvatar" , false )) {
503+ return ;
504+ }
505+ ProfilePhotoRequestBuilder photosRequestBuilder = getAzureClient ()
506+ .users (userDetails .getObjectID ()).photos ("48x48" );
507+ LOGGER .finest ("Fetching avatar metadata" );
508+ ProfilePhoto profilePhoto = photosRequestBuilder .buildRequest ().get ();
509+ LOGGER .finest ("Completed fetching avatar metadata" );
510+ if (profilePhoto != null ) {
511+ LOGGER .finest ("Fetching avatar" );
512+ try (InputStream inputStream = photosRequestBuilder .content ().buildRequest ().get ()) {
513+ if (inputStream != null ) {
514+ String mediaContentType = profilePhoto .additionalDataManager ().get ("@odata.mediaContentType" )
515+ .getAsString ();
516+ EntraAvatarProperty .AvatarImage avatarImage = new EntraAvatarProperty .AvatarImage (
517+ mediaContentType
518+ );
519+ EntraAvatarProperty entraAvatarProperty = new EntraAvatarProperty (avatarImage );
520+ File targetFile = new File (currentUser .getUserFolder (), "entra-avatar." + avatarImage .getFilenameSuffix ());
521+
522+ Files .copy (
523+ inputStream ,
524+ targetFile .toPath (),
525+ StandardCopyOption .REPLACE_EXISTING );
526+ currentUser .addProperty (entraAvatarProperty );
527+ LOGGER .finest ("Saved avatar" );
528+ }
529+
530+ } catch (IOException e ) {
531+ LOGGER .log (Level .WARNING , "Failed to save profile photo for %s" .formatted (currentUser .getId ()), e );
532+ }
533+ } else {
534+ LOGGER .finest ("No avatar found" );
535+ }
536+ } catch (GraphServiceException e ) {
537+ LOGGER .log (e .getResponseCode () == 404 ? Level .FINER : Level .WARNING , "Failed to get profile photo for %s" .formatted (currentUser .getId ()), e );
538+ }
539+ }
540+
483541 JwtClaims validateIdToken (String expectedNonce , String idToken ) throws InvalidJwtException {
484542 JwtClaims claims = getJwtConsumer ().processToClaims (idToken );
485543 final String responseNonce = (String ) claims .getClaimValue ("nonce" );
@@ -878,7 +936,7 @@ private void updateIdentity(final AzureAdUser azureAdUser, final User u) {
878936 if (StringUtils .isNotBlank (azureAdUser .getEmail ())) {
879937 UserProperty existing = u .getProperty (UserProperty .class );
880938 if (existing == null || !existing .hasExplicitlyConfiguredAddress ()) {
881- u .addProperty (new Mailer .UserProperty (azureAdUser .getEmail ()));
939+ u .addProperty (new Mailer .UserProperty (azureAdUser .getEmail ()));
882940 }
883941 }
884942 } catch (IOException e ) {
0 commit comments