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

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>

Related

Multi tenant Azure B2C Login - how to get external user email address

I am struggling to figure out how to configure Azure B2C for multi-tenant authentication, in particular getting access to the email address of a user that is logging in via an external Azure AD (we're interested in allowing our customers to log in either via a "Local Account" (email address, managed by B2C) or their own Azure AD).
A key part of the issue I am trying to result is the passing of the logged in users email address through to a REST endpoint where our application needs to do some things internally to inject additional application specific claims, which are used later on. Apart from our REST endpoint receiving the email address, everything else is working.
I've got a "Common AAD" technical profile setup like this:
<TechnicalProfile Id="Common-AAD">
<DisplayName>Work Account</DisplayName>
<Description>Login with your Work Account</Description>
<Protocol Name="OpenIdConnect"/>
<Metadata>
<Item Key="METADATA">https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration</Item>
<Item Key="client_id">my_client_id</Item>
<Item Key="response_types">code</Item>
<Item Key="scope">openid email profile</Item>
<Item Key="response_mode">form_post</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="DiscoverMetadataByTokenIssuer">true</Item>
<Item Key="ValidTokenIssuerPrefixes">https://login.microsoftonline.com/</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_AADAppSecret"/>
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
<OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid"/>
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="signInName" />
<OutputClaim ClaimTypeReferenceId="otherMails" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="upn" PartnerClaimType="upn" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
</TechnicalProfile>
In the orchestration, I am instructing B2C to pass in a bunch of these claims to an application-hosted REST API so that we can do our internal processing:
<TechnicalProfile Id="REST-GetProfile-Dev">
<DisplayName>Do some custom logic</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://the-endpoint.com</Item>
<Item Key="AuthenticationType">None</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="sub" />
<InputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<InputClaim ClaimTypeReferenceId="userPrincipalName" />
<InputClaim ClaimTypeReferenceId="displayName" />
<InputClaim ClaimTypeReferenceId="otherMails" />
<InputClaim ClaimTypeReferenceId="upnUserName" />
<InputClaim ClaimTypeReferenceId="alternativeSecurityId" />
<InputClaim ClaimTypeReferenceId="upn" />
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="socialIdpUserId" />
<InputClaim ClaimTypeReferenceId="identityProvider" />
<InputClaim ClaimTypeReferenceId="authenticationSource" />
</InputClaims>
<OutputClaims>
<!-- bunch of app specific claims -->
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
However, I can never seem to get an email address, or anything that contains the email address of the logged in user passing through.
I've tried to track through the processing that's defined in the Custom Policy XML files, and it's challenging. To be honest, I've been researching this and have tried adding all kinds of additional claims to outputs from various steps, but it's just not working for me.
Any help in detailing how to get the email address of a user logged in via an external Azure AD passed into a REST orchestration step would be much appreciated.
Thanks.
** Big Edit **
In response to Jas Suri, I have reset everything, applied the changes below as suggested, but am still not seeing this work.
Here's my TrustFrameworkBase.xml:
TrustFrameworkBase.xml
Here's my TrustFrameworkExtensions.xml:
TrustFrameworkExtensions.xml
Here's my Relying Party (SignInSignUpMulti.xml) file:
SignInSignUpMulti.xml
Now, looking at my scenarios:
When I sign in using a "local" account, I see this type of information pass through to my rest endpoint during the user journey:
{
"objectId": "1e91bfba-17a1-43b6-a451-9896fc3c1061",
"signInNames.emailAddress": "email#example.com",
"displayName": "User DispName",
"signInName": "email#example.com",
"authenticationSource": "localAccountAuthentication"
}
That's perfect. I can take this information and gather additional claims to return and all works exactly as I want.
When I sign in as an AD Account that's attached to my org, I get this:
{
"objectId": "a_guid",
"sub": "Not supported currently. Use oid claim.",
"userPrincipalName": "cpim_a_guid#TENANT.onmicrosoft.com",
"displayName": "ThisIs Correct",
"upnUserName": "14218711-5dd1-4a81-8e04-77bd08298aaf",
"alternativeSecurityId": "{\"type\":6,\"identityProvider\":\"https://login.microsoftonline.com/a_guid/v2.0\",\"key\":\"a_key\"}",
"identityProvider": "https://login.microsoftonline.com/a_guid/v2.0",
"authenticationSource": "socialIdpAuthentication"
}
I am missing an email (or the users Sign In) address.
And, the same happens when I try to sign in as an external AD:
{
"objectId": "a_guid",
"sub": "Not supported currently. Use oid claim.",
"userPrincipalName": "cpim_a_guid#TENANT.onmicrosoft.com",
"displayName": "ThisIs Correct",
"upnUserName": "9c865de4-2898-4b18-998b-7fa151f6623d",
"alternativeSecurityId": "{\"type\":6,\"identityProvider\":\"https://login.microsoftonline.com/a_guid/v2.0\",\"key\":\"a_key\"}",
"identityProvider": "https://login.microsoftonline.com/a_guid/v2.0",
"authenticationSource": "socialIdpAuthentication"
}
If I can work out how to pass through the email address or signin address, then I would be extremely happy.
Event during debugging, if I cause the user to be signed in anyway, I inspect the User.Identity, and while I see claims that my rest api is returning during the journey, I still don't have any claim that resembles the email address I am expecting (hoping) to see.
I can definitely work either way - an email address passed to the REST API, or the email address appearing in the final claim set that the application receives.
Any assistance would be much appreciated.
From Azure AD, all users will come back with a unique_name claim, which is the UPN in their Azure AD. You could also rely on this. If you rely on the email claim from AAD, it will only be present if the user has an Exchange Online inbox. You also have to set it up as an optional Azure AD claim in the AAD Multi Tenant App registration.
Usually the UPN and Email are the same in an Azure AD. So in the AAD technical profile, you could add this output claim to capture the AAD UPN:
<OutputClaim ClaimTypeReferenceId="aadUPN" PartnerClaimType="unique_name"/>
Then in the relying party secion, add this output claim:
<OutputClaim ClaimTypeReferenceId="aadUPN" PartnerClaimType="UPNfromAAD"/>

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.

Azure Ad b2c: Get email in Claims after successfully Signin in azure ad b2c

I am using starter pack of custom polices with SocialAndLocalAccounts pack.
It is working fine for me.
But I am facing one issue.I need to get email as claim after successfully login.
I am getting email as claim, once user has been been signed-up and redirects back immediately to application.
but I am not getting it when a user simply signs-in.
How can I get that?
where do I need to write an Output Claim to get the value of email in claim?
Kindly help me.
Thanks
For Chris Padgett's answer, you can add other emails (Alternate email) into the claim.
If you just want to add email claim from the SignIn name into the token, you can just take following steps:
Open your SignUporSignIn.xml file
Replace <OutputClaim ClaimTypeReferenceId="email" /> with <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" PartnerClaimType="email" />
Save this SignUporSignIn.xml file and upload it to Azure AD B2C to overwrite the policy.
Run the SignUporSignIn policy to test it.
Here is my test result, you can see the email claim in the token:
Hope this helps.
Following describes how you can save, load, and then issue the otherMails claim as emails from the sign-up/sign-in and password reset policies.
When writing a local account: You must create the otherMails claim from the email claim using the CreateOtherMailsFromEmail claims transformation and then persist the otherMails claim in the AAD-UserWriteUsingLogonEmail technical profile:
<TechnicalProfile Id="AAD-UserWriteUsingLogonEmail">
...
<IncludeInSso>false</IncludeInSso>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail" />
</InputClaimsTransformations>
<InputClaims>
...
</InputClaims>
<PersistedClaims>
...
<PersistedClaim ClaimTypeReferenceId="otherMails" />
</PersistedClaims>
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
...
</TechnicalProfile>
You must then pass the otherMails claim out from the LocalAccountSignUpWithLogonEmail technical profile that is invoked to register a local account:
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
</TechnicalProfile>
When writing a social account: The otherMails claim is already created from the email claim and then persisted in the AAD-UserWriteUsingAlternativeSecurityId technical profile.
You must then pass the otherMails claim out from the SelfAsserted-Social technical profile that is invoked to register a social account:
<TechnicalProfile Id="SelfAsserted-Social">
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
</TechnicalProfile>
When reading a local or social account: The otherMails claim is already read in the AAD-UserReadUsingObjectId, AAD-UserReadUsingEmailAddress, and AAD-UserReadUsingAlternativeSecurityId technical profiles.
You must then pass the otherMails claim out from the LocalAccountDiscoveryUsingEmailAddress technical profile that is invoked to recover a local password:
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="otherMails" />
</OutputClaims>
</TechnicalProfile>
To issue the otherMails claim as emails from the sign-up/sign-in and password reset policies: You must add the otherMails claim as <OutputClaim /> to the relying party policies:
<RelyingParty>
...
<TechnicalProfile Id="PolicyProfile">
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="emails" />
</OutputClaims>
</TechnicalProfile>
</RelyingParty>
Another option that is working for me was to extend AAD-UserReadUsingObjectId so that it copies the signInNames.emailAddress claim into email. That brought sign-in into alignment with our other journeys/sub-journeys for integrated sign-up, password reset, and social log-in -- which each populate email during first log-in/sign-up.
All I needed to do was add this to TrustFrameworkExtension.xml (under <ClaimsProviders>):
<ClaimsProvider>
<DisplayName>Azure Active Directory</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AAD-UserReadUsingObjectId">
<OutputClaims>
<OutputClaim
ClaimTypeReferenceId="email"
PartnerClaimType="signInNames.emailAddress"
/>
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>

Azure AD B2C - "emails" claim in custom policy

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" />

Azure AD B2C pre-populate a custom attribute in the SignUp policy

Does Azure AD B2C support pre-populating a custom attribute in the SignUp Policy when called from the Web application (ASP.Net MVC)?
We can create a custom SignUp attribute but we weren't able to find a specification in the documentation how to pass value to populate the custom attribute. If this is not supported out of the box, does anybody found a workaround?
Here are some more details for the context in case somebody has faced a similar scenario and found a useful solution:
We explore the options to solve the following scenario with Azure AD B2C: a registered user invites another person to signup to the application by sending an invitation email which has the url to the application’s login page along with a special invitation code(guid) as a query param, so it can click on the link and to be redirected to the Signup page. After the invited person creates an account, we need to use the code in order to associate the newly created user to the user who sent the invitation.
Currently this is implemented in the ASP.Net using the default identity provider (storing the user data in database with AspNet... tables). With replacing the local identity provider with the Azure AD B2C, we are loosing the context during the round-trip to the Azure AD B2C Signup page. The user clicks on the link on the email and gets to the SIgnUp page but the invitation code is not pre-populated.
A working sample of an invitation flow is here.
In the WingTipGamesWebApplication project, the InvitationController controller class has two action methods, Create and Redeem.
The Create action method sends a signed redemption link to the email address for the invited user. This redemption link contains this email address. It could also contain the invitation code.
The Redeem action method handles the redemption link. It passes the email address, as the verified_email claim in a JWT that is signed with the client secret of the Wingtip Games application (see the CreateSelfIssuedToken method in the Startup class in the WingTipGamesWebApplication project), from the redemption link to the Invitation policy. It could also pass the invitation code.
The Invitation policy can be found at here.
The Invitation policy declares the verified_email claim as an input claim:
<RelyingParty>
<DefaultUserJourney ReferenceId="Invitation" />
<TechnicalProfile Id="Invitation">
<InputTokenFormat>JWT</InputTokenFormat>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="WingTipGamesClientSecret" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_VerifiedEmail" />
</InputClaims>
</TechnicalProfile>
</RelyingParty>
The extension_verifiedEmail claim type, which is declared as a read-only field (so that it can't be modified by the end user), is mapped to the verified_email input claim:
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="extension_VerifiedEmail">
<DisplayName>Verified Email</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="verified_email" />
<Protocol Name="OpenIdConnect" PartnerClaimType="verified_email" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.wingtipb2c.net/identity/claims/verifiedemail" />
</DefaultPartnerClaimTypes>
<UserInputType>Readonly</UserInputType>
</ClaimType>
</ClaimsSchema>
</BuildingBlocks>
The Invitation user journey can be found in here.
The second orchestration step of the Invitation user journey executes the LocalAccount-Registration-VerifiedEmail technical profile:
<UserJourney Id="Invitation">
<OrchestrationSteps>
...
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
...
<ClaimsExchange Id="LocalAccountRegistrationExchange" TechnicalProfileReferenceId="LocalAccount-Registration-VerifiedEmail" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</UserJourney>
The LocalAccount-Registration-VerifiedEmail technical profile registers the local account with the verified email address:
<TechnicalProfile Id="LocalAccount-Registration-VerifiedEmail">
<DisplayName>WingTip Account</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.localaccount.registration</Item>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateEmailFromVerifiedEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_VerifiedEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_VerifiedEmail" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="displayName" Required="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="sub" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExists" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SSOSession-AzureActiveDirectory" />
</TechnicalProfile>
Before the local account is registered by the AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExists validation technical profile, the CreateEmailFromVerifiedEmail claims transformation copies the verified_email claim to the email claim:
<ClaimsTransformation Id="CreateEmailFromVerifiedEmail" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_VerifiedEmail" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
To save the invitation code against the local account, you must:
Add the "extension_InvitationCode" claim to the claims schema
Add it as an input claim to the Invitation policy
Add it as an input claim to the LocalAccount-Registration-VerifiedEmail technical profile
Add it as a persisted claim to the AzureActiveDirectoryStore-WriteUserByEmail-ThrowIfExist technical profile

Resources