How to capture Profile Picture from Azure Entra ID into ServiceNow User record?

Sharmila2
Tera Contributor

Hi All,

 

I am trying to capture Profile Picture for users with the data coming from Azure Entra ID. I created a IntegrationHub action to call Azure Entra ID endpoint for the profile picture and the data received is "binary data of a PNG file". Now I am trying to convert this binary data into Base64 encoded format to use that information and store it as an attachment. I am following this KB article https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0597756 and trying to replicate the same logic. Below is the logic in Action step.

 

 

var attachment = new GlideSysAttachment();
var EncodedBytes = GlideStringUtil.base64Encode(inputs.binary_data);

//set up inputs
var rec = new GlideRecord('sys_user');
rec.get('sys_id_of_user_record');
var fileName = 'image.png';
var contentType = 'image/png';
var content = GlideStringUtil.base64DecodeAsBytes(EncodedBytes);
var agr = attachment.write(rec, fileName, contentType, EncodedBytes);

 

 

File is getting attached to the user record, but the file which is generated is not in a recognized format. Can anyone share their experience in capturing Profile picture information from any sources into ServiceNow?

1 REPLY 1

Patrick Ouimet2
Tera Contributor

got something working for this:

Azure Portal

  • in the app registry for the entra / servicenow connection, added the following api permission:
    ProfilePhoto.Read.All

Servicenow


User record
added fields for :

  • graph photo etag - using this to track when there are changes in the photos
  • graph photo sync state - used to know how the sync went
  • graph photo sync attempts - used to track how many times photo sync happened
  • graph sync photo sync error - used to know the last message for the sync
  • graph photo last checked - time of last sync check

    PatrickOuimet2_3-1782326674753.png

     

 

Flow Designer:

Actions:
Mark Graph Photo Sync Result:

inputs:

PatrickOuimet2_4-1782327531998.png

script step:

PatrickOuimet2_5-1782327584747.png

 


 script:

(function execute(inputs, outputs) {

    outputs.success = false;

    if (!inputs.user_sys_id) {
        outputs.message = 'Missing user_sys_id';
        return;
    }
    
    var user = new GlideRecord('sys_user');

    if (!user.get(inputs.user_sys_id)) {
        outputs.message = 'User not found';
        return;
    }

    if (user.hasRole('admin') || user.hasRole('security_admin')) {
    outputs.success = true;
    outputs.message = 'Skipped - admin account';
    return;
}

    try {

        // ✅ Ensure a real change is detected
        var currentState = user.getValue('u_graph_photo_sync_state');

        if (currentState !== inputs.state) {
            user.setValue('u_graph_photo_sync_state', inputs.state);
        } else {
            // 🔥 force a change if same value (critical)
            user.setValue('u_graph_photo_sync_state', inputs.state + '');
        }

        user.setValue('u_graph_photo_sync_error', inputs.message || '');
        user.setValue('u_graph_photo_last_checked', new GlideDateTime());

        if (inputs.increment_count == true || inputs.increment_count == 'true') {
            var attempts = parseInt(user.getValue('u_graph_photo_sync_attempts') || '0', 10);
            user.setValue('u_graph_photo_sync_attempts', attempts + 1);
        }

        // ✅ This ensures the platform commits update
        //user.setForceUpdate(true);

        var result = user.update();

        gs.info('[PHOTO SYNC FIXED] Updated user: ' + result + ' state=' + inputs.state);

        outputs.success = true;
        outputs.message = 'Updated successfully';

    } catch (e) {
        outputs.success = false;
        outputs.message = e.message;
        gs.error('[PHOTO SYNC ERROR] ' + e.message);
    }

})(inputs, outputs);

outputs:
success and message, both linked to the script outputs
 
Check Graph User Photo Change:
inputs:

PatrickOuimet2_6-1782327646881.png

 


Rest step:

PatrickOuimet2_2-1782325985067.png

 

(function execute(inputs, outputs) {

    var ETAG_FIELD = 'u_graph_photo_etag';

    function normalizeEtag(etag) {
        if (etag === null || etag === undefined)
            return '';

        var value = etag.toString();

        value = value
            .replace(/\r|\n/g, '')
            .replace(/\\/g, '')
            .replace(/W\//g, '')
            .replace(/"/g, '')
            .trim();

        var match = value.match(/[a-fA-F0-9]{32,}/);
        if (match && match.length > 0)
            return match[0].toLowerCase();

        return value.toLowerCase();
    }

    var userSysId = inputs.user_sys_id;
    var status = parseInt(inputs.status_code, 10);

    outputs.status_code = status;
    outputs.should_download = false;
    outputs.graph_etag = '';
    outputs.stored_etag = '';
    outputs.photo_sys_id = '';
    outputs.message = '';
    outputs.debug_message = '';

    if (status == 404) {
        outputs.message = 'No Graph photo found';
        outputs.debug_message = 'Graph returned 404 for user_sys_id=' + userSysId;
        return;
    }

    if (status == 429) {
        outputs.success = false;
        outputs.message = 'Graph throttled - retry later';
        return;
    }

    if (status != 200) {
        outputs.message = 'Graph metadata call failed: ' + status;
        outputs.debug_message = 'Non-200 status. status=' + status + ', user_sys_id=' + userSysId;
        return;
    }

    var user = new GlideRecord('sys_user');
    if (!user.get(userSysId)) {
        outputs.message = 'User not found';
        outputs.debug_message = 'Could not find sys_user for sys_id=' + userSysId;
        return;
    }

    var body = {};

    try {
        body = (typeof inputs.response_body == 'string')
            ? JSON.parse(inputs.response_body)
            : inputs.response_body;
    } catch (e) {
        outputs.message = 'Unable to parse Graph metadata response';
        outputs.debug_message = 'Parse error=' + e.message + ', response_body=' + inputs.response_body;
        return;
    }

    var rawGraphEtag = body['@odata.mediaEtag'] || '';
    var graphPhotoId = body.id || '';

    var normalizedGraphEtag = normalizeEtag(rawGraphEtag);
    var rawStoredEtag = user.getValue(ETAG_FIELD) || '';
    var storedEtag = normalizeEtag(rawStoredEtag);
    var userPhoto = user.getValue('photo') || '';

    outputs.graph_etag = normalizedGraphEtag;
    outputs.stored_etag = storedEtag;
    outputs.photo_sys_id = userPhoto;

    outputs.debug_message =
        'User=[' + user.getDisplayValue() + ']' +
        ' | user_sys_id=[' + user.getUniqueValue() + ']' +
        ' | etag_field=[' + ETAG_FIELD + ']' +
        ' | raw_graph_etag=[' + rawGraphEtag + ']' +
        ' | raw_stored_etag=[' + rawStoredEtag + ']' +
        ' | graph_etag=[' + normalizedGraphEtag + ']' +
        ' | stored_etag=[' + storedEtag + ']' +
        ' | graph_len=[' + normalizedGraphEtag.length + ']' +
        ' | stored_len=[' + storedEtag.length + ']' +
        ' | photo=[' + userPhoto + ']' +
        ' | graph_photo_id=[' + graphPhotoId + ']';

    if (!normalizedGraphEtag || graphPhotoId == '1x1') {
        outputs.should_download = false;
        outputs.message = 'No real Graph photo available';
        return;
    }

    if (!userPhoto) {
        outputs.should_download = true;
        outputs.message = 'ServiceNow photo is empty';
        return;
    }

    if (!storedEtag) {
        outputs.should_download = true;
        outputs.message = 'Stored ETag is empty';
        return;
    }

    if (storedEtag !== normalizedGraphEtag) {
        outputs.should_download = true;
        outputs.message = 'Graph photo changed';
        return;
    }

    outputs.should_download = false;
    outputs.message = 'Photo unchanged - skipped';

})(inputs, outputs);

outputs

PatrickOuimet2_7-1782327784015.png

 

outputs:
should_download,  graph_etag, status_code, message, stored_etag, photo_sys_id, debug_message all linked to the script outputs

 

Download and Save Graph User Photo:
inputs

PatrickOuimet2_8-1782328045148.png

 

Script step

PatrickOuimet2_9-1782328085931.png

 

(function execute(inputs, outputs) {

    var userSysId = inputs.user_sys_id;
    var graphEtag = inputs.graph_etag || '';
    var status = parseInt(inputs.status_code, 10);

    outputs.success = false;
    outputs.status_code = status;
    outputs.message = '';

    if (!userSysId) {
        outputs.message = 'Missing user sys_id';
        return;
    }

    if (status == 404) {
        outputs.success = true;
        outputs.message = 'No Graph photo found';
        return;
    }

    if (status == 429) {
        outputs.success = false;
        outputs.message = 'THROTTLED';
        return;
    }

    if (status != 200) {
        outputs.success = false;
        outputs.message = 'Graph failed: ' + status;
        return;
    }

    var user = new GlideRecord('sys_user');
    if (!user.get(userSysId)) {
        outputs.message = 'User not found';
        return;
    }

    if (user.hasRole('admin') || user.hasRole('security_admin')) {
        outputs.success = true;
        outputs.message = 'Skipped - admin account';
        return;
    }

    try {

        // 🔥 THIS IS THE FIX
        var inputStream = inputs.response_stream;

        if (!inputStream) {
            outputs.message = 'No response stream available';
            return;
        }

        var gsa = new GlideSysAttachment();

        var attachmentSysId = gsa.write(
            user,
            'photo.jpg',
            'image/jpeg',
            inputStream
        );

        user.setValue('photo', attachmentSysId);
        user.setValue('u_graph_photo_etag', graphEtag);
        user.update();

        // Cleanup duplicates
        var cleanup = new GlideRecord('sys_attachment');
        cleanup.addQuery('table_name', 'sys_user');
        cleanup.addQuery('table_sys_id', user.sys_id);
        cleanup.addQuery('file_name', 'photo.jpg');
        cleanup.addQuery('sys_id', '!=', attachmentSysId);
        cleanup.query();

        while (cleanup.next()) {
            cleanup.deleteRecord();
        }

        outputs.success = true;
        outputs.message = 'Photo saved correctly';

    } catch (e) {
        outputs.success = false;
        outputs.message = e.message;
    }

})(inputs, outputs);

 

Rest Step

PatrickOuimet2_10-1782328138462.png

 

Script

PatrickOuimet2_11-1782328181787.png

(function execute(inputs, outputs) {

    outputs.success = false;
    outputs.message = '';
    outputs.status_code = parseInt(inputs.status_code, 10);

    var userSysId = (inputs.user_sys_id || '').toString();
    var graphEtag = (inputs.graph_etag || '').toString();

    if (!userSysId) {
        outputs.message = 'Missing user_sys_id';
        return;
    }

    if (outputs.status_code == 404) {
        outputs.success = true;
        outputs.message = 'No Graph photo found';
        return;
    }

    if (outputs.status_code == 429) {
        outputs.success = false;
        outputs.message = 'THROTTLED';
        return;
    }

    if (outputs.status_code != 200) {
        outputs.success = false;
        outputs.message = 'Graph photo download failed. Status=' + outputs.status_code;
        return;
    }

    var user = new GlideRecord('sys_user');
    if (!user.get(userSysId)) {
        outputs.message = 'User not found';
        return;
    }

    // Safety guard. Ideally admin users are skipped before this action is called.
    if (user.hasRole('admin') || user.hasRole('security_admin')) {
        outputs.success = true;
        outputs.message = 'Skipped - admin account';
        return;
    }

    try {

        // Find newest photo.jpg attachment created by the REST step
        var att = new GlideRecord('sys_attachment');
        att.addQuery('table_name', 'sys_user');
        att.addQuery('table_sys_id', user.sys_id);
        att.addQuery('file_name', 'photo.jpg');
        att.orderByDesc('sys_created_on');
        att.setLimit(1);
        att.query();

        if (!att.next()) {
            outputs.success = false;
            outputs.message = 'No photo.jpg attachment found after REST download';
            return;
        }

        var latestAttachmentId = att.sys_id.toString();

        // Link attachment to user photo field and store ETag
        user.setValue('photo', latestAttachmentId);
        user.setValue('u_graph_photo_etag', graphEtag);
        user.update();

        // Cleanup duplicate photo.jpg attachments
        var cleanup = new GlideRecord('sys_attachment');
        cleanup.addQuery('table_name', 'sys_user');
        cleanup.addQuery('table_sys_id', user.sys_id);
        cleanup.addQuery('file_name', 'photo.jpg');
        cleanup.addQuery('sys_id', '!=', latestAttachmentId);
        cleanup.query();

        while (cleanup.next()) {
            cleanup.deleteRecord();
        }

        outputs.success = true;
        outputs.message = 'Photo attached, linked, ETag stored, duplicates cleaned';

    } catch (e) {
        outputs.success = false;
        outputs.message = e.message;
    }

})(inputs, outputs);

 

 Outputs
success and message linked to the script outputs. Status code is the rest step status code.

 

 

 

Flow

PatrickOuimet2_12-1782328353768.png

 

  • Look up user records with the criteria (active = true, email contains whatever.com, and Graph Photo Sync State = ready)
  • for each match, use the "Mark Graph Photo Sync Result" action to set the graph photo sync state  action to processing 
  • then use the action for "Check Graph User Photo Change"
  • if the check returns that should_download = true, then use the "Download and Save Graph User Photo" action. Match the user email to the step 2, for each > user record > email, the user record to the For each > User Record, the graph_etag to step 4 Check Graph USer Photo Change > graph_etag, and user_sys_id to step 2, for each > user record > sys id
  • If the success message for the download step 6 is true, then do the action for "Mark Graph Photo Sync Result" for each of the step 2, for each > user record > sys id, state is done, and message is from step 6 for "Download and Save Graph User Photo" > message
  • Else, if the message back from step 6 contains the word "Throttled"  then we will use action "Mark Graph Photo Sync Result" for each of the step 2, for each > user record > sys id, state is ready, and message is from step 6 for "Download and Save Graph User Photo" > message
  • Step 14 should be an else for Step 5, and reuses the throttle conditions we did in case they should download is false. 

I ran one background script to mark everyone as ready to import.

(function () {

    var count = 0;

    var user = new GlideRecord('sys_user');
    user.addActiveQuery();
    user.addNotNullQuery('email');
    user.addQuery('email', 'CONTAINS', '@X.com');
    user.query();

    while (user.next()) {

        user.setWorkflow(false);

        user.setValue('u_graph_photo_sync_state', 'ready');
        user.setValue('u_graph_photo_sync_attempts', 0);
        user.setValue('u_graph_photo_sync_error', '');
        user.setValue('u_graph_photo_last_checked', '');

        user.update();

        count++;
    }

    gs.info('[Graph Photo Sync] Seeded users for photo sync: ' + count);

})();

 

I then build a quick dashboard just so I can track the progress. Skips may be due to people not having a graph photo to pull or already having a photo:

PatrickOuimet2_13-1782328672815.png

 

While I am sure there are holes or things I have not thought of, this does work and an image does show up on the user record