Multi-forest SSO to O365: implementing multiple immutable IDs

When implementing Office 365 for Enterprises in a multiple-forest environment you will find the topic of the immutable ID rearing its head more than once.  It’s very important.  And often overlooked.

In a single-forest environment the DIRSYNC appliance and AD FS take the Active Directory Domain Services (AD DS) objectGuid attribute as the immutable ID.  In a multi-forest environment this might not make sense.  If users move between forests then the GUID is no longer immutable and you have a painful re-matching experience ahead of you.  This is why the topic comes up and why the right decision needs to be made early on in the design process.

However, even when you choose to use something else, e.g. employee ID, that choice is generally not applicable to all objects, i.e. while employeeID works for employees what about room and equipment mailboxes?  Administrative accounts?  Groups?  And what about the scenario whereby there are different attributes used in different forests, e.g. utilises contosoUniqueID and utilises employeeID?

To cut a long story short, you’re probably going to need two different sets of immutable ID – for employees and contractors the employee ID or employee number or something local to your environment that is absolute, i.e. customer unique identifying ID; for non-employees the default of the objectGuid generally works best.  Handling this distinction in FIM is pretty trivial.  The immutable ID flow (it’s generally called the sourceAnchor in FIM unless you properly start from scratch) is an advanced (coded) rule.  Adding some logic to the method or code-block should be reasonably painless providing you have a criteria that distinguishes one set of objects from the other(s).

Handling this distinction in AD FS took me a little longer to get right.  So I thought I’d share my approach (and use the opportunity to add some more issuance rules to the blog for easy access when I’m on site and can’t remember the syntax).

Default issuance transform rules

The Convert-MsolDomainToFederated, New-MsolFederatedDomain or Update-MsolFederatedDomain cmdlets create the O365 relying party (RP) trust in your on-premises AD FS.  Depending on your arguments these cmdlets create (or update) the RP trust with two or three issuance transform rules.  Without the –SupportMultipleDomain switch you get two rules:

c:[Type == “”%5D

=> issue(store = “Active Directory”, types = (“;, “;), query = “samAccountName={0};userPrincipalName,objectGUID;{1}”, param = regexreplace(c.Value, “(?<domain>[^\\]+)\\(?<user>.+)”, “${user}”), param = c.Value);

c:[Type == “”%5D

=> issue(Type = “;, Value = c.Value, Properties[“”%5D = “urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified”);

With the –SupportMultipleDomain switch you get a third rule:

c:[Type == “”%5D
=> issue(Type = “;, Value = regexreplace(c.Value, “.+@(?<domain>.+)”, “http://${domain}/adfs/services/trust/“));

In the multi-forest scenario these rules need to be modified.  That third rule only works for contiguous namespaces, e.g. and  If you have multiple domains in a non-contiguous namespace, e.g., and you need to tweak that rule a little…

c:[Type == “”%5D
issue(Type = “;, Value = regexreplace(c.Value, “^((.*)([.|@]))?(?<domain>[^.]*[.].*)$”, “http://${domain}/adfs/services/trust/“));

If, in your multi-forest scenario you are changing the immutable ID, then you also need to change the first rule a little, e.g.

c:[Type == “”%5D

=> issue(store = “Active Directory”, types = (“;, “;), query = “samAccountName={0};userPrincipalName,employeeID;{1}”, param = regexreplace(c.Value, “(?<domain>[^\\]+)\\(?<user>.+)”, “${user}”), param = c.Value);

In the above rule I change objectGUID to be employeeID – you’d change it to be whatever you decide your immutable ID is going to be.

Job done.

Except, what about users without employeeID values?  Administrative accounts, test accounts, accounts in the other forest that use a different attribute, etc.

You might not need these to actually authenticate to the service.  Great.  In that case it really is job done.  But what if your administrative accounts are going to be managing SharePoint Online?  What if the immutable ID is actually different in each forest (this can cause other issues, but let’s just assume we have different sets of valid users with different immutable ID requirements).  You could add an arbitrary value to the employeeID, e.g. the employeeID with an A in front of it for the administrator account scenario, or something equally nasty which will inevitably come back to bite you in the future.

Alternatively you could modify those rules to support both types of user, just like you did in your sourceAnchor inbound attribute flow in FIM.

Modified issuance transform rules

In my scenario I’m going to utilise two attributes to distinguish one set of users from another.  The businessCategory attribute will describe an account type, e.g. Standard for a regular user, Elevated for an administrative user, Equipment for an equipment mailbox, Facility for room mailboxes, etc.  The employeeID attribute will be mandatory for Standard users.  What this means is that an employee who will be utilising O365 in some way will have a businessCategory of Standard and an employeeID.  Anything else is not a typical employee, therefore I’m going to utilise the defaults – objectGUID for the immutable ID.

In FIM I have a configuration file for the rules extension that specifies the attribute and value that defines a standard user and the attribute and a regex that defines the immutable ID.  If they’re a standard user with a valid employeeID I use the employeeID as the sourceAnchor/immutable ID.  For everyone else the GUID.

In AD FS I will also apply this logic via the following rules.

@RuleName = “Lookup AD DS attributes”
c:[Type == “;, Issuer == “AD AUTHORITY”] => add(store = “Active Directory”, types = (“urn:mso365:tmp/addsbuscat”, “urn:mso365:tmp/addsempid”, “;, “urn:mso365:tmp/addsobjectguid”), query = “;businessCategory,employeeID,userPrincipalName,objectGuid;{0}”, param = c.Value);

@RuleName = “Issue UPN”
c1:[Type == “”%5D => issue(Type = c1.Type, Value = c1.Value);

@RuleName = “Issue empid as ImmutableID for Standard Users”
c1:[Type == “urn:mso365:tmp/addsbuscat”, Value =~ “Standard”] && c2:[Type == “urn:mso365:tmp/addsempid”, Value =~ “^([P|C]{0,1})\d{4,8}”] => issue(Type = “;, Value = c2.Value);

@RuleName = “Check for non-standard user-type”
NOT EXISTS([Type == “urn:mso365:tmp/addsbuscat”]) => add(Type = “urn:mso365:tmp/addsbuscat”, Value = “NonStandard”);

@RuleName = “Issue objectGuid as ImmutableID for non-Standard users”
c1:[Type == “urn:mso365:tmp/addsbuscat”, Value != “Standard”] && c2:[Type == “urn:mso365:tmp/addsobjectguid”] => issue(Type = “;, Value = c2.Value);

@RuleName = “Issue nameid (unspecified format)”
c:[Type == “”%5D => issue(Type = “;, Value = c.Value, Properties[“”%5D = “urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified”);

@RuleName = “Issue issuerid”
c:[Type == “”%5D => issue(Type = “;, Value = regexreplace(c.Value, “^((.*)([.|@]))?(?[^.]*[.].*)$”, “http://${domain}/adfs/services/trust/”));

I’ve just dumped those using PowerShell.  They can go back in via PowerShell in this format too.  Note also that I’ve essentially deleted all the rules created by the MSO cmdlets.  I still issue the claims issued by the default cmdlets, I just do it a little differently.

Let’s discuss each rule individually.  Note that the order is very important.

First rule (Lookup AD DS attributes)

@RuleName = “Lookup AD DS attributes”
c:[Type == “;, Issuer == “AD AUTHORITY”]
=> add(store = “Active Directory”, types = (“urn:mso365:tmp/addsbuscat”, “urn:mso365:tmp/addsempid”, “;, “urn:mso365:tmp/addsobjectguid”), query = “;businessCategory,employeeID,userPrincipalName,objectGuid;{0}”, param = c.Value);

This is an efficiency rule.  I’ll do all the LDAP lookups in one place and the rest of the rules will utilise values in the claims pipeline.

This rule gets businessCategory, employeeID, userPrincipalName and objectGuid.  It adds these values to the claim pipeline.  UPN is added as the UPN claim expected by MSO and the other attributes as arbitrary claim types that make it easy for me to work with.

Second rule (Issue UPN)

@RuleName = “Issue UPN”
c1:[Type == “”]
=> issue(Type = c1.Type, Value = c1.Value);

This rule simply issues the UPN claim added to the pipeline in the first rule.  Because I needed some values that I won’t necessarily issue I chose to retrieve them all in one hit to minimise LDAP traffic.  The UPN that came back is good to be issued, so this rule simply does that.

Third rule (Issue empid as ImmutableID for Standard Users)

@RuleName = “Issue empid as ImmutableID for Standard Users”
c1:[Type == “urn:mso365:tmp/addsbuscat”, Value =~ “Standard”]
&& c2:[Type == “urn:mso365:tmp/addsempid”, Value =~ “^([P|C]{0,1})\d{4,8}”]
=> issue(Type = “;, Value = c2.Value);

As the name implies this rule issues the employee ID as the immutable ID claim.  However this only issues the immutable ID if the value of businessCategory (in the pipeline as the type urn:mso365:tmp/addsbuscat) is Standard and the employeeID (in the pipeline as the type urn:mso365:tmp/addsempid) is of the format 00123456, P1234 or C001234 (plus some other permutations but you get my point, I basically support three different employeeID types).  If the business category is not “Standard” or the employee ID doesn’t match my regex pattern then this claim is not issued.  This is important.

Fourth rule (Check for non-standard user-type)

@RuleName = “Check for non-standard user-type”

NOT EXISTS([Type == “urn:mso365:tmp/addsbuscat”])
=> add(Type = “urn:mso365:tmp/addsbuscat”, Value = “NonStandard”);

If the business category claim does not exist (there is no value in AD DS) add a dummy value.  Why?  Because you can’t concatenate another clause with NOT EXISTS!  Sad smile

Fifth rule (Issue objectGuid as ImmutableID for non-Standard users)

@RuleName = “Issue objectGuid as ImmutableID for non-Standard users”
c1:[Type == “urn:mso365:tmp/addsbuscat”, Value != “Standard”]
&& c2:[Type == “urn:mso365:tmp/addsobjectguid”]
=> issue(Type = “;, Value = c2.Value);

Issue the objectGuid as the immutable ID claim if the business category is not “Standard”.  Basically, I need to check for object GUID and then issue that value (transform the claim).  I can’t seem to do this with the NOT EXISTS() function in the clause, so I added a dummy value in rule four and now perform an inequality comparison in addition to the presence check and, ultimately, issue an immutable ID if the previous rule (rule three) did not.

Sixth rule (Issue nameid (unspecified format))

@RuleName = “Issue nameid (unspecified format)”
c:[Type == “”%5D
=> issue(Type = “;, Value = c.Value, Properties[“”%5D = “urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified”);

This is one of the default rules.  It issues the immutable ID value as an unspecified SAML name ID claim.

Seventh rule (Issue issuerid)

@RuleName = “Issue issuerid”
c:[Type == “”%5D
=> issue(Type = “;, Value = regexreplace(c.Value, “^((.*)([.|@]))?(?<domain>[^.]*[.].*)$”, http://${domain}/adfs/services/trust/));

This is the modified default rule needed to support multiple non-contiguous domain names (UPN suffixes).


This post turned out to be longer than planned.  I hope it’s useful.  The long and short of this post is that it is possible to configure your AD FS issuance transform rules to support multiple sets of immutable ID in multi-forest deployments.  I provided one example solution to this problem.


About Paul Williams

IT consultant working for Microsoft specialising in Identity Management and Directory Services.
This entry was posted in AD FS, Office 365 and tagged , , , . Bookmark the permalink.

17 Responses to Multi-forest SSO to O365: implementing multiple immutable IDs

  1. Please excuse some of the weird formatting where the claim types have been translated into URLs and the closing double quote HTML encoded. I can’t see anything wrong with this in Live Writer, Live Write source view, WordPress WYSIWYG editor or WordPress source view! 😦

  2. Hello Dear.
    I already have a tenant with DirSync I would like to know if with these rules can transform it to multiple forest support without losing synced users with your mailbox ?

    Thanks so much advance!

    • The techniques described allow you the flexibility to handle multiple sets of users, so the short answer is yes. Whether or not my exact rules will work is not so clear. Multiple forest support requires some changes to the rules, as described in the first part of this post. The rest of the post is about adding additional flexibility. It’s really a question about immutable ID.

      • Thanks so much by reply!

        My question like you tell is about ImmutableID.

        Can you help me explain like convert a already production environment with a only forest and Dirsync to multi forest with FIM ??

        Can you leave your contact ?

      • Sorry for the delay. I wrote most of a reply and closed the browser before submitting thinking I’d posted the response! 🙂

        Contact details – no sorry, for that you’ll need to go through Microsoft Services. I contribute to the community as best I can but I have a job and a family so can’t be providing ad-hoc support and assistance all the time.

        You only have one real choice when you determine that you need to move from DirSync and one forest and a default configuration to multiple forests or an atypical configuration: you replace DirSync with FIM. You can choose to maintain the objectGuid immutable ID for the existing forest and do a basic swap, assuming you have the right configuration available, and look to add new forests using appropriate logic. Or you can start again, and define a new immutable ID that makes more sense in a multi-forest environment. For more info. on picking the right immutable ID see

        Note that if you change the immutable ID you need to delete all user objects in the tenant and recreate. This is a slow process with downtime and can have a major impact on 365 services such as EXO and OneDrive.

        If you stick with the defaults and just introduce a new forest be mindful that you cannot delete users and recreate them, which means you cannot move users between forests without losing licenses and possibly more in O365.

        There’s a lot to think about, so test and plan, don’t rush in!

  3. Pingback: Windows Azure Active Directory Connector part 3: immutable ID | Yet another identity management blog

  4. Pingback: (2014-03-21) GALSync, DIRSync And SSO With Office 365 Blog Posts From MSResource.NET « Jorge's Quest For Knowledge!

  5. Sol K says:

    Also watch out for the smart /curly quotes! Have to change them to standard straight quotes

    • Thanks for pointing that out. I’m a real lazy wordpress user and use what I’m given and don’t fiddle around (in other words I know identity but not web ;-)) so yeah, ’cause I can’t format half my posts properly you often need to do some find and replace action with the quotes (and if someone knows how to stop it HTLM-encoding my quotes where it’s not supposed to then please do tell).

  6. Pingback: adfs default claim rules | Peter's ruminations

  7. Pingback: (2014-10-01) TroubleShooting Federation/SSO To Windows Azure AD And Office 365 « Jorge's Quest For Knowledge!

  8. Pingback: Revisiting the Microsoft Online immutable ID design decision | Yet another identity management blog

  9. jason says:

    Excellent writeup. Using these claim rules have you ever encountered an issue with using UPN for both the Source/Target anchor(immutableID) with this approach? Reasoning being in a multiforest environment the UPN make it clear which forest a user came from.

  10. Pingback: Claim rules for the Azure Active Directory (#AzureAD) Relying Party (RP) trust | Yet another identity management blog

  11. Jon says:


    This post has been immensely useful to me – working on a method to handle migrating users from one forest into another and coping with the objectGUID/ immutableID change.

    In Rule 1 – you populate the “temporary” store – urn:mso365:tmp with the AD attributes. Is this method documented anywhere that you can point me to? That method alone is very powerful within the claims pipeline, I’m sure I’ll be using it again.

    Many thanks for posting this article.



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s