Azure B2C - Accept query params into OAuth2 JWT - azure

I was curious if it was possible to read query parameters when requesting an OAuth2 token through Azure?
Essentially, when making a test call with a policy that I created, I would like to have an additional query parameters read from the call and the orchestration (user journey) steps should read these values and inject that value into a custom claim (for the JWT or ID token).
I know from the follow links that it may* be possible with Azure B2C service? But I can't find any good concrete examples.
Sign-up policy - Set user attributes through code
Add Custom Attribute Not Used in Sign-Up nor Edit Policy
How can I return the PolicyId Claim after executing my Custom SignUpSignIn policy?
How do i include email in the redirect to AZURE AD B2C
I then proceeded in trying a bunch of configurations out but there are so many options to choose from, I don't know which to choose. In addition, I haven't been able to find any Azure docs that describe the options used when configuring these policies. In any case, here is what I have.
I downloaded the TrustFrameworkBase.xml and TrustFrameworkExtensions.xml from here. I got this Github link from this Azure doc, which I also followed the steps on setting up policy keys and added an app registration with delegated permissions. For my relying party configuration, I simply made a custom policy through the Azure B2C portal and downloaded it as a starting point to explore what it looks like in a basic form.
Here is my custom claim added to the base policy within the ClaimsSchema tag. extension_Test is the claim where I want to inject the value from a query param:
<ClaimType Id="extension_Test">
<DisplayName>Test value</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="extension_Test" />
<Protocol Name="OpenIdConnect" PartnerClaimType="extension_Test" />
</DefaultPartnerClaimTypes>
<UserInputType>Readonly</UserInputType>
</ClaimType>
</ClaimsSchema>
In the same base policy, here's the userjourney that I added for SignIn:
<UserJourney Id="SignIn">
<OrchestrationSteps>
<OrchestrationSteps>
<!-- The following orchestration step is always executed. -->
<OrchestrationStep Order="1" Type="ClaimsProviderSelection" ContentDefinitionReferenceId="api.idpselection.signupsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="LocalAccountRegistrationExchange" />
</ClaimsProviderSelections>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountRegistrationExchange" TechnicalProfileReferenceId="LocalAccount-Registration-VerifiedEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
</UserJourney>
Here's my relying config XML:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_Test" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="extension_Test" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
I think uploaded the base, extensions and RP policies XML files in that order. The GET request that I'm sending looking like this (got from the "Run now" button for the custom policy):
https://login.microsoftonline.com/<TENANT>/oauth2/v2.0/authorize?p=B2C_1A_test&client_id=<TENANTID>&nonce=defaultNonce&redirect_uri=http%3A%2F%2Flocalhost%2Fredirect&scope=openid&response_type=id_token&prompt=login&extension_Test=aaa
Any help would be greatly appreciated, thanks! Or Azure documents that explain more options within these config files - as in what does CpimIssuerTechnicalProfileReferenceId="JwtIssuer" mean? Or AzureFunction-WrapWebHook mean?

You are close.
An end-to-end example of inputting a claim to a journey, and then using it in this journey (e.g. pre-conditions or storage) as well as outputting it from the journey, can be found in this "Implementing an invitation flow" document (which I was author of).
The high-level solution is:
1) At design-time, configure the relying party policy with the input claim.
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<InputTokenFormat>JWT</InputTokenFormat>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_MySharedSecret" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_Test" />
</InputClaims>
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="extension_Test" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
You must create a policy key (in the above example, this is called "MySharedSecret", but it can be called anything) containing a shared secret that is known to the application that is invoking this policy (where the client secret for this application can be this shared secret).
2) At runtime, create a self-issued JWT containing the input claim, sign this JWT with the shared secret, and then add the JWT to the authentication request using the "client_assertion_type" and "client_assertion" parameters.
The code example for this can be found in the Wingtip sample.
An example of the authentication request is:
https://login.microsoftonline.com/b2ctechready.onmicrosoft.com/oauth2/v2.0/authorize?p=b2c_1a_invitation&...&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJhbGci...7m9s&state=CfDJ8EPk...Et0w

Related

AAD B2C SignUpOrSignIn with both verified Email AND verified Phone

Usually we have a signup/signin with either email or phone, but I have a requirement to do signup/signin requiring both verified email and verified phone number as well.
I tried to set this up using default 'User Flows', with MFA for phone/sms enabled, but the number is not accessible via the graph API call for phoneMethods, if it were, I could have also copied the number to the profile (which would mean that it was a verified phone number). Further on the same, enabling Authentication Method, OR even updating (separating phone number with a space char for code and number) the phone number on the profile page causes the phone number to appear via the phoneMethods graph API call, but not before then.
Can someone who's done this before share some insights on their approach or probably share the custom xml policy if possible? I did look into the github sample policies [https://github.com/azure-ad-b2c/samples/tree/master/policies], but I guess there is none with BOTH.
[Edit on the above]
Based on Wes' input, I ended up creating a technical profile as per below and used it into my UserJourney -> Orchestration step, post the PhoneFactor-InputOrVerify step as per below (apologies on the formatting):
<TechnicalProfile Id="AAD-UserWritePhoneNumberUsingObjectId">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="objectId" />
<PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="extension_PhoneNumber" />
</PersistedClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="extension_PhoneNumber"/>
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
Ofcourse, the "extension_PhoneNumber" was setup in ClaimsSchema as per below:
<ClaimType Id="extension_PhoneNumber">
<DisplayName>Phone Number</DisplayName>
<DataType>string</DataType>
</ClaimType>
Hope this helps someone...
Cheers! ;)
PS: Appreciate any updates to any of the above comments to make it correct/better.
I have done something somewhat similar to this but in my case I verified a new MFA number as my flow resets the users MFA number, but hopefully this will give you an idea. As far as an approach, you could take the standard login flow from the samples -> add verify the email step -> add verify phone number step. I do not know your level of experience with custom policies, but I believe using the sample custom policy for sign in as an initial template will be your best starting place. Then add the two orchestration steps for verification in. Here are slightly modified excerpts from my implementation.
<OrchestrationSteps>
<OrchestrationStep ContentDefinitionReferenceId="api.signin" Order="1" Type="CombinedSignInAndSignUp">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="EmailVerifyOnSignIn" TechnicalProfileReferenceId="EmailVerifyOnSignIn" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewPhoneFactor" TechnicalProfileReferenceId="PhoneFactor-Verify" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep CpimIssuerTechnicalProfileReferenceId="JwtIssuer" Order="5" Type="SendClaims" />
Steps 1,2 and 5 should be able to be copied directly from the samples with a small change of outputting the email as a read only claim to be used to verify(see below steps).
Your local account signin will need to add a transform that generates the readonly email from sign in name if you choose this route.
So in your local account signin you will have
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopySignInNameToReadOnly" />
</OutputClaimsTransformations>
just after your output claims and the transform that is being used will be as follows:
<ClaimsTransformation Id="CopySignInNameToReadOnly" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
Be sure to define your readonly claim in the claimstypes section:
<ClaimType Id="readOnlyEmail">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<UserHelpText />
<UserInputType>Readonly</UserInputType>
</ClaimType>
This is just a simple claim copy transformation.
Steps 3 and 4, which do each of the verification, have the technical profiles similar to below:
<TechnicalProfile Id="EmailVerifyOnSignIn">
<DisplayName>EmailVerifyOnSignIn</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.selfasserted.EmailPage</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlyEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" PartnerClaimType="Verified.Email" />
</OutputClaims>
</TechnicalProfile>
This will verify the email that was used to sign in, then the following orchestration step will be to verify the MFA.
You will have to make changes to this next technical profile, but the basic idea will be the same as with email, here is a starting point :
<TechnicalProfile Id="PhoneFactor-Verify">
<DisplayName>PhoneFactor</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.PhoneFactorProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
The end result would be:
User logs in with email and password
User clicks button to verify the email used during login
User verifies phone number
Claims are sent and journey completes

Azure AD B2C Sign Up with Email Invitation Does Not Show Sign Up Page

I am incorporating a Sign Up with Email Invitation flow in my project by following this Azure AD B2C sample from microsoft:
https://github.com/azure-ad-b2c/samples/tree/master/policies/invite
For test reasons I am setting the redirect_uri parameter of the invitation URL to https://jwt.ms and my expectation for the workflow is:
Clicking on the invite URL takes me to b2clogin
Azure B2C validates the ID hint token
I land on the Sign Up page with pre-populated values in the ID token hint
Upon a successful sign up, I am redirected to https://jwt.ms
My expectation, however, is not met and upon clicking the invite URL, I immediately land on the https://jwt.ms with a JWT token containing the invitation number (more details below) and the object ID (sub) of one of the previously created profiles in AD, plus the standard claims like exp, aud, etc.
I suspect that there is a gap in my understanding of how the invite workflow function. What areas of code/policies should I pay attention and modify to ensure a successful invitation sign up?
Some extra details:
I am including an invitation number in the ID token hint and NOT an email, therefore the ReadOnlyEmail is replaced with InvitationNumber throughout the custom policy.
I have copied fields from my normal sign up policy to the invitation policy, expecting that he user should be able to sign up with any emails they like as long as it's validated by B2C (hence the "False" is removed from the sample technical profile for invitation sign up)
The invitation number is also set as an output claim for my app to process it once the JWT token is received from B2C.
The invitation policy uses the same policy base as my normal sign in/up.
In the shared policy base, I have added a new claims provider for ID token hint validation next to my normal JwtIssuer which references the signing certificate that my app uses to sign the ID token hint and use it in the last step of the SignUpInvitation user journey. I am not sure this is the right thing, but once I use the JwtIssuer, I get an error in B2C that it cannot verify the signature of ID token hint.
The technical profile for sign up is as follows, and it is being called from the user journey:
<TechnicalProfile Id="LocalAccountSignUpWithInvitationToken">
<DisplayName>Email signup</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyInvitationToken" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_InvitationToken" />
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<!-- Optional claims, to be collected from the user -->
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
<OutputClaim ClaimTypeReferenceId="extension_InvitationToken" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
7. The user journey is:
<UserJourneys>
<UserJourney Id="SignUpInvitation">
<OrchestrationSteps>
<!--Read the input claims from the id_token_hint-->
<OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />
<!-- Check if user tries to run the policy without invitation -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>extension_InvitationToken</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Self-asserted sign-up page -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSignUpWithInvitationToken" TechnicalProfileReferenceId="LocalAccountSignUpWithInvitationToken"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Issue an access token-->
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIdTokenHintValidator"/>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>
</UserJourneys>
Change <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" /> to <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />. You are skipping the page due to SSO it seems.
https://github.com/azure-ad-b2c/samples/blob/master/policies/invite/policy/SignUpInvitation.xml#L100

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