Adding app insights logging inside AD B2C Display Controls (emailVerificationControl) - azure-ad-b2c

We implemented a custom email verification workflow through send grid and it works great. We want to add app insights logging to know how many are putting their email but not verifying it. Calling AppInsights-EmailVerificationSent Technical Profile in the display control below is giving me a "Unable to validate the information provided." error. How can I call this technical profile to log the action in app insights? Thanks very much!
<TechnicalProfile Id="AppInsights-Common">
<DisplayName>Application Insights</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.Insights.AzureApplicationInsightsProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="InstrumentationKey">xxxxx</Item>
<Item Key="DeveloperMode">true</Item>
<Item Key="DisableTelemetry ">false</Item>
</Metadata>
<InputClaims>
<!-- Properties of an event are added through the syntax {property:NAME}, where NAME is property being added to the event. DefaultValue can be either a static value or a value that's resolved by one of the supported DefaultClaimResolvers. -->
<InputClaim ClaimTypeReferenceId="EventTimestamp" PartnerClaimType="{property:EventTimestamp}" DefaultValue="{Context:DateTimeInUtc}" />
<InputClaim ClaimTypeReferenceId="PolicyId" PartnerClaimType="{property:Policy}" DefaultValue="{Policy:PolicyId}" />
<InputClaim ClaimTypeReferenceId="CorrelationId" PartnerClaimType="{property:CorrelationId}" DefaultValue="{Context:CorrelationId}" />
<InputClaim ClaimTypeReferenceId="Culture" PartnerClaimType="{property:Culture}" DefaultValue="{Culture:RFC5646}" />
</InputClaims>
</TechnicalProfile>
<TechnicalProfile Id="AppInsights-EmailVerificationSent">
<InputClaims>
<InputClaim ClaimTypeReferenceId="EventType" PartnerClaimType="eventName" DefaultValue="EmailVerificationSent" />
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="{property:UserId}" DefaultValue="NoValue" />
</InputClaims>
<IncludeTechnicalProfile ReferenceId="AppInsights-Common" />
</TechnicalProfile>
<DisplayControls>
<DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="email" Required="true" />
<DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
<Actions>
<Action Id="SendCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtp" />
**<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AppInsights-EmailVerificationSent" />**
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
**<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AppInsights-EmailVerified" />**
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>

Having had this exact problem recently the only way I found was to set up an API endpoint that would write custom events to App Insights, then use a REST technical profile to call that API from the DisplayControl.

Related

In Azure B2C Sign Up Flow, How to warn user email is in use when clicking 'Send Verfication Code' button

I have a B2C sign-up only custom policy, with a custom email display control. Currently, the user is notified AFTER validating the email, that the email is already in use. (profile and password information are collected on a later step)
Here is some relevant code to accomplish this:
<TechnicalProfile Id="PartnerSignUpVerifyEmailPage">
<DisplayName>Local Email Verification</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.partners.signUpVerifyEmailPage</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">A user with this email address already exists.</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GetLocalizedStringsForEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" DefaultValue="{OIDC:LoginHint}" AlwaysUseDefaultValue="true" />
</InputClaims>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="localizedSignUpEmailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<!-- this validation asserts the email provided isn't already in use -->
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists" />
</ValidationTechnicalProfiles>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="email" Required="true" />
<DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
<Actions>
<Action Id="SendCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp"/>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp"/>
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" />
<!-- blank the object id, in the case that it was used, so we can let the user change the email and retest -->
<OutputClaimsTransformation ReferenceId="SetObjectIdToNull" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
My goal is: Upon pressing the 'Send Verification Code' button, validate that the email is not already in use. Then, if the email is in use: put up an error message, don't send an email and don't progress the control.
I have tried 3 different approaches and none seem to do the job.
Approach 1: Validate, Error and Stop
Details:
At the top of my SendCode ValidationClaimsExchanges add a ValidationClaimsExchangeTechnicalProfile for AAD-UserReadUsingEmailAddress-RaiseIfExists
On the other ValidationClaimsExchangeTechnicalProfiles add ContinueOnError="false"
Results:
Displays an error to the user when using an email already in use
The control still prompts for sending a code
However, the other validation steps still execute and an email is still sent.
In this case, it looks like failing the validation and sending an error message is not the same kind of error that the ContinueOnError looks for.
Approach 2: Evaluate New Claim, Use Precondition
Details:
Create a boolean claim emailAlreadyRegistered
At the top of my SendCode ValidationClaimsExchanges add a ValidationClaimsExchangeTechnicalProfile for AAD-UserReadUsingEmailAddress-RaiseIfExists
In AAD-UserReadUsingEmailAddress-RaiseIfExists add a CompareClaims claims transformation to set emailAlreadyRegistered = True if the emails match
Add preconditions to the other ValidationClaimsExchangeTechnicalProfiles using ClaimEquals to see if emailAlreadyRegistered is True
Results:
Displays an error to the user when using an email already in use
The control still prompts for sending a code
However, the other validation steps still execute and an email is still sent.
I think in this case the precondition is not seeing the update to the claim (if I set the claim from my PartnerSignUpVerifyEmailPage technical profile then the preconditions seem to respect it).
Approach 3: Nested Validation Steps
Details:
Create a boolean claim emailAlreadyRegistered
Create a new claims transformation technical profile
The new profile takes the email as input and uses a claims transformation to set emailAlreadyRegistered in the input claim transformation
The new technical profile ALSO has 3 validationtechincalprofiles: AAD-UserReadUsingEmailAddress-RaiseIfExists, GenerateOtp, SendLocalizedOtp
In SendCode ValidationClaimsExchanges replace all ValidationClaimsExchangeTechnicalProfiles for the one new technical profile
Results:
Does not display the error when the email is in use
The control proceeds to the code verification step
No email is sent
In this case it looks like none of the nested validation technical profiles work at all.
Solution: (Credit: Christian Le Breton)
Create a boolean claim emailAlreadyRegistered
Split AAD-UserReadUsingEmailAddress-RaiseIfExists into two separate Technical Profiles
The first is the same as described without the output transformations
The second is a proper ClaimsTransformationProtocolProvider technical profile with the two OutputClaimsTransformations described above, plus a third in between them, This transformation sets emailAlreadyRegistered using a CompareClaims transform. This technical profile also outputs the claim emailAlreadyRegistered with DefaultValue = "false"
At the top of my SendCode ValidationClaimsExchanges add the two new Technical Profiles
Add preconditions to the other ValidationClaimsExchangeTechnicalProfiles using ClaimEquals to see if emailAlreadyRegistered is 'true'
Basically, what I failed to understand was that an AAD Technical Profile can't set new claims into the bag (I suppose) and a Claims Technical Profile can't read AAD, so these two activities needed to be separated, once I did that the 2nd approach worked.
Here are some of the tricky bits from the final result.
<DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="email" Required="true" />
<DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
<Actions>
<Action Id="SendCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt1" ContinueOnError="false" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2" ContinueOnError="false" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" ContinueOnError="false" >
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>emailAlreadyRegistered</Value>
<Value>true</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationClaimsExchangeTechnicalProfile>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp" ContinueOnError="false">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>emailAlreadyRegistered</Value>
<Value>true</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationClaimsExchangeTechnicalProfile>
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt1">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2">
<DisplayName>Check If Email Is Registered</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="objectIdNotFound" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailAlreadyRegistered" />
<OutputClaim ClaimTypeReferenceId="objectId" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="SetEmailAlreadyRegistered" />
<!-- blank the object id, in the case that it was used, so we can let the user change the emamil and retest -->
<OutputClaimsTransformation ReferenceId="SetObjectIdToNull" />
</OutputClaimsTransformations>
</TechnicalProfile>
I think approach 1 is likely your best approach, with some slight modifications. The main issue is that claims transformations need to be called from a ClaimsTransformations technical profile that is a validation technical profile, not by calling the ClaimsTransformations profile from within a validation technical profile.
Instead of trying to do everything in one step, it should be broken up into a few steps.
Have your user read technical profile just output the objectId and objectIdNotFound, without the claims transformation, and have your send code step use the read technical profile, the assert technical profile and the set to null technical profile as verification profiles in that order, prior to generating or sending the OTP.

How to update an Azure AD B2C Username sample for sending custom emails with SendGrid?

I'm using this policy for Username-based SUSI extended off of this starter pack for local account SUSI. But I had also changed that starter pack for custom verification emails with SendGrid according to the how-to found here. The requirements for login went from "just use B2C" to "login with a username and any number of users may share a verification email."
I know that TrustFrameworkExtensions_Username is extending TrustFrameworkExtensions from the local account starter pack. I can apply the same overrides from the "Local Account" ClaimsProvider over the LocalAccountSignUpWithLogonEmail and LocalAccountDiscoveryUsingEmailAddress TechnicalProfiles to the "Local Account SignIn" ClaimsProvider's LocalAccountSignUpWithLogonName and LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress TechnicalProfiles directly.
The policy inheritance looks like this:
LocalAccounts starter pack TrustFrameworkBase
|
v
LocalAccounts starter pack TrustFrameworkLocalization
|
v
LocalAccounts starter pack TrustFrameworkExtensions <- modified for custom emails via SendGrid
|
v
username-signup-or-signin sample TrustFrameworkExtenions_Username
| |
v v
username-signup-or-signin sample SignupOrSignin_Username and PasswordReset_Username
The display controls in local extensions from the how-to:
<DisplayControls>
<DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="email" Required="true" />
<DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
<Actions>
<Action Id="SendCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtp" />
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>
Tech profiles referenced from the display controls:
<ClaimsProvider>
<DisplayName>One time password technical profiles</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="GenerateOtp">
<DisplayName>Generate one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">GenerateCode</Item>
<Item Key="CodeExpirationInSeconds">1200</Item>
<Item Key="CodeLength">6</Item>
<Item Key="CharacterSet">0-9</Item>
<Item Key="ReuseSameCode">true</Item>
<Item Key="NumRetryAttempts">5</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otp" PartnerClaimType="otpGenerated" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="VerifyOtp">
<DisplayName>Verify one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">VerifyCode</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
<InputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpToVerify" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>RestfulProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SendOtp">
<DisplayName>Use SendGrid's email API to send the code the the user</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://api.sendgrid.com/v3/mail/send</Item>
<Item Key="AuthenticationType">Bearer</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="ClaimUsedForRequestPayload">emailRequestBody</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BearerAuthenticationToken" StorageReferenceId="B2C_1A_SendGridSecret" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GenerateEmailRequestBody" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailRequestBody" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The SUSI tech profile overrides in the local extensions:
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
<DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
<DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</DisplayClaims>
</TechnicalProfile>
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
I can get the sign-up part of the journey working in this manner - I could take the overrides in the extension changes found in the how-to here and add the DisplayClaims to the LocalAccountSignUpWithLogonName TechnicalProfile in
TrustFrameworkExtensions_Username.xml:
<TechnicalProfile Id="LocalAccountSignUpWithLogonName">
<DisplayName>User ID 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="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="signInName" Required="true" />
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
<DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
<DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" Required="true" />
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonName" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
From the OutputClaim with ClaimTypeReferenceId=email PartnerClaimType="Verified.Email" was removed. Not necessary for getting the display control actually used but for later use of the email claim in the claim bag in a subsequent orchestration step (was not set otherwise).
But when updating the TechnicalProfile for LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress in a similar way:
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<DisplayName>Reset password using username</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.localaccountpasswordchange1.1</Item>
<Item Key="AllowGenerationOfClaimsWithNullValues">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">An account could not be found for the provided user ID.</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
<Item Key="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<!-- Reduce the default self-asserted retry limit of 7 for the reset journey -->
<Item Key="setting.retryLimit">3</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<!-- using this requires removal of OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true", below
according to https://stackoverflow.com/a/73709880/2347078 -->
<!-- line is required for non-customized email journey presently -->
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<!-- <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" /> -->
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="emails" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="sub" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromObjectID" />
</OutputClaimsTransformations>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingUserNameAndValidateStrongAuthenticationEmailAddress" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
In this case, see the comments as to why the OutputClaim with ClaimTypeReferenceId="email" was modified, which is referencing Jas Suri - MSFT below. With the OutputClaim looking like <OutputClaim ClaimTypeReferenceId="email" Required="true" /> the display claim is ignored entirely (the built-in verification control is presented).
With the OutputClaim entirely removed, instead the password reset policy winds up not having any fields:
All starter packs/samples are from current repos, and are using v2 pages supporting display claims.
Remove
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />

Azure B2C custom Sign up not displaying input fields

So I am trying to build a custom sign up page, and I need the first name, last name and phone number.
I am building out a proof of concept first so I am using one of the templates to see if i can push it to our API.
In the Extensions I have this technical profile.
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmailCustom">
<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="ContentDefinitionReferenceId">api.localaccountsignup</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GetCurrentDateTime" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_termsOfUseConsentChoice" DefaultValue="AgreeToTermsOfUseConsentNo" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="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" PartnerClaimType="FirstName" />
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="LastName" />
<OutputClaim ClaimTypeReferenceId="extension_termsOfUseConsentChoice" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<!-- During sign up, write the current time of consent, and version -->
<TechnicalProfile Id="AAD-UserWriteUsingLogonEmail">
<PersistedClaims>
<PersistedClaim ClaimTypeReferenceId="currentTime" PartnerClaimType="extension_termsOfUseConsentDateTime" />
<PersistedClaim ClaimTypeReferenceId="extension_termsOfUseConsentChoice" />
<PersistedClaim ClaimTypeReferenceId="extension_termsOfUseConsentVersion" DefaultValue="V1" />
</PersistedClaims>
</TechnicalProfile>
This profile seems to work fine.
I see all the fields thats are in the output claims which is givenName and Surname and obviously the email and password.
But I want to add phone number and zip code.
When i add the following claims.
<OutputClaim ClaimTypeReferenceId="extension_phonenumber" PartnerClaimType="phonenumber" />
<OutputClaim ClaimTypeReferenceId="postalCode" PartnerClaimType="zipcode" />
I get this issue when i try to render the sign up page.
The page cannot be displayed because an internal server error has occurred.
I have made sure in my base file this is in the input claim
<InputClaim ClaimTypeReferenceId="postalCode" />
Im kind of at a loss for how to collect this user information..
They shouldn’t have partnerclaimtypes. And make sure the claim definition of each output claim has a userInputType defined.

Azure B2C: How to check if strongAuthenticationPhoneNumber is persisting against the right object ID?

I have the following custom policy TP for the phone verification step (which create a temp ID) for MFA.
<ClaimsProvider>
<DisplayName>PhoneFactor</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="PhoneFactor-InputOrVerify">
<DisplayName>PhoneFactor</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.PhoneFactorProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.phonefactor</Item>
<Item Key="ManualPhoneNumberEntryAllowed">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<!-- <InputClaimsTransformation ReferenceId="CreateUserIdForMFA" /> -->
<!-- toggle -->
<InputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
<InputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
</InputClaimsTransformations>
<InputClaims>
<!-- <InputClaim ClaimTypeReferenceId="userIdForMFA" PartnerClaimType="UserId" /> -->
<!-- toggle -->
<InputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="UserId" />
<InputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="Verified.OfficePhone" />
<OutputClaim ClaimTypeReferenceId="newPhoneNumberEntered" PartnerClaimType="newPhoneNumberEntered" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-MFA" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The next step in the journey is to save the phone number using the object id. But I don't think it is saving it!
You can always read it back in another journey or use ms graph api to return the strongAuth attribute.
https://learn.microsoft.com/en-gb/graph/api/resources/phoneauthenticationmethod?view=graph-rest-beta

Validation Profiles still execute if previous profiles throw error

I have a custom sign-up policy that has 3 validation profiles.
<ValidationTechnicalProfile ReferenceId="UsernameCheck" ContinueOnError = "false"/>
<ValidationTechnicalProfile ReferenceId="API-VerifyStep1" ContinueOnError = "false"/>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonName" ContinueOnError="false" />
<TechnicalProfile Id="UsernameCheck">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<TechnicalProfile Id="API-VerifyStep1">
<DisplayName>Validate user's input data and return if valid to sign-up</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://...</Item>
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production environments -->
<Item Key="AuthenticationType">None</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="extension_accessNumber" />
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="extension_error" PartnerClaimType="extension_error" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="extension_message" PartnerClaimType="extension_message" DefaultValue="Error"/>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="newUser" PartnerClaimType="newClaimsPrincipalCreated" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertErrorMessageFalseStep1" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
The problem is all validation profiles are executed regarldess of throwing an error. If either of the first two throw an error I receive a message on the sign-up page. However, the user still gets added to Azure AD. Why does it do this? What is the point of ContinueOnError? How do I ensure the write only happens after the other two are done validating?
Thank you in advance
ContinueOnError is by default false, so you shouldn't need to specify it as long as you want the default behavior.
Since you are using a restful provider, I suspect you are running into the issue mentioned by following thread -
Custom policy REST API ValidationTechnicalProfile ContinueOnError not working for HTTP codes like 404 NotFound and 401 unauthorized
Basically the validation technical profile needs to return 409.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile
You can also use Preconditions as mentioned on the above page to avoid executing a technical profile.

Resources