Custom security attributes in Azure AD part 1: a trip down memory lane

While Azure AD has never been positioned as a direct replacement for Active Directory, many customers have expectations that functionalities that have existed for decades in on-premises environments are brought to the cloud as well. Or a reasonably suitable replacement is made available. One of the most prominent examples of such functionality is extending the directory schema to support additional object types and attributes. In this series, we will present a quick summary of the methods that have been available until now, and introduce you to the latest addition, custom security attributes.

A quick refresher

Let’s start by refreshing your knowledge about the methods available in the service to address such scenarios. Those include the AAD Connect “directory extensions” feature, as well as two methods implemented within the Graph API: schema extensions and open extensions. In addition, the “traditional” set of extensionAttributes deserve an honorable mention.

Azure AD Connect Sync Directory Extensions

Probably the most popular method, or method most people have at least heard of, is Azure AD Connect Sync Directory Extensions. Provided as part of the “optional features” you can configure within the AAD Connect config wizard, Directory extension attribute sync was first introduced back in 2015. It allowed for up to 100 user- and/or group-related AD attributes to be synchronized, with support for multi-valued attributes added shortly after the feature reached GA.

The setup is fairly straightforward, with the Azure AD Connect config wizard UI hiding all the gritty details. In a nutshell, all you need to do is make sure the Directory extensions optional feature is enabled, then select from the list of available attributes. On the screenshot below, we’ve selected two such attributes, namely employeeID and employeeNumber.

Just to add some context on how things work on the “backend”, on Azure AD side of things a new application is provisioned to “host” the set of attribute extensions. Said application is named “Tenant Schema Extension App”, as shown below.

The set of Azure AD extensions can then be obtained by querying the /extensionProperties endpoint, and they all have quite peculiar names, linked to the appid (clientId) value of the Azure AD application (not the same as the objectID). Some examples are listed below:

On the “client” side of things, a set of inbound and outbound synchronization rules “map” the values of the selected AD attributes to the corresponding extension, and serve to enforce limits on the length and number of attributes (maximum of 100 extension property values per object). The screenshots below showcase the inbound and outbound rules corresponding to the employeeID and employeeNumber extensions, as introduced above:

It is important to understand that we can create such extensions outside of the AAD Connect config wizard. In fact, you can configure directory extensions on any Azure AD integrated application, by issuing a POST request against the /extensionProperties endpoint on the application object. Even multi-tenant scenarios are supported, with some caveats as described in the official documentation. Using this approach allows you to also configure extensions for object types such as service principals or devices. An inherent dependency on the application object is enforced however: should the object get deleted, all defined directory extensions immediately become inaccessible.

To query all the available extensions within the directory, one has to query all applications individually, as currently the $expand operator is not supported for the extensionProperties navigation property. The important thing is that there is at least one method available, unlike some of the methods we will cover next. Once we have the list of extension properties in use within the tenant, we can also generate a list of all objects that have such attributes configured. Here as well we have to rely on client-side filters, as the properties are not indexed, and server-side filtering is not possible even when advanced query parameters are used.

The examples below show how you can query the set of available extensions via the Azure AD PowerShell module or the Microsoft Graph SDK. Once you have the set of attributes, you can also query their values across all users, or filter based on the value, including filtering just the user that have the given attribute configured. The last three examples below are leveraging the Graph API endpoints directly:

C:\> Get-AzureADApplication | Get-AzureADApplicationExtensionProperty

C:\> Get-MgApplication | % { Get-MgApplicationExtensionProperty -ApplicationId $_.id }

GET https://graph.microsoft.com/v1.0/users?$select=id,extension_1d101edaaa7f48c586902d4cd567e0a9_employeeNumber

GET https://graph.microsoft.com/v1.0/users?$filter=extension_1d101edaaa7f48c586902d4cd567e0a9_employeeNumber eq '11111111'&$select=id,extension_1d101edaaa7f48c586902d4cd567e0a9_employeeNumber

https://graph.microsoft.com/beta/users?$filter=extension_1d101edaaa7f48c586902d4cd567e0a9_employeeNumber ne null&$select=id,extension_1d101edaaa7f48c586902d4cd567e0a9_employeeNumber&$count=true

Most of the feature complexity remains hidden behind the AAD Connect config wizard UI, and overall the experience of creating and synchronizing directory extensions is fairly straightforward. What is not as straightforward is “consuming” the data, as said attributes are only exposed via the Graph API endpoints. Well, technically, one could also query them via the Get-AzureADApplicationExtensionProperty cmdlet (Get-MgApplicationExtensionProperty if using the Microsoft Graph module) or also leverage them for the creation Dynamic membership rules.

And with that, let’s move to the two extensibility methods provided by the Microsoft Graph, namely open extensions and schema extensions. We’ll start by covering the latter.

Schema extensions

Schema extensions are global, as in once defined and published, they can be leveraged by any organization. In other words, schema extension definitions are not tenant-specific (data values are though, so there is no security issue here). This in turn means that you can simply browse the set of available schema extensions and choose one that fits your needs. If no appropriate schema extensions are available, you can also create your own (refer to the official documentation for details on this).

If you are willing to spend some time sifting through the gazillion of already published schema extensions and find a good match, you can directly use it/assign values for the corresponding object types. For the sake of example, we can pick a random schema extension, such as the adatumisv_courses one (https://graph.microsoft.com/v1.0/schemaExtensions?$filter=id eq ‘adatumisv_courses’), then use a PATCH request to configure its values for one of our users. The process is identical to changing any other (predefined) attribute:

Once the change has been made, we can query the values by explicitly requesting the adatumisv_courses attribute to be returned (by adding it to the $select operator). Do note that neither the /v1.0 nor the /beta endpoint will return said attribute, unless we are using $select. Thus, when using schema extensions, you need to have their IDs readily available, otherwise you cannot even check their values.

In addition to user objects, schema extensions can be used for few other resource types, namely: administrativeUnit, device, event, group, message, organization, post, and user. Few different data types are supported for property values, with the max length set to 256 bytes/characters. And of course, only the “owner” (signified by the application Id provided as part of the creation process) of a given schema extension can further update it, if needed.

The main challenge with schema extensions is their discoverability, specifically being able to list all the schema extensions that are in use within the organization and their values. Unless you want to iterate over each available schema extension definition (remember, those are global!), you better pray the dev team adheres to proper knowledge management practices. For schema extensions your organization has created, you can filter based on the appID (“owner”), and once you have the corresponding set of attributes, you can query or even filter their values across all objects. For example:

GET https://graph.microsoft.com/beta/users?$select=id,displayName,adatumisv_courses

GET https://graph.microsoft.com/beta/users?$filter=adatumisv_courses/id eq 1&$select=id,displayName,adatumisv_courses

Another inconvenience is represented by the naming schema requirements. As every organization can create extensions, the id of the extension cannot be a random value, but it’s instead linked to a custom domain within the tenant or prefixed with “ext” (check id property description for more details). Properties within the schema extension need to be referenced by the “parent” id, so instead of having an attribute such as “employeeID”, we end up with something akin to “domainname_extname/employeeID”.

Open extensions

Open extensions on the other hand do not enforce any naming schema requirements. They are represented by their own resource within the Graph, namely the openTypeExtension resource. This enables us to easily see which open extensions are configured for a given object, by simply querying the extensions navigation property (/extensions endpoint). Similar to schema extensions, open extensions support administrativeUnit, device, event, group, message, organization, post, and user resources, but they also add support for tasks, contacts and more.

Unlike schema extensions though, open extensions only pertain to the specific object(s) they were created for. There are no “global” extensions, and even within the same tenant different objects of the same type can have different sets of extensions configured. This in turn makes them difficult to use for scenarios such as extending the directory schema, although that’s technically still possible.

To create an open extension, one needs to have sufficient permissions, depending on the type of resource. After permissions have been taken care of, issue a POST request against the /extensions endpoint for the corresponding object. For example, the request below will add the “employeeID” open extension to a user object and set its value:

Once the property has been added, in order to obtain the list of extensions and their values we can query the same /extensions endpoint, via a GET request:

Similarly, updating the property value is done via a PATCH request against the corresponding /extension/{id} endpoint. You need to keep in mind however that open extensions are configured per-object. Thus, if we try to perform the same PATCH operation against a different user object, it will fail. In other words, open extensions do not extend the tenant-wide schema, but each individual object’s schema.

Once you have configured open extensions for all objects you care about and populated their values, you can use simple Graph API queries to list them across the directory. Filtering however is a bit tricker, as it only works against Outlook open extensions. Thus, there is no easy way to filter all objects with non-null values for their “employeeID“ attribute, instead you will need to fetch the full set of users.

GET https://graph.microsoft.com/beta/users?$select=id&$expand=extensions

GET https://graph.microsoft.com/beta/users?$select=id&$expand=extensions($filter=id eq 'Com.Contoso.OpenExtension1') //only returns selected ext in the output, still all users

In addition, because of the lack of naming schema or input validation, you can end up with the same attribute being used for storing different types of data across your objects.

Honorable mention:  extensionAttributes1-15

Last but not least, let’s not forget about the “classic” set of extensionAttributes1-15, added as part of the Exchange AD schema extension. While their availability across Microsoft 365 had been a mixed story, the current situation is that you can query them directly via the Graph API (part of the onPremisesExtensionAttributes navigation property), or via the Exchange Online PowerShell cmdlets (where they are known as CustomAttribute1-15). If you are syncing their values from on-premises AD, the attributes are read-only as one would expect, however for cloud-authored objects you can manage them directly. And just recently, Microsoft added support for a similar set of attributes for device objects, namely extensionAttributes/ExtensionAttribute1-15. That said, Group objects in Azure AD still do not offer support for this set of attributes (although the corresponding CustomAttribute1-15 are available for Exchange group objects).

Since the attribute names are predefined and cannot be changed, you’ll likely need to keep a list of “mappings” for their values, if you are to leverage said attributes for storing custom data within the directory. On the positive side of things, discovering which objects take advantage of said attributes is very easy, and so is filtering, especially on Exchange side of things. You can also use the Graph API for most filtering scenarios, for example to list all users with non-zero values for a given extensionAttribute:

GET https://graph.microsoft.com/beta/users?$filter=onPremisesExtensionAttributes/extensionAttribute9 ne null&$count=true&$select=id,onPremisesExtensionAttributes

For additional information about extension attributes and how they compare to Custom Security Attributes (which we will cover next), refer to this recent article by Tony Redmond.

In the next article of these series, we will introduce Azure AD Custom Security Attributes. Part 3 covers some additional scenarios. Lastly, in part 4, we will compare Custom Security Attributes against the “older” methods we introduced here.

This entry was posted in Azure AD, Graph API, Microsoft 365, Office 365. Bookmark the permalink.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.