Some PDIs are currently unavailable, and PDI actions are paused. View the latest updates here. Read More

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