2

Hello i am trying to write an azure b2c custom policy that split email validation sending a custom email with sendgrid(Display Control) and after that ask the user to input values like name or surname.

I am using the following technical profile to get the email and validate it with a custom mail using sendgrid and displays controls:

   <TechnicalProfile Id="EmailVerification">
      <DisplayName>Initiate Email Address Verification For Local 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.localaccountsignup</Item>
        <Item Key="language.button_continue">Continue</Item>

         <!--OTP validation error messages-->
        <Item Key="UserMessageIfSessionDoesNotExist">You have exceed the maximum time allowed.</Item>
        <Item Key="UserMessageIfMaxRetryAttempted">You have exceed the number of retries allowed.</Item>
        <Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
        <Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
      </CryptographicKeys>
      <IncludeInSso>false</IncludeInSso>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" />
      </InputClaims>
      <DisplayClaims>
        <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
      </DisplayClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
      </OutputClaims>
    </TechnicalProfile>

The display control code is the following:

<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="SendGrid" />
        </ValidationClaimsExchange>
      </Action>
      <Action Id="VerifyCode">
        <ValidationClaimsExchange>
          <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
        </ValidationClaimsExchange>
      </Action>
    </Actions>
  </DisplayControl>
</DisplayControls>

The next step is to ask the user for some additional input with the following technical profile:

<TechnicalProfile Id="LocalSignUpWithREmailWithToS">
      <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>
        <!-- Sample: Remove sign-up email verification -->
        <Item Key="EnforceEmailVerification">False</Item>
      </Metadata>
      <InputClaimsTransformations>
        <InputClaimsTransformation ReferenceId="CreateReadonlyEmailClaim" />
      </InputClaimsTransformations>
      <InputClaims>
        <!--Sample: Set input the ReadOnlyEmail claim type to prefilled the email address-->
        <InputClaim ClaimTypeReferenceId="readOnlyEmail" />
      </InputClaims>
      <DisplayClaims>
        <DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="displayName" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
      </DisplayClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId" />
        <!-- Sample: Display the ReadOnlyEmail claim type (instead of email claim type)-->
        <OutputClaim ClaimTypeReferenceId="readOnlyEmail" 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="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surName" />
        <OutputClaim ClaimTypeReferenceId="AgreedToTermsOfService" Required="true" />
        <!--Sample: This is set to "false" by default to bypass OrchestrationStep 5 during Sign-in-->
        <OutputClaim ClaimTypeReferenceId="renewalTOSrequired" DefaultValue="false" />
      </OutputClaims>
      <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
      </ValidationTechnicalProfiles>
      <!-- Sample: Disable session management for sign-up page -->
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
    </TechnicalProfile>

But it appears that the outputClaim "EMAIL" from the first step is failing when executing the claims transformation

  <ClaimsTransformation Id="CreateReadonlyEmailClaim" TransformationMethod="FormatStringClaim">
    <InputClaims>
      <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
    </InputClaims>
    <InputParameters>
      <InputParameter Id="stringFormat" DataType="string" Value="{0}" />
    </InputParameters>
    <OutputClaims>
      <OutputClaim ClaimTypeReferenceId="readonlyEmail" TransformationClaimType="outputClaim" />
    </OutputClaims>
  </ClaimsTransformation>

I was able to capture the error with application insights

Exception Message:A Claim of ClaimType with id "email" was not found, which is required by the ClaimsTransformationImpl of Type "Microsoft.Cpim.Data.Transformations.FormatStringClaimTransformation" for TransformationMethod "FormatStringClaim" referenced by the ClaimsTransformation with id "CreateReadonlyEmailClaim" in policy "B2C_1A_TrustFrameworkExtensionsReMeSplit" of tenant "XXXXXXX.onmicrosoft.com"., Exception Type:PolicyException, CorrelationID.

Does anyone know what is the correct form to read the output claim "EMAIL" from the display control to use it inside the claim transformation?

alfespa17
  • 335
  • 1
  • 3
  • 12
  • Similar GitHub issue “Azure B2C custom policy for Split Signup and custom email OTP combined #193” https://github.com/azure-ad-b2c/samples/issues/193 . – Michael Freidgeim Jun 17 '23 at 05:44

4 Answers4

4

In the EmailVerification technical profile replace this fragment:

<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />

with this fragment:

<OutputClaim ClaimTypeReferenceId="email" />

Then email will be passed as a claim to the next technical profile where it will be converted to readonlyEmail input claim.

Complete EmailVerification technical profile:

<TechnicalProfile Id="EmailVerification">
      <DisplayName>Initiate Email Address Verification For Local 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.localaccountsignup</Item>
        <Item Key="language.button_continue">Continue</Item>

         <!--OTP validation error messages-->
        <Item Key="UserMessageIfSessionDoesNotExist">You have exceed the maximum time allowed.</Item>
        <Item Key="UserMessageIfMaxRetryAttempted">You have exceed the number of retries allowed.</Item>
        <Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
        <Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
      </Metadata>
      <CryptographicKeys>
        <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
      </CryptographicKeys>
      <IncludeInSso>false</IncludeInSso>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" />
      </InputClaims>
      <DisplayClaims>
        <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
      </DisplayClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="email" />
      </OutputClaims>
    </TechnicalProfile>
Daniel Krzyczkowski
  • 2,732
  • 2
  • 20
  • 30
  • 4
    It would be awesome if someone could explain why the default/built-in Microsoft email verification control needs PartnerClaimType="Verified.Email", while a custom one does not. Just been racking my brain for hours on a non-descript error during a return from our sign-up sub-journey "The page cannot be displayed because an internal server error has occurred." that seemed related to the read-only claims transformation. Looks like the issue was exactly what is described here -- the name of the claim is different with a custom control. – GuyPaddock Aug 03 '21 at 05:10
0

Make sure you have the email claim in one of your base policies.

https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack/blob/master/LocalAccounts/TrustFrameworkBase.xml#L179

      <ClaimType Id="email">
        <DisplayName>Email Address</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OpenIdConnect" PartnerClaimType="email" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Email address that can be used to contact you.</UserHelpText>
        <UserInputType>TextBox</UserInputType>
        <Restriction>
          <Pattern RegularExpression="^[a-zA-Z0-9.!#$%&amp;'^_`{}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$" HelpText="Please enter a valid email address." />
        </Restriction>
      </ClaimType>
Alex AIT
  • 17,361
  • 3
  • 36
  • 73
0

In LocalAccountSignUpWithLogonEmail Technical Profile of your Policy Base (e.g. TrustFrameworkBase.xml) change email output claim

from:

<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />

to

<OutputClaim ClaimTypeReferenceId="email" />

marek_lani
  • 3,895
  • 4
  • 29
  • 50
  • Is it the same as [Daniel Krzyczkowski answer](https://stackoverflow.com/questions/62251477/azure-b2c-claimstransformation-error-with-display-control-outputclaim/67210599#67210599) ? – Michael Freidgeim Jun 19 '23 at 13:47
0

In addition to Daniel K.'s essential answer, I had to add the following missing claim type in ClaimsSchema block. I found this out looking at my App Insights exception and at starter pack (https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack/blob/main/Display%20Controls%20Starterpack/SocialAndLocalAccountsWithMfa/TrustFrameworkBase.xml):

<ClaimType Id="verificationCode">
  <DisplayName>Verification Code</DisplayName>
  <DataType>string</DataType>
  <UserHelpText>Enter your verification code</UserHelpText>
  <UserInputType>TextBox</UserInputType>
</ClaimType>

And a ClaimProvider for sending and receiving code...

It was not easy at first look to notice that Daniel K.'s solution was already integrated to starter pack as time on me playing with this (08/2022)... when you already have the technical profile defined in your XML for months and "just want" to add DisplayClaims support.

barbara.post
  • 1,581
  • 16
  • 27