azureazure-ad-b2cidentity-experience-framework

How to Implement Azure AD B2C Identity Experience Framework Sign-In with Optional Email OTP


I have a business requirement but I've failed to figure out how to implement it.

  1. The very first time a user signs-in, they are challenged with email OTP followed by a forced password reset.
  2. All subsequence sign-in attempts will prompt the user only for email address and password (OTP is a one-time user flow).

My problem is that Orchestration step 1 collects the email address, then I query AAD by the email address provided and read an extension attribute called extension_EmailValidated. If the attribute is not TRUE, B2C will force the user to do email OTP verification, however, Orchestration step 2 won't let me pre-populate the previously entered email address input box AND show the OTP buttons. It will only let me do one or the other (hope that makes sense).

My UserJourney looks like this

  <UserJourneys>
    <UserJourney Id="B2CSignIn">
      <OrchestrationSteps>
        <!--User enters their email address-->
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="AADEmailDiscovery" TechnicalProfileReferenceId="AAD-EmailDiscovery" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!--User enters their OTP-->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_EmailVerified</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADEmailVerification" TechnicalProfileReferenceId="AAD-EmailVerification" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!--Set extension_EmailVerified to TRUE-->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_EmailVerified</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserWriteEmailVerifiedUsingEmail" TechnicalProfileReferenceId="AAD-UserWriteEmailVerifiedUsingEmail" />
          </ClaimsExchanges>
        </OrchestrationStep>

Here are the technical profiles for Step 1 and Step 2.

<TechnicalProfile Id="AAD-EmailDiscovery">
  <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>
    <Item Key="setting.showCancelButton">False</Item>
  </Metadata>
  <CryptographicKeys>
    <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
  </CryptographicKeys>
  <IncludeInSso>false</IncludeInSso>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="email" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="email" />
    <OutputClaim ClaimTypeReferenceId="extension_EmailVerified" DefaultValue="false" Required="true" />
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
  </ValidationTechnicalProfiles>
</TechnicalProfile>

<TechnicalProfile Id="AAD-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>
    <Item Key="setting.showCancelButton">False</Item>
  </Metadata>
  <CryptographicKeys>
    <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
  </CryptographicKeys>
  <IncludeInSso>false</IncludeInSso>
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="email" />
  </InputClaims>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
  </OutputClaims>
  <ValidationTechnicalProfiles>
    <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
  </ValidationTechnicalProfiles>
</TechnicalProfile>

Another very confusing issue is that the only way I can get the OTP buttons to appear on Step 2 is to include "ParterClaimType=Verified.Email" for both the InputClaim and OutputClaims -- and that makes no sense.

If I omit "ParterClaimType=Verified.Email" from InputClaims and OutputClaims, I can get the email address to pre-populate from Step 1.

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

Advice and guidance is very much appreciated.

Thanks!


Solution

  • The solution here is to make the input text box readOnly.

    Send the email collected at the sign in screen through a claim transform to copy it to a new claim, called readOnlyEmail. The claim should be defined as read only.

          <ClaimType Id="readOnlyEmail">
            <DisplayName>Email Address</DisplayName>
            <DataType>string</DataType>
            <UserHelpText/>
            <UserInputType>Readonly</UserInputType>
          </ClaimType>
    

    Copy the email claim into the readOnly claim

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

    Then make a call to the claims transform from your sign up technical profile using an OutputClaimsTransformations node.

          <OutputClaimsTransformations>
                <OutputClaimsTransformation ReferenceId="CopySignInNameToReadOnly" />
          </OutputClaimsTransformations>
    

    Finally on the email verification technical profile (selfAsserted technical profile), pass in the readOnly email to be validated:

              <InputClaims>
                <InputClaim ClaimTypeReferenceId="readOnlyEmail"/>
              </InputClaims>
              <OutputClaims>
                <!-- Required claims -->
                <OutputClaim ClaimTypeReferenceId="readOnlyEmail" PartnerClaimType="Verified.Email"/>
              </OutputClaims>
    

    The concept is demonstrated here: https://github.com/azure-ad-b2c/samples/tree/master/policies/signin-email-verification

    You just need to add your conditional logic on top of that sample.