Azure AD B2C Custom Policy - MFA - TOTP - Failed to show verification page

Suresh Siddagari 0 Reputation points
2024-10-16T14:44:20+00:00

Hi,

I'm trying to configure TOTP as a MFA to our application using information provided https://github.com/azure-ad-b2c/samples/blob/master/policies/totp/policy/TrustFrameworkExtensions_TOTP.xml

Problem : It shows signup page and I can enter email, password, first name and last name. When I press Create, it creates an user in Entra ID ( I have verified in Users). After it shows QR code, I can scan the QR code and press Continue to enter code, it shows blank page. So, it is crashed here.

From B2C_1A_SIGNUP_SIGNIN.xml

<RelyingParty>
    <DefaultUserJourney ReferenceId="CustomSignUpSignIn" />
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid" />
        <OutputClaim ClaimTypeReferenceId="identityProvider" />
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
        <OutputClaim ClaimTypeReferenceId="isForgotPassword" DefaultValue="false" />
        <OutputClaim ClaimTypeReferenceId="otherMails" PartnerClaimType="emails" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>

Here are user journeys:

<!--UserJourneys>-->
  <UserJourneys>
    <UserJourney Id="CustomSignUpSignIn">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <!--<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />-->
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="AzureADCommonExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Check if the user has selected to sign in using one of the social providers -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <!--<ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />-->
            <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
            <ClaimsExchange Id="AzureADCommonExchange" TechnicalProfileReferenceId="AADCommon-OpenIdConnect" />
            <ClaimsExchange Id="ForgotPasswordExchange" TechnicalProfileReferenceId="ForgotPassword" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>isForgotPassword</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="PasswordReset" />
          </JourneyList>
        </OrchestrationStep>
        <!-- For social IDP authentication, attempt to find the user account in the directory. -->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>localAccountAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId). 
          This can only happen when authentication happened using a social IDP. If local account was created or authentication done
          using ESTS in step 2, then an user account must exist in the directory by this time. -->
        <OrchestrationStep Order="5" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent 
          in the token. -->
        <OrchestrationStep Order="6" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>authenticationSource</Value>
              <Value>socialIdpAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect 
             from the user. So, in that case, create the user in the directory if one does not already exist 
             (verified using objectId which would be set from the last step if account was created in the directory. -->
        <OrchestrationStep Order="7" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        
        <!-- Call the TOTP enrollment ub journey. If user already enrolled the sub journey will not ask the user to enroll -->
        <OrchestrationStep Order="8" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Input" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Call the TOTP validation sub journey-->
        <OrchestrationStep Order="9" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Verify" />
          </JourneyList>
        </OrchestrationStep>
        <OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>
  </UserJourneys>
  <!-- </UserJourneys-->

and here SubJourneys

  <SubJourneys>
    <SubJourney Id="PasswordReset" Type="Call">
      <OrchestrationSteps>
        <!-- Validate user's email address. -->
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Call the TOTP enrollment ub journey. If user already enrolled the sub journey will not ask the user to enroll -->
        <OrchestrationStep Order="2" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Input" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Call the TOTP validation sub journey-->
        <OrchestrationStep Order="3" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Verify" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Collect and persist a new password. -->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
    <!-- TOTP enrollment sub journey-->
    <SubJourney Id="TotpFactor-Input" Type="Call">
      <OrchestrationSteps>
        <!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
        <OrchestrationStep Order="1" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="SetTotpInitialValue" />
          </JourneyList>
        </OrchestrationStep>
        <!-- If current user is not a new one (this is a sign-in flow, and not sign-up), 
             check the number of available devices. -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>newUser</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- If the number of available devices is zero (user hasn't enrolled before), 
            render the TOTP enrollment page to scan the QR code that starts the enrollment process -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>numberOfAvailableDevices</Value>
              <Value>0</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AuthenticatorForSignUp" TechnicalProfileReferenceId="EnableOTPAuthentication" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- If the number of available devices is zero (user hasn't enrolled before), 
             begin the TOTP verification session.  -->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>numberOfAvailableDevices</Value>
              <Value>0</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AuthenticatorForSignInBeginSession" TechnicalProfileReferenceId="AzureMfa-BeginVerifyOTP" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- If the number of available devices is zero (user hasn't enrolled before), 
             render the TOTP verification page.  -->
        <OrchestrationStep Order="5" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>numberOfAvailableDevices</Value>
              <Value>0</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AuthenticatorForSignIn" TechnicalProfileReferenceId="OTPVerification" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
    <!-- TOTP verification sub journey-->
    <SubJourney Id="TotpFactor-Verify" Type="Call">
      <OrchestrationSteps>
        <!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
        <OrchestrationStep Order="1" Type="InvokeSubJourney">
          <JourneyList>
            <Candidate SubJourneyReferenceId="SetTotpInitialValue" />
          </JourneyList>
        </OrchestrationStep>
        <!-- If current user is not a new one (this is a sign-in flow, and not sign-up), 
             check the number of available devices. -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>newUser</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- If the number of available devices isn't zero (user has enrolled before), 
             begin the TOTP verification session -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>numberOfAvailableDevices</Value>
              <Value>0</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AuthenticatorForSignInBeginSession" TechnicalProfileReferenceId="AzureMfa-BeginVerifyOTP" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- If the number of available devices isn't zero (user has enrolled before), 
             render the TOTP verification page -->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>numberOfAvailableDevices</Value>
              <Value>0</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AuthenticatorForSignIn" TechnicalProfileReferenceId="OTPVerification" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
    <!--TOTP subjourneys-->
    <!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
    <SubJourney Id="SetTotpInitialValue" Type="Call">
      <OrchestrationSteps>
        <!-- If number of available device claim not exists, set the value to 0-->
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>numberOfAvailableDevices</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpDefaultValue" TechnicalProfileReferenceId="SetTotpDefaultValue" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- The following orchestration steps try to get the user identifier for different 
             type of authentication, such as local and social account.-->
        <!-- Try to get the identifier from UserId-->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>totpIdentifier</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>UserId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpIdentifierAsUserId" TechnicalProfileReferenceId="CreateTotpIdentifier-UserId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Get the identifier from email-->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>totpIdentifier</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>email</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpIdentifierAsEmail" TechnicalProfileReferenceId="CreateTotpIdentifier-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Get the identifier from the user's emails -->
        <OrchestrationStep Order="4" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>totpIdentifier</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>emails</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="ExtractEmailFromEmailsForTotpIdentifier" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Get the identifier from local account sign-in name-->
        <OrchestrationStep Order="5" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>totpIdentifier</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>signInName</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpIdentifierAsSignInName" TechnicalProfileReferenceId="CreateTotpIdentifier-SignInName" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
    <!-- Get the identifier from the user's emails -->
    <SubJourney Id="ExtractEmailFromEmailsForTotpIdentifier" Type="Call">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>emails</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="GetEmailAddress" TechnicalProfileReferenceId="GetEmailAddress" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>ReadOnlyEmail</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="GetEmailFromReadOnlyEmail" TechnicalProfileReferenceId="GetEmailFromReadOnlyEmail" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>email</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpIdentifierAsEmail" TechnicalProfileReferenceId="CreateTotpIdentifier-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>signInName</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SetTotpIdentifierAsSignInName" TechnicalProfileReferenceId="CreateTotpIdentifier-SignInName" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
  </SubJourneys>

Microsoft Entra ID
Microsoft Entra ID
A Microsoft Entra identity service that provides identity management and access control capabilities. Replaces Azure Active Directory.
22,093 questions
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.