Azure B2C: Custom claim isn't written into AAD via custom policy - azure

It seems that I've hit a road block when it comes to writing custom claims to Azure Active Directory (AAD). I'm trying to write the organization into ADD, but it appears that when I query the users via Graph API, I don't see any trace of the organization data. I'm wondering if there's something off with how I attempted to write the data or there's a techincal detail that I'm not aware of that can cause this issue?
Here's the custom claim that I want to save to AAD.
<ClaimType Id="extension_organization">
<DisplayName>Organization Name</DisplayName>
<DataType>string</DataType>
<UserHelpText>Name of admin's organization.</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
And here is where I'm writing the claims (it's pretty much what you would see in the examples):
<TechnicalProfile Id="AAD-UserWriteUsingLogonEmail">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<PersistedClaims>
<!-- Required claims -->
<PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" />
<PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>
<PersistedClaim ClaimTypeReferenceId="displayName" DefaultValue="unknown" />
<PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration" />
<!-- Optional claims. -->
<PersistedClaim ClaimTypeReferenceId="givenName" />
<PersistedClaim ClaimTypeReferenceId="surname" />
<PersistedClaim ClaimTypeReferenceId="extension_organization" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
On an interesting note, it seems that not even the e-mail can be seen.

When querying the Graph API for custom/extension attributes, you will need to make sure you select the extension attributes with the following syntax:
extension_{b2cExtensionsAppId}_organization
Where {b2cExtensionsAppId} is the Application/Client ID for the application in your B2C tenant that is automatically generated:
b2c-extensions-app. Do not modify. Used by AADB2C for storing user data.
Edit - Remove the dashes (-) from the Extensions Application/Client ID
79af1ae0-cacb-401a-9a42-1f2178adc0ef gets converted to 79af1ae0cacb401a9a421f2178adc0ef.
Example:
b2c_79af1ae0cacb401a9a421f2178adc0ef_organization

Related

Azure B2C: How to use OtherMails attribute for MFA

I am using B2C custom policies which allows signup/signin with the username instead of the traditional email.
As part of the signup process, I am saving the Email in the otherMails attribute.
when choosing MFA as Email, I don't see the email field prepopulated with the email that I have on user record.
Can otherMails attribute be used for MFA email?
<ClaimsTransformation Id="CreateEmailsFromOtherMailsAndSignInNamesInfo" TransformationMethod="AddItemToStringCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="otherMails" TransformationClaimType="collection" />
</InputClaims>
<TechnicalProfile Id="AAD-UserWriteUsingLogonName">
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="otherMails" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="LocalAccountSignUpWithLogonName">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<TechnicalProfile Id="AAD-ReadCommon">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<RelyingParty>
<OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="emails" />
</RelyingParty>
The field used for MFA is "strongAuthenticationEmailAddress".
That's used by the back end so I doubt it can be changed.

Is there a way to conditionally copy claims in custom policies?

We want to return the user email as one of the claims after signin. However, the email is in different claims depending on how the user signed in or even after they just signed up. Is there a way to merge them all into a single email claim?
Our current workaround is to merge them in the very last step by just specifying the claim multiple times. However, when creating a SAML policy, this will create multiple claims and cause errors. We want to merge at an earlier stage.
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="trustFrameworkPolicy" Required="true" DefaultValue="{policy}" />
<!-- If the user used social sign in -->
<OutputClaim ClaimTypeReferenceId="email" />
<!-- Required when the user just signed up and still has the "sign up session" -->
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
<!-- Get email from local account - must not set default value -->
<OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="identityProvider" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
What I tried so far:
<ClaimsTransformation Id="CopySignInNameToEmail" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="inputClaim"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
However, this will fail if the input claim is empty or not yet present.
• You can try using the ‘FormatStringMultipleClaims’ type of claim transformation method as below by giving input claims as multiple attributes and then merging the output claims as desired in a single string as below: -
<ClaimsTransformation Id="CreateEmailIDFromFirstNameDisplayNameLastName"
TransformationMethod="FormatStringMultipleClaims">
<InputClaims>
<InputClaim ClaimTypeReferenceId="givenName"
TransformationClaimType="inputClaim1" />
<InputClaim ClaimTypeReferenceId="surName"
TransformationClaimType="inputClaim2" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0} {1}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim"
/>
</OutputClaims>
</ClaimsTransformation>
• The above transformation uses C# String.Format method in which the input claims act as string format {0} and {1} parameter which supports string claims transformation expressions.
• Please find the below links for more information: -
https://learn.microsoft.com/en-us/azure/active-directory-b2c/string-transformations
https://learn.microsoft.com/en-us/azure/active-directory-b2c/claimstransformations

Identity Experience Framework and social accounts

I developed a policy, which allows to login with username and password (B2C user) or using the Microsoft account, connecting the AD as an OpenId Identiy provider. It works fine, but when I login with my Microsoft account, the email is not set:
I guess I have to set something in the following snippet:
<TechnicalProfile Id="AAD-UserWriteUsingAlternativeSecurityId">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
<Item Key="UserMessageIfClaimsPrincipalAlreadyExists">You are already registered, please press the back button and sign in instead.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="AlternativeSecurityId" PartnerClaimType="alternativeSecurityId" Required="true" />
</InputClaims>
<PersistedClaims>
<!-- Required claims -->
<PersistedClaim ClaimTypeReferenceId="alternativeSecurityId" />
<PersistedClaim ClaimTypeReferenceId="userPrincipalName" />
<PersistedClaim ClaimTypeReferenceId="mailNickName" DefaultValue="unknown" />
<PersistedClaim ClaimTypeReferenceId="displayName" DefaultValue="unknown" />
<!-- Optional claims -->
<PersistedClaim ClaimTypeReferenceId="otherMails" />
<PersistedClaim ClaimTypeReferenceId="givenName" />
<PersistedClaim ClaimTypeReferenceId="surname" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
I tried to add <PersistedClaim ClaimTypeReferenceId="email"/> to the PersistedClaims, but I get a Bad Request with message: One or more property values specified are invalid.
On this page I see that the correct definition is mail. I also tried to define a new ClaimType and use a ClaimsTransformation to get the value from otherMails, but the field is not set.
This will not work:
<PersistedClaim ClaimTypeReferenceId="email"/>
Because there is no such attribute on the user object called "email".
What you can do is this:
<PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress"/>
This will then show up under userPrincipalName in the Azure Portal. It will also mean that the user cannot then sign up with that Email for a local account, essentially you create a "dual/linked account", albeit without a password.
The user then needs to reset the password on this account to access it with Username and Password, if they ever delete their Social Account at Facebook for example.
Another option is to write to otherMails
<PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="otherMails"/>
This might need to be an array, so use a claimTransform to build an Array first before writing it.
Do not write the email to the mail attribute, the supported attributed for B2C accounts are here.

How to read user by claim in AAD B2C custom policy?

Is there any option to read azure AD B2C tenant users (local or social) by a claim value such as email address or by custom claim such as extension_company_user_id etc.
Actually I need something like below:
<TechnicalProfile Id="AAD-UserReadUsingClaimValue">
<Metadata>
<Item Key="Operation">Read</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="company_user_id" PartnerClaimType="extenstion_company_user_id" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="country" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="extension_company_user_id" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
You cannot do that because the claim has no uniqueness constraint. You need to read users by the signInNames property or the userIdentities/alternativeSecurityId property, these have a uniqueness constraint.
This would be valid:
<InputClaim ClaimTypeReferenceId="userEnteredEmail" PartnerClaimType="signInNames.emailAddress" />
If extenstion_company_user_id is going to be unique, and a user identifier, write it to signInNames.companyUserId. AAD B2C will automatically register the schema extension when doing so in custom policy.
Then you can read a user like this:
<InputClaim ClaimTypeReferenceId="company_user_id" PartnerClaimType="signInNames.companyUserId" />

Azure B2C Account Creation via custom policy AAD Write creates the user as locked

I am trying to create a sign in flow with just in time migration. Upon verifying user via REST API I need to write the user to directory via AAD Write validation technical profile. It works fine but it creates the accounts as "locked" - when I go in to portal I see "Block Sign in= YES"
What could be the reason? I use the same technical profile and it works fine on the sign up flow
Below is the technical profile I execute within the local sign in technical profile
<TechnicalProfile Id="AAD-UserWriteUsingLogonEmail-Migrate">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<PersistedClaims>
<!-- Required claims -->
<PersistedClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.emailAddress" />
<PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" />
<PersistedClaim ClaimTypeReferenceId="displayName" DefaultValue="unknown" />
<PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration" />
<!-- Optional claims. -->
<PersistedClaim ClaimTypeReferenceId="givenName" />
<PersistedClaim ClaimTypeReferenceId="surname" />
<PersistedClaim ClaimTypeReferenceId="jobTitle" />
<PersistedClaim ClaimTypeReferenceId="extension_Phone" />
<PersistedClaim ClaimTypeReferenceId="extension_companyId" />
<PersistedClaim ClaimTypeReferenceId="extension_companyName" />
<PersistedClaim ClaimTypeReferenceId="extension_communicationOptin" />
<PersistedClaim ClaimTypeReferenceId="streetAddress" />
<PersistedClaim ClaimTypeReferenceId="city" />
<PersistedClaim ClaimTypeReferenceId="state" />
<PersistedClaim ClaimTypeReferenceId="postalCode" />
<PersistedClaim ClaimTypeReferenceId="country" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
...
<PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" />
...
It turns out the "newpassword" claim is not available since the claim I was collecting during the self assertion sign in step is "password". Adding the "PartnerClaimType=password" did not copy the "password" into the "newpassword" claim as well.
The solution was to simply rename "newpassword" to "password" and the flow worked fine
<PersistedClaim ClaimTypeReferenceId="password" />
As Chris commented when the password is not there AAD Write still creates the user but creates it as disabled - which in my opinion should not happen and B2C should give a meaningful error message

Resources