Linking a federated login against an existing Azure AD B2C local account

Rory Braybrook
The new control plane
5 min readJun 22, 2022
Image showing linking — Wikimedia

This post was inspired by this stackoverflow question.

It refers to this sample policy.

I have posted around linking here and here so I thought it was worth exploring the subject further.

Normally, when you federate with AAD, B2C creates a “shadow” local account.

The identity structure looks like this:

"identities": [
{
"signInType": "federated",
"issuer": "https://login.microsoftonline.com/00d...c79/v2.0",
"issuerAssignedId": "215...6bd"
},
{
"signInType": "userPrincipalName",
"issuer": "b2ctenant.onmicrosoft.com",
"issuerAssignedId": "cpim_c..3-596f-4...4-9e83-3...4@b2ctenant.onmicrosoft.com"
}
],

You have a federated account where the issuer is the AAD tenant and the issuerAssignedId is the objectID of the AAD user that signed in.

You also have a “local” account where B2C creates a GUID and then creates a UPN of the form cpim_GUID@b2ctenant.

What’s interesting is that the attributes for this user include:

  • “accountEnabled”: false,
  • “creationType”: null,

The account is disabled because this user does not have a B2C password. It doesn’t need one because this type of account always signs in from an IDP. In B2C, you cannot attach a local account to a social (federated) account. You have to do it the other way around. Hence the need for a dummy UPN.

The creationType would normally be something like “localAccountAuthentication” or “socialIdpAuthentication”.

If the user you want to link already has federated, then the policy throws this error:

Inage showing your AAD account already exists in the directory. You need to delete this account.

The objectID is for the existing B2C shadow account. You can’t have duplicate identities so you can’t attach this identity if it already exists. You have to delete it.

The idea is that you create the local account first and then you add an “oidToLink” attribute that contains the objectId of that user’s Azure AD account. When the AAD user federates, you search B2C for the matching key and that’s the local account you link the federated account to.

If there is not a duplicate account but you have not created the “oidToLink” attribute, you get:

The technical Profile with id \""AAD-FindB2CUserWithAADOid\"" in Policy id \""B2C_1A_AAD_Link_TrustFrameworkExtensions of Tenant id \""b2ctenant.onmicrosoft.com\"" requires that an error be raised if the claims principal record does not exist for storing claims

When you federate with the Azure AD account, you then link that account with the existing local account. There is no shadow account.

Create the user. I always use this utility.

The JSON file is:

{
"accountEnabled": true,
"displayName": "Linked User",
"givenName": "Linked",
"surname": "User",
"mailNickname": "LinkU",
"userPrincipalName": "LinkU@b2ctenant.onmicrosoft.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": false,
"password": "xWw...WH-d"
},
"passwordPolicies": "DisablePasswordExpiration",
"identities": [
{
"signInType": "oidToLink",
"issuer": "b2ctenant.onmicrosoft.com",
"issuerAssignedId": "215...6bd"
}
],
"extension_51f...e4e_requiresMigrationBool": "true"
}

and the command is:

b2c create-user create-azure-link.json

When I compare this account to the federated one, I see:

  • “accountEnabled”: true
  • “creationType”: “LocalAccount”
  • “passwordPolicies”: “DisablePasswordExpiration”

Notice that issuerAssignedId matches that of the federated user above. When we run the policy, it will link this account with the “Linked User” local account.

Before we run the policy, the identities of “Linked User” are:

"identities": [
{
"signInType": "oidToLink",
"issuer": "b2ctenant.onmicrosoft.com",
"issuerAssignedId": "215...6bd"
},
{
"signInType": "userPrincipalName",
"issuer": "b2ctenant.onmicrosoft.com",
"issuerAssignedId": "LinkU@b2ctenant.onmicrosoft.com"
}
],

Notice that we have a “real” UPN.

After we run the policy, the identities of “Linked User” are:

“identities”: [
{
“signInType”: “federated”,
“issuer”: “https://login.microsoftonline.com/00d…c79/v2.0",
“issuerAssignedId”: “215…6bd”
},
{
“signInType”: “oidToLink”,
“issuer”: “b2ctenant.onmicrosoft.com”,
“issuerAssignedId”: “215…6bd”
},
{
“signInType”: “userPrincipalName”,
“issuer”: “b2ctenant.onmicrosoft.com”,
“issuerAssignedId”: “LinkU@b2ctenant.onmicrosoft.com
}
],

Also:

"extension_51f...e4e_requiresMigrationBool": false,

is set to “false” whereas it was “true” in the JSON above.

The reason for this is that the first time we run the policy if the federated user exists, it’s a bug. If the user doesn’t exist, we link it. When we run the policy the second time, the user will exist because it’s been linked. We then do not link it. If we tried, we would get an error because the identities have to be unique.

It’s worth looking at the code that does this linking.

The code is in the AAD federation technical policy:

<OutputClaimsTransformation ReferenceId="CreateUserIdentity"/>
<OutputClaimsTransformation ReferenceId="AppendUserIdentity"/>

The code for these are:

<!-- On sign-in (first time) with social account, create a userIdentity claim, using issuerUserId and issuer name -->
<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>
<!-- Add a userIdentity to the userIdentities collection. .-->
<ClaimsTransformation Id="AppendUserIdentity" TransformationMethod="AddItemToUserIdentityCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="userIdentity" TransformationClaimType="item"/>
<InputClaim ClaimTypeReferenceId="userIdentities" TransformationClaimType="collection"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="userIdentities" TransformationClaimType="collection"/>
</OutputClaims>
</ClaimsTransformation>

We create the federated user identity (userIdentity) that contains issuerUserId (“215…6bd”) and issuer (“https://login.microsoftonline.com/00d…c79/v2.0").

We then add this userIdentity to the userIdentities string collection that contains the “identities” (the signInType) above.

The updated collection is written to the user via “AAD-UserUpdateWithUserIdentities”.

Once the users are linked, I can log in locally with the UPN and password in the JSON above and I can also login via federation to AAD and I get the same claims back:

"given_name": "Linked",
"family_name": "User",
"name": "Linked User",

This makes sense because they are two different logins for the same user account.

And if I check the users in B2C, there is no shadow account.

As always the custom policy is in this gist.

That answers the first part of the question 😃.

The second part is around blocking access if the account is disabled i.e.

“accountEnabled”: false

The account is blocked for users who log in using the local account and for users who log in using the federated account.

Using the utility, the JSON file is:

{
"accountEnabled": "false"
}

and the command is:

B2C update-user <user object ID> update.json

When I try and login with the local user I get:

Image showing the username or password is invalid

There must be something in “login-NonInteractive” that does this? Anyway, it stops the user logging in.

When I try and login with the AAD federated user I get:

Image showing your UPN account is bloacked

All good!

--

--

Rory Braybrook
The new control plane

NZ Microsoft Identity dude and MVP. Azure AD/B2C/ADFS/Auth0/identityserver. StackOverflow: https://bit.ly/2XU4yvJ Presentations: http://bit.ly/334ZPt5