How to capture Profile Picture from Azure Entra ID into ServiceNow User record?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
07-07-2024 08:57 PM
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?
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Wednesday
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
Flow Designer:
Actions:
Mark Graph Photo Sync Result:
inputs:
script step:
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:
Rest step:
(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
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
Script step
(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
Script
(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
- 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:
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