Using identities and linking them in Azure AD B2C

Image of fingerprint
commons.wikimedia (Adaiyaalam)

There’s a good description of identities in Azure AD B2C here.

To paraphrase:

“A customer account, which could be a consumer, partner, or citizen, can be associated with these identity types:

  • Local identity — The username and password are stored locally in the Azure AD B2C directory. Theses are called local accounts.
  • Federated identity — Also known as a social or enterprise accounts, the identity of the user is managed by a federated identity provider like Facebook or ADFS.

A user with a customer account can sign in with multiple identities. For example, username, email, employee ID, government ID, and others. A single account can have multiple identities, both local and social, with the same password.

In the Microsoft Graph API, both local and federated identities are stored in the user identities attribute, which is of type objectIdentity.

The identities collection represents a set of identities used to sign in to a user account. This collection enables the user to sign in to the user account with any of its associated identities.

The identities attribute can contain up to ten objectIdentity objects.

Each object contains the following properties:

signInType

Specifies the user sign-in types in your directory.

For local account: emailAddress, emailAddress1, emailAddress2, emailAddress3, userName, or any other type you like.

Social accounts must be set to federated. issuer string.

This specifies the issuer of the identity. For local accounts (where signInType is not federated), this property is the local B2C tenant default domain name, for example contoso.onmicrosoft.com.

For social identity (where signInType is federated) the value is the name of the issuer, for example facebook.com

issuerAssignedId

Specifies the unique identifier assigned to the user by the issuer. The combination of issuer and issuerAssignedId must be unique within your tenant.

For local accounts, when signInType is set to emailAddress or userName, it represents the sign-in name for the user.

When signInType is set to:

  • emailAddress (or starts with emailAddress like emailAddress1) issuerAssignedId must be a valid email address
  • userName (or any other value), issuerAssignedId must be a valid local part of an email address
  • federated, issuerAssignedId represents the federated account unique identifier

e.g. a local account identity with a sign-in name, an email address as sign-in, and with a social identity.

"identities": [
{
"signInType": "userName",
"issuer": "contoso.onmicrosoft.com",
"issuerAssignedId": "johnsmith"
},
{
"signInType": "emailAddress",
"issuer": "contoso.onmicrosoft.com",
"issuerAssignedId": "jsmith@yahoo.com"
},
{
"signInType": "federated",
"issuer": "facebook.com",
"issuerAssignedId": "5eecb0cd"
}
]

“ (End of quote).

In this example, the user could sign in with “johnsmith” or “jsmith@yahoo.com”. They would share a common password.

Custom Policy

In a custom policy, the user above would use:

“SelfAsserted-LocalAccountSignin-Email” or

“SelfAsserted-LocalAccountSignin-Username”

For the Facebook account, the user would sign in using Facebook. This may have a different sign in name and password.

Local accounts

The “signinname” can be anything you like. In the policies samples, you will find:

  • “signInNames.emailAddress”
  • “signInNames.username”
  • “signInNames.phoneNumber”
  • “signInNames.oidToLink”

For the phoneNumber sample e.g.

<TechnicalProfile Id="CombineCountryCodeAndNationalNumber">
<DisplayName>Combine country code and national number</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="ConvertStringToPhoneNumber"/>
</InputClaimsTransformations>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInNames.phoneNumber"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>

<TechnicalProfile Id="AAD-UserReadUsingPhoneNumber">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInNames.phoneNumber" Required="true"/>
</InputClaims>
...

So the phoneNumber is a combination of country code and national number resulting in “signInNames.phoneNumber”.

Notice that the input to “AAD-UserReadUsingPhoneNumber” is “signInNames.phoneNumber” whereas the input to “AAD-UserReadUsingUserName” would be “signInNames.username”.

If the users have a linked loyaltyNumber, then you could have:

  • “signInNames.username”
  • “signInNames.loyaltyNumber”

so you could sign in either with the username or the loyaltyNumber.

Federated accounts

At the moment, there is a problem.

In the base file, we find:

<ClaimsTransformation Id="CreateAlternativeSecurityId" TransformationMethod="CreateAlternativeSecurityId">
<InputClaims>
<InputClaim ClaimTypeReferenceId="issuerUserId" TransformationClaimType="key"/>
<InputClaim ClaimTypeReferenceId="identityProvider" TransformationClaimType="identityProvider"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="alternativeSecurityId" TransformationClaimType="alternativeSecurityId"/>
</OutputClaims>
</ClaimsTransformation>

and the documentation is here.

These are always of type “federated”.

Using the Facebook example above:

  • identityProvider = facebook.com
  • issuerUserId = 5eecb0cd

The collection is called “AlternativeSecurityIds”.

You can use “AddItemToAlternativeSecurityIdCollection” to build up a collection of these.

However, in the custom policy samples for account linking, we find:

<ClaimsTransformation Id="CreateUserIdentity" TransformationMethod="CreateUserIdentity">
<InputClaims>
<InputClaim ClaimTypeReferenceId="issuerUserId" TransformationClaimType="issuerUserId"/>
<InputClaim ClaimTypeReferenceId="identityProvider" TransformationClaimType="issuer"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="userIdentity" TransformationClaimType="userIdentity"/>
</OutputClaims>
</ClaimsTransformation>

A userIdentity is the equivalent of alternativesecurityId and the collection is called userIdentities.

You can use “AddItemToUserIdentityCollection” to build up a collection of these.

There is currently no documentation around this.

They are essentially two different naming conventions that refer to the same base structure.

I’ve added a gist that shows this in action.

The steps are:

<OutputClaimsTransformation ReferenceId="CreateIssuer"/>
<OutputClaimsTransformation ReferenceId="CreateIssuerUserId"/>
<OutputClaimsTransformation ReferenceId="CreateUserIdentityToLink"/>
<OutputClaimsTransformation ReferenceId="AppendUserIdentityToLink"/>

Using the Facebook example, the:

  • identityProvider = facebook.com
  • issuerId = 5eecb0cd

You combine these to create a userIdentity and then add that to the userIdentities collection.

You then link the federated identity to the local user by using “AAD-UserUpdateWithUserIdentities” and adding the userIdentities collection.

...
PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId"/>
<PersistedClaim ClaimTypeReferenceId="userIdentities"/>
</PersistedClaims>
...

Note that while you can add a federated account to a local account, the reverse is not true. “This is due to not being able to add a Local Account to a Federated-only account.”

Using custom policies, the “signInType” is always “federated”.

Non-federated accounts

To have “different” signin types, you have to use something like:

  • “signInNames.username”
  • “signInNames.loyalty”

It is not possible to have two “signInNames.username” with different values as the one overwrites the other.

The basic problem is that there doesn’t seem to be a collection of signInNames.

The custom policy below is also in the gist above.

Let’s link a loyalty number to an existing user name.

We ask for the two pieces of information, then we read by username to get the objectId and then we write by objectId with:

<PersistedClaim ClaimTypeReferenceId="username" PartnerClaimType="signInNames.userName"/>
<PersistedClaim ClaimTypeReferenceId="loyaltyName" PartnerClaimType="signInNames.loyalty"/>

This links the two together.

Run the policy and type an existing user name and a loyalty number.

Image with username of “linkuser” and loyalty number of “12345678”

This will link the two. You see the result in the JWT returned.

Image of JWT showing signInNames.username of “linkuser” and signInNames.loyalty number of “12345678”

Looking at the linking in the portal (see Portal below), you see that they are linked.

Image showing “Sign-in type” of “username” and “Sign-in type” of “loyalty”

If I try and run the policy again with a different loyalty number, the loyalty number I type in overwrites the loyalty number above.

There is, however, a problem with this.

To read the user using username, you need “AAD-UserReadUsingUsername” where the input is “signInNames.username”.

To read the user using loyalty number, you need to create a “AAD-UserReadUsingLoyaltyNumber” where the input is “signInNames.loyaltyNumber”.

The problem is when to use each one. You could say that loyalty numbers are all digits and usernames have to have a least one non-digit character and then use a regex to decide but that’s not ideal.

Luckily, there’s a neat trick to get around this 😃

The custom policy for this is also in the gist above.

It’s based on this sample. Thanks to @Jas for pointing me in the right direction.

<TechnicalProfile Id="AAD-UserReadUsingIdentifier">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames" Required="true"/>
</InputClaims>
...

The idea is that you put the identifier in “signInName” and use “signInNames” as the partner.

Image showing “Sign-in type” of “username” and “Sign-in type” of “loyalty”

Running the sample, you need to enter the signInName. I can enter either “linkuser” or “12340987” and I get the same claims for both! I don’t need to figure out which is which.

Image showing signInName = linkuser.
Image showing signInName = 12340987

Very neat!

The identities need to be unique. If I pick another user and try and link that identity to “12340987” (that has already been linked above), I get:

Image showing “AADB2C99001: This user already exists, and profile ‘AAD-UserWriteProfileUsingObjectId-Link’ does not allow the same user to be created again.”

Graph API

The API to create users and identities is here. There’s a sample here. Look at “Example 2” for an outline of how to create the “identities” JSON array.

Note that when using the Graph API, you can specify the “signInType”. This allows you to have a local user with two different usernames.

You could use either username in “AAD-UserReadUsingUsername” and you would get the same claims returned.

You can also use this client and add the “identities” structure in the JSON.

Portal

You can create multiple identities for a user in the portal.

Image showing user with two email addresses and two usernames

When you view the user, click on the hyperlink.

Image showing to use hyperlink under “Issuer”

This shows you all the identities the user has.

Image showing user with two email addresses and two usernames and an UPN

The UPN is the base identity that all the others link to.

All good!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store