Azure AD B2C - "emails" claim in custom policy - azure-ad-b2c

I'm looking for a way to add an emails claim (collection of emails) to a custom policy for Azure AD B2C. This application claim is available from the Azure Portal directly but I cannot find a way to implement this in a custom policy which I need to create.
What I want to achieve is to have Azure AD B2C authentication for my WebApp users and Azure AD authentication as custom Authentication Provider for employees so It means I will need to add emails claim twice - for Local accounts and for Azure AD.
I followed this guide to make custom policy so I've added a new ClaimsProvider to TrustFrameworkExtensions.xml file.
When I download Sign Up & Sign In policy created in Azure Portal then I can see the following Output Claim:
<OutputClaim ClaimTypeReferenceId="emails" />
I tried to put that line to my custom policy but it does not return emails claim.
Any ideas?

I couldn't find an answer this either - it looks like the "emails" claim is being returned by a custom OutputClaimsTransformation, the configuration of which isn't available in the samples.
I did find the this answer on SO which helped, but it covers updated the "otherMails" claim for NEW users and I had existing users on the basic policies who I couldn't update in that way.
It seems that emails is being populated by concatenating "otherMails" (in the case of social signups) with the first entry in the "signInNames" array.
I ended up doing the following to get the "emails" claim dynamically created.
Create two new ClaimTypes in TrustFrameworkExtensions.xml
<ClaimType Id="emails">
<DisplayName>Emails</DisplayName>
<DataType>stringCollection</DataType>
<UserHelpText>User's email addresses</UserHelpText>
</ClaimType>
<ClaimType Id="firstOtherMail">
<DisplayName>First Other mail</DisplayName>
<DataType>string</DataType>
<UserHelpText>Other Mail</UserHelpText>
</ClaimType>
Create 3 new ClaimsTransformations in TrustFrameworkExtensions.xml
<ClaimsTransformation Id="GetFirstOtherMail" TransformationMethod="GetSingleItemFromStringCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="otherMails" TransformationClaimType="collection" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="firstOtherMail" TransformationClaimType="extractedItem" />
</OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="CopyFirstOtherMailToEmail" TransformationMethod="AddItemToStringCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="firstOtherMail" TransformationClaimType="item" />
<InputClaim ClaimTypeReferenceId="emails" TransformationClaimType="collection" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emails" TransformationClaimType="collection" />
</OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="CopySignInNamesEmailToEmails" TransformationMethod="AddItemToStringCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInNames.emailAddress" TransformationClaimType="item" />
<InputClaim ClaimTypeReferenceId="emails" TransformationClaimType="collection" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emails" TransformationClaimType="collection" />
</OutputClaims>
</ClaimsTransformation>
Create a new TechnicalProfile in TrustFrameworkExtensions.xml:
<!-- The following technical profile is used to create the emails collection after user authenticates. -->
<TechnicalProfile Id="AAD-UserCreateEmailsClaim">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emails" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="GetFirstOtherMail"/>
<OutputClaimsTransformation ReferenceId="CopySignInNamesEmailToEmails"/>
<OutputClaimsTransformation ReferenceId="CopyFirstOtherMailToEmail"/>
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
Add a new OrchestrationStep to the SignUpOrSignIn UserJourney just before the last step (SendClaims) in SignUpOrSignIn
<OrchestrationStep Order="8" Type="ClaimsExchange">
<ClaimsExchanges>
<!-- create the emails claim combining signInNames and otherMails -->
<ClaimsExchange Id="AADUserCreateEmailsClaim" TechnicalProfileReferenceId="AAD-UserCreateEmailsClaim" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
Edit the PolicyProfile TechnicalProfile and add the OutputClaim:
<OutputClaim ClaimTypeReferenceId="emails" />

I took a much simpler route, and just added the following output claim in the SignInSignUp.xml (I left the existing email output claim in, that anyway gets populated only for social sign-ins)
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />

Related

Is there a way for use UserName instead of email in Password Reset flow in Azure AD B2C?

I'm customizing the PasswordReset flow in azure ad b2c using custom policies, but i can't find a way for use UserName instead Email for restore password. I've tried to use input signInName instead of email in the technical profile AAD-UserReadUsingEmailAddress, but still shows the email in the form.
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.userName" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<!-- Optional claims -->
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="accountEnabled" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertAccountEnabledIsTrue" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
It's possible to do this with userName?
This technical profile is the implementation to READ the account. What you are trying to achieve is to show the Username text box first and foremost. To display something on screen, you need to modify a selfAsserted technical profile.
The key is to change the operating mode to Username in the selfAsserted technical profile which asks the user for their identifer (which from the starter pack is: LocalAccountDiscoveryUsingEmailAddress), the latest key name is setting.operatingMode, reference here, set it to username. Then the textbox validation will be for username.
There is a complete sample here, and you can quick deploy using this link.

How to fetch only one user from Azure AD B2C using a custom policy with different user matches

I have a validation technical profile that checks if there is an existing user with the same company custom attribute during sign up and returns an error. It works great if there is just one user that matches the company name but throws an error when there are multiple which is possible.
Exception is application insight is:
Only one retrieved principal can be returned.
<TechnicalProfile Id="AAD-CheckDuplicateCompany">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_company" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertObjectIdAADUserObjectIdNotFoundAreEqual" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<ClaimsTransformation Id="AssertObjectIdAADUserObjectIdNotFoundAreEqual" TransformationMethod="AssertStringClaimsAreEqual">
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" TransformationClaimType="inputClaim1" />
<InputClaim ClaimTypeReferenceId="objectIdNotFound" TransformationClaimType="inputClaim2" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringComparison" DataType="string" Value="ordinalIgnoreCase" />
</InputParameters>
</ClaimsTransformation>
AAD-CheckDuplicateCompany is used as a validation technical profile in LocalAccountSignUpWithLogonEmail, so it will not insert the user if there is at least one user that exists with the same company attribute. Is there a way to get just one user match?
Not possible. It’s only supported to use an input claim that uniquely identifies an account.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#inputclaims
You need to make your own REST API call and perform your custom logic there.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-rest-api-claims-exchange?pivots=b2c-custom-policy

Find existing user by email in Azure Active Directory B2C Custom Policy

I'm creating a custom policy in Azure AD B2C to let invited users sign in via another Azure AD (or even ADFS). The problem I have is that a new user gets created when they sign in (rather than the invited user). I have found that I have been trying to find existing user in my AAD using alternativesecurityid or objectid and both of these are not matching. So I think I need to find an existing user by email, and not any IDs. This too, is not working, because I can see my invited user's email is sitting in otherMails and mail properties (via GraphAPI), and apparently I cannot query B2C via these fields.
<TechnicalProfile Id="AAD-ReadUserByEmail">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailFromSocialAccount" PartnerClaimType="mail" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
Only <InputClaim ClaimTypeReferenceId="emailFromSocialAccount" PartnerClaimType="signInNames.emailAddress" Required="true" /> passes validation, but this field is not having any data.
How do I find the invited user?

Azure Active Directory B2C SAML Integration

I'm following the official MS guide Set up sign-in with a Salesforce SAML provider by using custom policies in Azure Active Directory B2C. I have completed all the configuration but when I try to run the application I receive a strange error which states Claim with id "userId" already exists in the claims collection.
I was searching for userId in my custom policies:
TrustFrameworkBase.xml: in this file userid is NOT declared, a claim called issuerUserId is declared in ClaimsSchema as datatype string and it is used in the following claim transformation:
<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>
issuerUserId is then used in Facebook claim provider:
<ClaimsProvider>
<Domain>facebook.com</Domain>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="id" />
...
</OutputClaims>
...
</ClaimsProvider>
TrustFrameworkExtension.xml: the claim userid is the output claim of the claim provider I'm using (salesforce):
<ClaimsProvider>
<Domain>salesforce</Domain>
...
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="userid"/>
</OutputClaims>
...
</ClaimsProvider>
SignUpOrSigninSalesforce.xml: the claim userid is the output claim of the relying party I'm using (salesforce):
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpSignInSalesforce" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" />
...
</OutputClaims>
</TechnicalProfile>
</RelyingParty>
Based on the error claim userid is defined twice, but I don't find a double definition, do you have any clue?
Thanks.
After 3 days of exhausting troubleshooting I have found five minutes ago the error.
In my case I did not follow exactly microsoft steps, I wrongly changed SignUpSignInSalesforce UserJourney in the orchestration step 3 in TrustFrameworkExtensions.xml:
<UserJourney Id="SignUpSignInSalesforce">
....
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="salesfoce" />
</ClaimsExchanges>
</OrchestrationStep>
...
</UserJourney>
The wrong value is TechnicalProfileReferenceId. When I changed the value from salesforce to AAD-UserReadUsingAlternativeSecurityId-NoError the solution started working.
What I have done it was re-reading the documentation of Microsoft step by step looking for mistakes.

Linking multiple social accounts to Azur B2C local account through custom policies

I am trying to link Azure B2C local account with multiple (Facebook, Google) social providers.
I've successfully setup the sample here.
But it always writes only one social provider to the local account. If I first link Facebook and then try to link also Google, Facebook userIdentities item is overwritten. And vice versa.
I've tried to replace the Protocol with AAD-UserWriteProfileUsingObjectId but user object is not updated.
I think there might be an issue with the Protocol, which only overrides and does not append.
Only one social provider is included:
"userIdentities": [
{
"issuer": "google.com",
"issuerUserId": "MDExMDk2RTg3NTM0OTk3Mjk5OTI3"
}
],
Here is a user journey part that updates the social account for a local user
<!-- Demo: Updates the social account for a user, identified by the object
identifier for the user, in the Azure AD identity store.
An error is raised if the user does not exist. -->
<OrchestrationStep Order="6" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AAD-UserWriteUsingAlternativeSecurityId-ThrowIfNotExists" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId-ThrowIfNotExists" />
</ClaimsExchanges>
</OrchestrationStep>
And here is the corresponding Technical profile:
<TechnicalProfile Id="AAD-UserWriteUsingAlternativeSecurityId-ThrowIfNotExists">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId" />
<!-- Demo: Persist the alternativeSecurityId claim -->
<PersistedClaim ClaimTypeReferenceId="alternativeSecurityId" />
</PersistedClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
But the user object should contain both, Google and Facebook:
"userIdentities": [
{
"issuer": "google.com",
"issuerUserId": "MDExMDk2RTg3NTM0OTk3Mjk5OTI3"
},
{
"issuer": "facebook.com",
"issuerUserId": "KVExMDk2RTg3NTM0OTk3Mjk5OTI4"
}
],
You can add to and remove from the userIdentities property using the social accounts claims transformations.
Firstly, declare an alternativeSecurityIds claim:
<ClaimType Id="alternativeSecurityIds">
<DisplayName>Alternative Security IDs</DisplayName>
<DataType>alternativeSecurityIdCollection</DataType>
</ClaimType>
Next, add the alternativeSecurityIds claim as an output claim to the AAD-UserReadUsingObjectId technical profile, to get the existing user identities for a user:
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="alternativeSecurityIds" />
</OutputClaims>
</TechnicalProfile>
Next, declare an AddAlternativeSecurityIdToAlternativeSecurityIds claims transformation, to add a new alternative security ID item to an existing alternative security ID collection:
<ClaimsTransformation Id="AddAlternativeSecurityIdToAlternativeSecurityIds" TransformationMethod="AddItemToAlternativeSecurityIdCollection">
<InputClaims>
<InputClaim ClaimTypeReferenceId="alternativeSecurityId" TransformationClaimType="item" />
<InputClaim ClaimTypeReferenceId="alternativeSecurityIds" TransformationClaimType="collection" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="alternativeSecurityIds" TransformationClaimType="collection" />
</OutputClaims>
</ClaimsTransformation>
Next, add the AddAlternativeSecurityIdToAlternativeSecurityIds claims transformation as an output claims transformation to each of the social account claims providers, to add the new user identity (which is created by the CreateAlternativeSecurityId claims transformation) to the existing user identities (which was retrieved by the AAD-UserReadUsingObjectId technical profile) for the user:
<ClaimsProvider>
<Domain>facebook.com</Domain>
<DisplayName>Facebook</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Facebook-OAUTH">
<OutputClaimsTransformations>
...
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
<OutputClaimsTransformation ReferenceId="AddAlternativeSecurityIdToAlternativeSecurityIds" />
</OutputClaimsTransformations>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Finally, rather than the alternativeSecurityId claim, refer to the alternativeSecurityIds claim, as a persisted claim in the AAD-UserWriteUsingAlternativeSecurityId-ThrowIfNotExists technical profile, to update the existing user identities for the user:
<TechnicalProfile Id="AAD-UserWriteUsingAlternativeSecurityId-ThrowIfNotExists">
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId" />
<PersistedClaim ClaimTypeReferenceId="alternativeSecurityIds" />
</PersistedClaims>
</TechnicalProfile>

Resources