{"id":391,"date":"2023-12-07T13:24:15","date_gmt":"2023-12-07T12:24:15","guid":{"rendered":"https:\/\/sparrowte.ch\/?p=391"},"modified":"2024-11-04T20:22:41","modified_gmt":"2024-11-04T19:22:41","slug":"391","status":"publish","type":"post","link":"https:\/\/sparrow365.de\/index.php\/en\/2023\/12\/07\/391\/","title":{"rendered":"Working with Entra ID Directory Extensions"},"content":{"rendered":"<h2>Entra ID Directory Extensions<\/h2>\n<p>Have you ever wanted to save information in Entra ID, but couldn&#8217;t find an appropriate attribute to store your data?<br \/>\nFor example, storing someones nickname in a usable fashion? Or you need a specific attribute from your HR Software for Single Sign-On or authorization? Or for Dynamic Groups? <\/p>\n<p>If you have synchronized Active Directory attributes not available to Entra ID by default, you might have unintentionally created Directory Extensions &#8211; and now you need to read those values.<\/p>\n<p>This information is not available in the Entra ID GUI (as of December 2023), so your only option is the Graph API &#8211; my personal choice of interface is PowerShell. And since the <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/extensibility-overview\">Microsoft documentation<\/a> is missing Snippets I thought it might be interesting to write this down. <\/p>\n<p><br class=\"\"><\/p>\n<blockquote>\n<p>I use a separate JSON File to store information that I don&#8217;t necessarily want to share on the internet:<br \/>\n<strong>tenantId<\/strong> : GUID of the Entra ID Tenant<br \/>\n<strong>clientID<\/strong> : Application ID of my App Registration \/ Enterprise App<br \/>\n<strong>thumb<\/strong> : Thumbprint of the certificate I use to authenticate as the App Registration<br \/>\n<strong>exampleUser<\/strong> : User UPN of my Demo user, Object ID also works<br \/>\n<strong>extensionAppOID<\/strong> : OID &#8211; <strong><em>Object ID !<\/em><\/strong> of the App Registration I want to associate my Directory Extension with  <\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<p>Connecting:<\/p>\n<pre><code class=\"language-powershell\">$graphConfig = ConvertFrom-Json $(Get-Content -Raw $hiddenValuesPath)\nConnect-MgGraph -TenantId $graphConfig.tenantID -ClientId $graphConfig.clientID -CertificateThumbprint $graphConfig.thumb<\/code><\/pre>\n<p><br class=\"\"><\/p>\n<h2>Finding and Reading Directory Extensions<\/h2>\n<blockquote>\n<ul>\n<li>Use Find-MgGraphCommand &quot;CmdLet&quot; for the API Uri used by the CmdLet, this gives <a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/api\/overview?view=graph-rest-1.0\">better documentation<\/a> as well as the necessary permissions  <\/li>\n<li>Get-MgUser \/ The Graph API will only return the Properties you ask for \ud83d\ude09<\/li>\n<\/ul>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<pre><code class=\"language-powershell\"># Store the Directory Extension I want to look at, since the actual Name of the extension is rather unwieldy\n$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match &quot;exampleExtension&quot;\n\n# &quot;I try to use as few Microsoft.Graph modules as possible because updating takes forever&quot;\n# $extension = (Invoke-MgGraphRequest POST &quot;\/v1.0\/directoryObjects\/getAvailableExtensionProperties&quot; -OutputType PSObject).Value | Where-Object Name -match &quot;exampleExtension&quot;\n\n# Get the Value for a specific User\n$user = Get-MgUser -UserId $graphConfig.exampleUser -Property Displayname, Id, UserPrincipalName, $extension.Name \n\n# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties\n$extensionValueExpr = @{Name = &quot;$($extension.Name)&quot;; Expression = {$_.AdditionalProperties.$($extension.Name)}}\n$user | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table<\/code><\/pre>\n<p>Result:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrowte.ch\/wp-content\/uploads\/2023\/12\/image-1701948539195.png\" alt=\"Exemple Result 1\" \/><\/p>\n<hr \/>\n<p>Alternatively, we can filter all Users by the Extension Attributes:<\/p>\n<pre><code class=\"language-powershell\">$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match &quot;nickName&quot;\n$users = Get-MgUser -Filter &quot;startswith($($extension.Name),&#039;I&#039;)&quot; -Property Displayname, Id, UserPrincipalName, $extension.Name \n\n# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties\n$extensionValueExpr = @{Name = &quot;nickName&quot;; Expression = {$_.AdditionalProperties.$($extension.Name)}}\n$users | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table<\/code><\/pre>\n<p>Result:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrowte.ch\/wp-content\/uploads\/2023\/12\/image-1701948756759.png\" alt=\"Example Result 2\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>Create an Extension<\/h2>\n<blockquote>\n<p>! This should not be done with the extensions in use by Entra ID Connect Sync &#8211; the Source of Truth for these attributes should remain OnPremises<br \/>\nUse the Entra ID Connect Sync Wizard to add Extensions<\/p>\n<\/blockquote>\n<p><br class=\"\"><\/p>\n<pre><code class=\"language-powershell\"># There must always be an assigned app, but otherwise the Extensions can be assigned rather broadly\n$oid = $graphConfig.extensionAppOID\n$params = @{\n    name = &quot;freshExtension&quot;\n    dataType = &quot;String&quot;\n    targetObjects = @(&quot;User&quot;)\n}\nNew-MgApplicationExtensionProperty -ApplicationId $oid @params\n\n# If I were to use the App ID of my Registration with the attached Permissions I would have to fetch the ID from the Graph API:\n# $oid = (Get-MgApplication -Filter $(&quot;AppId eq &#039;{0}&#039;&quot; -f $graphConfig.clientID)).Id\n\n# Less Modules, Params from Above are used:\n# Invoke-MgGraphRequest POST &quot;\/v1.0\/applications\/$oid\/extensionProperties&quot; -Body $params<\/code><\/pre>\n<p>Result:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrowte.ch\/wp-content\/uploads\/2023\/12\/image-1701948782039.png\" alt=\"Example Result 3\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>Set the Extension on a User<\/h2>\n<blockquote>\n<p>! This should not be done with the extensions in use by Entra ID Connect Sync &#8211; the Source of Truth for these attributes should remain OnPremises<br \/>\nUse OnPrem AD Management solutions to change Values (Set-ADUser, IAM, etc.)<\/p>\n<\/blockquote>\n<pre><code class=\"language-powershell\">$extension  = Get-MgDirectoryObjectAvailableExtensionProperty  | where Name -match &quot;freshExtension&quot;\n\n# We could also set multiple Extensions with one Request, but then we would have to explicitly Name them all :)\n$params = @{\n    &quot;$($extension.Name)&quot; = &quot;Hello, this is a fresh value&quot;\n}\nUpdate-MgUser -UserId $graphConfig.exampleUser -BodyParameter $params\n\n# The Update does not return a result on Success, so lets have a look:\n$user = Get-MgUser -UserId $graphConfig.exampleUser -Property Displayname, Id, UserPrincipalName, $extension.Name \n\n# Define a Calculated Property for Select-Object, that resolves the nesting of AdditionalProperties\n$extensionValueExpr = @{Name = &quot;freshExtension&quot;; Expression = {$_.AdditionalProperties.$($extension.Name)}}\n$user | Select-Object Displayname, Id, UserPrincipalName, $extensionValueExpr | Format-Table\n<\/code><\/pre>\n<p>Result:<br \/>\n<img decoding=\"async\" src=\"https:\/\/sparrowte.ch\/wp-content\/uploads\/2023\/12\/image-1701948805997.png\" alt=\"Example Result 4\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>Read all Extension Attributes<\/h2>\n<p>If we want to see all extension properties at once, things get a little more complicated &#8211; do let me know if there is a better way \ud83d\ude09  <\/p>\n<pre><code class=\"language-powershell\"># Get all Extension Properties - if run on a regular basis these could be cached\n$extensions = Get-MgDirectoryObjectAvailableExtensionProperty\n\n# Standard User Properties we want to Fetch\n$properties = @(&quot;Displayname&quot;, &quot;Id&quot;, &quot;UserPrincipalName&quot;) \n\n# Fetch all users and the Extension Properties\n$users = Get-MgUser -All -Property ($properties + $extensions.Name)\n\n$allUsersParsed = [System.Collections.Arraylist]::new()\n\n# Flatten user data by merging extension properties from nested hashtables into a single-level hashtable\n# Also filter out unnecessary fields from the full Graph User Schema to leave populated properties\nForeach ($u in $users){\n    $userParsed = @{}\n    Foreach ($prop in $properties) {\n        $userParsed.$prop = $u.$prop\n    }\n    $userParsed += $u.AdditionalProperties\n    $allUsersParsed.Add([pscustomobject]$userParsed) | Out-Null\n}\n\n$allUsersParsed | Format-Table ($properties + $extensions.Name)<\/code><\/pre>\n<p>Result:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/sparrowte.ch\/wp-content\/uploads\/2023\/12\/image-1701948827692.png\" alt=\"Example Result 5\" \/><\/p>\n<p><br class=\"\"><\/p>\n<h2>Conclusion<\/h2>\n<p>So we can now read and create directory extensions &#8211; very cool. But where do we go from here? <strong>I<\/strong> did this for fun, but let me know what useful things you used this knowledge for\ud83d\ude01   <\/p>\n<p><br class=\"\"><\/p>\n<p>I won&#8217;t be moderating comments and I don&#8217;t want your email address; please join the discussion on <a href=\"https:\/\/www.linkedin.com\/posts\/julian-sperling-4bba72228_arbeiten-mit-entra-id-directory-extensions-activity-7138549730292256768-8x3h?utm_source=share&amp;utm_medium=member_desktop\">my corresponding LinkedIn Post<\/a>.<\/p>\n<p><br class=\"\"><\/p>\n<p>If you are interested in the things I do, please <a href=\"https:\/\/www.linkedin.com\/in\/julian-sperling-4bba72228\/\">follow me on LinkedIn<\/a>.   <\/p>\n<p><br class=\"\"><\/p>\n<h2>Further Reading<\/h2>\n<ul>\n<li>\n<p><a href=\"https:\/\/learn.microsoft.com\/en-us\/entra\/identity\/hybrid\/connect\/how-to-connect-sync-feature-directory-extensions\">How to configure Entra ID Connect Sync Directory Extensions<\/a> <\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/extensibility-overview?tabs=http#comparison-of-extension-types\">Comparison of different ways to add custom data to Entra ID<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/extensibility-overview?tabs=http#considerations-for-using-directory-extensions\">Considerations when deleting Directory Extensions<\/a><\/p>\n<\/li>\n<li>\n<p><a href=\"https:\/\/learn.microsoft.com\/en-us\/graph\/api\/application-post-extensionproperty?view=graph-rest-1.0&amp;tabs=http#request-body\">More Options when creating Directory Extensions<\/a><\/p>\n<\/li>\n<\/ul>\n<p><br class=\"\"><\/p>\n<p><br class=\"\"><\/p>\n<blockquote>\n<p>Updates: <\/p>\n<ol>\n<li>08.12.23 &#8211; Improvements of Code Clarity around Select-Object<\/li>\n<\/ol>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>Entra ID Directory Extensions Have you ever wanted to save information in Entra ID, but couldn&#8217;t find an appropriate attribute to store your data? For example, storing someones nickname in a usable fashion? Or you need a specific attribute from your HR Software for Single Sign-On or authorization? Or for Dynamic Groups? If you have&#8230; &raquo; <a class=\"read-more-link\" href=\"https:\/\/sparrow365.de\/index.php\/en\/2023\/12\/07\/391\/\">weiterlesen<\/a><\/p>\n","protected":false},"author":2,"featured_media":404,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[76,78],"tags":[80,82,84,86,88,90,92,94,96,98,100,103,105],"class_list":["post-391","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-me-id-en","category-powershell-en","tag-aad-en","tag-aad-connect-en","tag-azure-ad-en","tag-directoryextensions-en","tag-entra-en","tag-entra-id-en","tag-entra-id-connect-en","tag-entra-id-connect-sync-en","tag-graph-en","tag-graph-api-en","tag-microsoft-graph-en","tag-powershell-en","tag-powershell-sdk-en"],"_links":{"self":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/391","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/comments?post=391"}],"version-history":[{"count":11,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/391\/revisions"}],"predecessor-version":[{"id":412,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/posts\/391\/revisions\/412"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media\/404"}],"wp:attachment":[{"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/media?parent=391"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/categories?post=391"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sparrow365.de\/index.php\/wp-json\/wp\/v2\/tags?post=391"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}