Integrate SMS And TOTP In Azure AD B2C Custom Policy

Azure AD B2C custom policies are XML files that identity developers can use for programming identity tasks. For example, authenticating a user using standard protocols such as OpenID Connect, OAuth or SAML. Custom policies also support REST API calls, which can be useful if you need an extra step to retrieve user claims using the user info endpoint. The Azure AD B2C custom policy starter pack comes with prebuilt policies to get started quickly. In this article I will show you how to integrate both SMS and TOTP multifactor authentication methods (MFA) into these policies. The user will be able to choose between one or the other and save its configuration in Azure AD B2C. During subsequent sign-ins, the user will be asked to authenticate using the configured MFA method.

Prerequisites

Enabling both SMS and TOTP is an advanced scenario which we will built on top of the custom policies from Microsoft. In order to apply what comes next, I assume you have some level of knowledge and experience. For example, you need to know how to program behavior in these XML files. You also need an Azure AD B2C tenant and know how to deploy the XML files and run the custom policies. I won’t go into these details, so make sure you get to this level first.

When you’re ready, download the custom policy starter pack from GitHub. You can also clone the repository from https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack. Update the XML files in the SocialAndLocalAccountsWithMFA starter pack with your Azure AD B2C tenant name.

After that, download the custom policy files from https://github.com/azure-ad-b2c/samples/tree/master/policies/totp. You need these files to support TOTP. Update the XML files with your Azure AD B2C tenant name. Make sure to include TrustFrameworkExtensions.xml, TrustFrameworkLocalization.xml, and TrustFrameworkBase.xml XML files from the SocialAndLocalAccountsWithMFA starter pack.

Updating phone number causes problems due to incorrect format

The TrustFrameworkBase.xml file contains the PhoneFactor-InputOrVerify technical profile. It uses the PhoneFactorProtocolProvider which provides a user interface to interact with the user to verify, or enroll a phone number. This all works well until you want to update your phone number using the Graph API.

The problem is that the PhoneFactorProtocolProvider returns a verified phone number without space between country prefix and phone number (in Azure Portal it’s +11112223333, but should be +1 1112223333). The verified phone number is then saved in Azure AD B2C. If you ever want to update the phone number, the query for phone number using the Graph API returns an empty array. And if you try to delete the phone number, the Graph API returns a 404.

You can work around this problem by using the AzureMfaProtocolProvider and save the phone number in a custom claim. Read on to find out how this works.

Claim Types, Transformations & Technical Profiles

Claim types function like variables and are used to store values like the chosen MFA method and phone number. Some of these values need to be saved in the user object in Azure AD B2C. You also need to be able to retrieve these values during subsequent sign-ins. For example, after signing-in to check what MFA method the user has chosen. In order to do that, you need to add some technical profiles. Before you can work with these technical profiles, you need claim transformations to provide the correct input.

Add Claim Declarations

The claims declarations shown below are the extra claims you need on top of the claims in the starter pack. Add these claims to the TrustFrameworkExtensions_TOTP.xml. Claims that start with the extension prefix will be saved with the user in Azure AD B2C. The extension_mfaScenario is a custom claim that will hold the value SMS or TOTP. When the user signs in for the first time, he or she is required to choose one of these options. If the user chooses the SMS option, a phone number is also required and will be saved in the extension_mfaPhoneNumber claim. All the other claims are used to store user input and output from claims transformations during execution of the custom policy.

<ClaimType Id="extension_mfaScenario">
    <DisplayName>Select MFA method.</DisplayName>
    <DataType>string</DataType>
    <UserInputType>RadioSingleSelect</UserInputType>
    <Restriction>
        <Enumeration Text="SMS" Value="sms" SelectByDefault="true" />
        <Enumeration Text="TOTP" Value="totp" />
    </Restriction>
</ClaimType>

<ClaimType Id="extension_mfaPhoneNumber">
    <DataType>string</DataType>
    <Mask Type="Simple">XXX-XXX-</Mask>
    <UserInputType>Readonly</UserInputType>
</ClaimType>

<ClaimType Id="mfaSelected">
    <DisplayName>mfaSelected</DisplayName>
    <DataType>boolean</DataType>
</ClaimType>

<ClaimType Id="fullPhoneNumber">
    <DisplayName>Phone number</DisplayName>
    <DataType>phoneNumber</DataType>
    <UserInputType>Readonly</UserInputType>
</ClaimType>

<ClaimType Id="inputPhoneNumber">
    <DisplayName>Phone Number</DisplayName>
    <DataType>string</DataType>
    <UserInputType>TextBox</UserInputType>
</ClaimType>

<ClaimType Id="verificationCode">
    <DisplayName>Verification code</DisplayName>
    <DataType>string</DataType>
    <UserInputType>TextBox</UserInputType>
</ClaimType>

<ClaimType Id="inputCountryCode">
    <DisplayName>Country</DisplayName>
    <DataType>string</DataType>
    <UserHelpText>Country Code</UserHelpText>
    <UserInputType>DropdownSingleSelect</UserInputType>
    <Restriction>
        <Enumeration Text="Australia(+61)" Value="AU" />
        <Enumeration Text="Canada(+1)" Value="CA" />
        <Enumeration Text="France(+33)" Value="FR" />
        <Enumeration Text="Germany(+49)" Value="DE" />
        <Enumeration Text="India(+91)" Value="IN" />
        <Enumeration Text="Netherlands(+31)" Value="NL" SelectByDefault="true" />
        <Enumeration Text="Norway(+47)" Value="NO" />
        <Enumeration Text="United Kingdom(+44)" Value="GB" />
        <Enumeration Text="United States(+1)" Value="US" />
        <Enumeration Text="Sweden(+46)" Value="SE" />
    </Restriction>
</ClaimType>

Add Phone Number Claims Transformations

You need a few claims transformations in order to work with the technical profiles shown in the following chapters. The claims transformations shown below transform a phone number string to the phoneNumber data type and vice versa.

Add these claims transformations to the TrustFrameworkExtensions_TOTP.xml.

<ClaimsTransformation Id="ConvertStringToPhoneNumber" TransformationMethod="ConvertStringToPhoneNumberClaim">
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" TransformationClaimType="phoneNumberString" />
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="fullPhoneNumber" TransformationClaimType="outputClaim" />
    </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="ConvertStringToPhoneNumberWithCountryCode" TransformationMethod="ConvertStringToPhoneNumberClaim">
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="inputPhoneNumber" TransformationClaimType="phoneNumberString" />
        <InputClaim ClaimTypeReferenceId="inputCountryCode" TransformationClaimType="country" />
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="fullPhoneNumber" TransformationClaimType="outputClaim" />
    </OutputClaims>
</ClaimsTransformation>

<ClaimsTransformation Id="ConvertPhoneNumberToString" TransformationMethod="ConvertPhoneNumberClaimToString">
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="fullPhoneNumber" TransformationClaimType="phoneNumber" />
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" TransformationClaimType="phoneNumberString" />
    </OutputClaims>
</ClaimsTransformation>

Add Technical Profile: Create Full Phone Number

After a user has configured the SMS MFA method, the phone number is saved as a string in Azure AD B2C. The next time the user signs in, this phone number is retrieved from Azure AD B2C. In order to work with the other technical profiles, this string value needs to be transformed to a phoneNumber data type. The technical profile shown below transforms the extension_mfaPhoneNumber to a phoneNumber data type. It does so using the ConvertStringToPhoneNumber claims transformation. This technical profile is called by orchestration step three in the MultiFactorSubJourney.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="CreateFullPhoneNumber">
    <DisplayName>Create Full Phone Number</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="fullPhoneNumber" />
    </OutputClaims>
    <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="ConvertStringToPhoneNumber" />
    </OutputClaimsTransformations>
</TechnicalProfile>

Add Technical Profile: Validate Phone Number

When the signed-in user chooses the SMS MFA method, he needs to select a country code and enter his phone number. These values together need to be transformed to a phoneNumber data type. The technical profile shown below transforms the country code and phone number to a phoneNumber data type. It does so using the ConvertStringToPhoneNumberWithCountryCode claims transformation. This technical profile is called by the self asserted technical profile PhoneFactor-Configure that gathers this input from the user.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="ValidatePhoneNumber">
    <DisplayName>Validate Phone Number</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="fullPhoneNumber" />
    </OutputClaims>
    <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="ConvertStringToPhoneNumberWithCountryCode" />
    </OutputClaimsTransformations>
</TechnicalProfile>

Add Technical Profile: Send SMS

The technical profile shown below is used to send a code via SMS. It does so by using the AzureMfaProtocolProvider. This provider requires the user principal name and the user’s phone number as input claims. It is called by the self asserted technical profiles PhoneFactor-Configure and PhoneFactor-SendCode that gather input from the user.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="AzureMfa-SendSms">
    <DisplayName>Send Sms</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureMfaProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <Metadata>
        <Item Key="Operation">OneWaySMS</Item>
    </Metadata>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="userPrincipalName" />
        <InputClaim ClaimTypeReferenceId="fullPhoneNumber" PartnerClaimType="phoneNumber" />
    </InputClaims>
</TechnicalProfile>

Add Technical Profile: Verify SMS

The technical profile shown below is used to verify the code sent via SMS. It does so by using the AzureMfaProtocolProvider. This provider requires the phone number and verification code as input claims. It is called by the self asserted technical profile PhoneFactor-EnterVerificationCode that gather input from the user.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="AzureMfa-VerifySms">
    <DisplayName>Verify Sms</DisplayName>
    <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureMfaProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
    <Metadata>
        <Item Key="Operation">Verify</Item>
    </Metadata>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="fullPhoneNumber" PartnerClaimType="phoneNumber" />
        <InputClaim ClaimTypeReferenceId="verificationCode" />
    </InputClaims>
</TechnicalProfile>

Add Technical Profile: Write MFA Method

After the user has chosen his preferred MFA method, it needs to be saved in Azure AD B2C. The technical profile shown below will write the chosen MFA method to the user’s object. It is called by orchestration step seven in the MultiFactorSubJourney. Because the chosen MFA method will be saved as a custom claim, the name of the claim starts with the extension prefix.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="AADUserWriteChosenMfa">
    <Metadata>
        <Item Key="Operation">Write</Item>
        <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
    </Metadata>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="objectId" />
    </InputClaims>
    <PersistedClaims>
        <PersistedClaim ClaimTypeReferenceId="objectId" />
        <PersistedClaim ClaimTypeReferenceId="userPrincipalName" />
        <PersistedClaim ClaimTypeReferenceId="displayName" />
        <PersistedClaim ClaimTypeReferenceId="extension_mfaScenario" />
    </PersistedClaims>
    <IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>

Add Technical Profile: Write MFA Phone Number

If the user has chosen the SMS MFA method, the phone number needs to be saved as well. This is done using the technical profile shown below, which is called by orchestration step eight in the MultiFactorSubJourney. Because the phone number will be saved as a custom claim, the name of the claim starts with the extension prefix. Before persisting, it first transforms the phone number to a string. After that, the extension_mfaPhoneNumber claim will be saved as an extra property of the user.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="AADUserWritePhoneNrUsingObjectId">
    <Metadata>
        <Item Key="Operation">Write</Item>
        <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
    </Metadata>
    <IncludeInSso>false</IncludeInSso>
    <InputClaimsTransformations>
        <InputClaimsTransformation ReferenceId="ConvertPhoneNumberToString" />
    </InputClaimsTransformations>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
    </InputClaims>
    <PersistedClaims>
        <PersistedClaim ClaimTypeReferenceId="objectId" />
        <PersistedClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
    </PersistedClaims>
    <IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>

Add Technical Profile: Get MFA Claims

The next time a user signs in, the MFA claims will be retrieved using the technical profile shown below. It is called by the first orchestration step in the MultiFactorSubJourney. If the user has chosen the TOTP MFA method, the extension_mfaPhoneNumber will be empty.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="AADUserReadChosenMfaUsingObjectId">
    <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="userPrincipalName" />
        <OutputClaim ClaimTypeReferenceId="extension_mfaScenario" />
        <OutputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
    </OutputClaims>
    <IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>

Gather User Input Using Self Asserted Technical Profiles

The user needs to be able to choose and configure a MFA method and enter a verification code. In order to gather this user input, you need to add the self asserted technical profiles discussed in this chapter.

Add Self Asserted Technical Profile: Choose MFA Method

The first time the user signs in, he selects his preferred MFA method. This is accomplished using the self asserted technical profile shown below, which is called by orchestration step two in the MultiFactorSubJourney. The user is presented with a page in which he can choose between SMS and TOTP. These options are declared using the extension_mfaScenario claim. After choosing a MFA method, the mfaSelected claim is set to true. This claim is used as a precondition in the orchestration steps discussed later.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="SelfAssertedChooseMfa">
    <DisplayName>Choose MFA Method</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</Item>
        <Item Key="setting.showCancelButton">true</Item>
    </Metadata>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="extension_mfaScenario" Required="true" />
        <OutputClaim ClaimTypeReferenceId="mfaSelected" DefaultValue="true" />
    </OutputClaims>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Add Self Asserted Technical Profile: Configure Phone Number

The self asserted technical profile shown below is used by the user to configure the claims required by the SMS MFA method. It is called by the first orchestration step in the PhoneFactorSubJourney. After entering his country code and phone number, the user hits the send code button. As a result, the validation technical profiles are executed. After successful execution of the ValidatePhoneNumber technical profile, the AzureMfa-SendSms technical profile starts. As a result, the user receives a code sent via SMS.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="PhoneFactor-Configure">
    <DisplayName>Configure Phone Factor</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</Item>
        <Item Key="language.button_continue">Send Code</Item>
        <Item Key="UserMessageIfClaimsTransformationInvalidPhoneNumber">The phone number is not valid.</Item>
        <Item Key="UserMessageIfServerError">Internal error.</Item>
        <Item Key="UserMessageIfThrottled">Please wait 30 seconds before trying to receive a new SMS code.</Item>
    </Metadata>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="inputCountryCode" Required="true" />
        <OutputClaim ClaimTypeReferenceId="inputPhoneNumber" Required="true" />
        <OutputClaim ClaimTypeReferenceId="fullPhoneNumber" />
    </OutputClaims>
    <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="ConvertPhoneNumberToString" />
    </OutputClaimsTransformations>
    <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="ValidatePhoneNumber" />
        <ValidationTechnicalProfile ReferenceId="AzureMfa-SendSms" />
    </ValidationTechnicalProfiles>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Add Self Asserted Technical Profile: Send Code Via SMS

The user does not have to configure his phone number again if he has already done that. The next time the user signs in, we just show the configured phone number using the self asserted technical profile shown below. It is called by orchestration step two in the PhoneFactorSubJourney. When the user hits the send code button, a unique code is sent using the AzureMfa-SendSms technical profile.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="PhoneFactor-SendCode">
    <DisplayName>PhoneFactor Send Code</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</Item>
        <Item Key="language.button_continue">Send Code</Item>
        <Item Key="UserMessageIfServerError">Internal error.</Item>
        <Item Key="UserMessageIfThrottled">Please wait 30 seconds before trying to receive a new SMS code.</Item>
    </Metadata>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
    </InputClaims>
    <DisplayClaims>
        <DisplayClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
    </DisplayClaims>
    <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="AzureMfa-SendSms" />
    </ValidationTechnicalProfiles>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Add Self Asserted Technical Profile: Enter Verification Code Sent Via SMS

After receiving the code sent via SMS, the user needs to enter this code so it can be verified. The page that allows this is created using the self asserted technical profile shown below. It is called by orchestration step three in the PhoneFactorSubJourney. After entering the code in the textbox, the code will be verified using the AzureMfa-VerifySms technical profile.

Add the following technical profile to TrustFrameworkExtensions_TOTP.xml.

<TechnicalProfile Id="PhoneFactor-EnterVerificationCode">
    <DisplayName>Enter Verification Code</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</Item>
        <Item Key="language.button_continue">Verify</Item>
        <Item Key="UserMessageIfWrongCodeEntered">The verification code you entered does not match the code sent via sms.</Item>
        <Item Key="UserMessageIfMaxAllowedCodeRetryReached">Max allowed code retry reached.</Item>
        <Item Key="UserMessageIfServerError">Internal error.</Item>
        <Item Key="UserMessageIfThrottled">Please wait 30 seconds before trying to receive a new SMS code.</Item>
    </Metadata>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
    </InputClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="extension_mfaPhoneNumber" />
        <OutputClaim ClaimTypeReferenceId="verificationCode" Required="true" />
    </OutputClaims>
    <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="AzureMfa-VerifySms" />
    </ValidationTechnicalProfiles>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Integrate Multi Factor Authentication In User Journey

The user journey defines the path the user must follow in order to authenticate and retrieve its claims. By default, the starter pack defines the sub journeys, its orchestration steps and the technical profiles for MFA via TOTP. You added the necessary technical profiles and transformations for MFA via SMS in the previous chapters. In this chapter, you will add the Phone Factor Sub Journey and its orchestration steps that call these technical profiles. After that, you will add the Multi Factor Sub Journey that allows the user to choose between the journeys for SMS and TOTP. To finalize, you add a user journey that calls the Multi Factor Sub Journey.

Add Phone Factor Sub Journey

The Phone Factor Sub Journey shown below consists of orchestration steps that interact with the technical profiles for MFA via SMS. Each orchestration step contains a precondition. If the precondition is not met, the technical profile gets executed. For example, the first orchestration step will check if a MFA method is selected. If it is and the MFA scenario is equal to phone, the PhoneFactor-Configure technical profile is executed. This technical profile will allow the user to configure his phone number.

The second orchestration step gets executed if the MFA method is not selected. This is the case if the user has already configured his phone number. In that case, the user is shown a page with his phone number and a send code button. If the user hits the send code button, a verification code is sent via SMS which the user must enter in the last orchestration step.

Add the following sub journey to TrustFrameworkExtensions_TOTP.xml.

<SubJourney Id="PhoneFactorJourney" Type="Call">
    <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>mfaSelected</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="PhoneFactor-Configure" TechnicalProfileReferenceId="PhoneFactor-Configure" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                    <Value>mfaSelected</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="PhoneFactor-SendCode" TechnicalProfileReferenceId="PhoneFactor-SendCode" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="3" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="PhoneFactor-EnterVerificationCode" TechnicalProfileReferenceId="PhoneFactor-EnterVerificationCode" />
            </ClaimsExchanges>
        </OrchestrationStep>

    </OrchestrationSteps>
</SubJourney>

Add Multi Factor Sub Journey

The Multi Factor Sub Journey shown below will allow the user to choose between SMS and TOTP. After choosing a MFA method, the user will not be asked to configure it again. For this reason, the first orchestration step will query Azure AD B2C to check if the user already configured a MFA method. If not, the user must choose a MFA method in the second orchestration step. The third orchestration step creates a phone number if that claim was returned in the first orchestration step. That phone number will then be used by the Phone Factor Sub Journey in the fourth orchestration step.

If the user has chosen TOTP, the fifth orchestration step will call the TOTP enrollment sub journey. If the user already enrolled, the sub journey will not ask the user to enroll again. In orchestration step six, the user must enter the verification code.

After choosing a MFA method, orchestration step seven writes the chosen MFA method to Azure AD B2C. If the user has chosen the SMS MFA method, the phone number is saved to Azure AD B2C as well.

Add the following sub journey to TrustFrameworkExtensions_TOTP.xml.

<SubJourney Id="MultiFactorSubJourney" Type="Call">
    <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserReadChosenMfaUsingObjectId" TechnicalProfileReferenceId="AADUserReadChosenMfaUsingObjectId" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
                    <Value>extension_mfaScenario</Value>
                    <Value>totp</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="SelfAssertedChooseMfa" TechnicalProfileReferenceId="SelfAssertedChooseMfa" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="3" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaPhoneNumber</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="CreateFullPhoneNumber" TechnicalProfileReferenceId="CreateFullPhoneNumber" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="4" Type="InvokeSubJourney">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <JourneyList>
                <Candidate SubJourneyReferenceId="PhoneFactorJourney" />
            </JourneyList>
        </OrchestrationStep>

        <OrchestrationStep Order="5" Type="InvokeSubJourney">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>totp</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <JourneyList>
                <Candidate SubJourneyReferenceId="TotpFactor-Input" />
            </JourneyList>
        </OrchestrationStep>

        <OrchestrationStep Order="6" Type="InvokeSubJourney">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>totp</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <JourneyList>
                <Candidate SubJourneyReferenceId="TotpFactor-Verify" />
            </JourneyList>
        </OrchestrationStep>

        <OrchestrationStep Order="7" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>mfaSelected</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserWriteChosenMfa" TechnicalProfileReferenceId="AADUserWriteChosenMfa" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="8" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
                    <Value>mfaSelected</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                    <Value>extension_mfaScenario</Value>
                    <Value>sms</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserWritePhoneNrUsingObjectId" TechnicalProfileReferenceId="AADUserWritePhoneNrUsingObjectId" />
            </ClaimsExchanges>
        </OrchestrationStep>

    </OrchestrationSteps>
</SubJourney>

Add User Journey And Call Multi Factor Sub Journey

The user journey shown below contains the orchestration steps for local sign-in, but if you want you can add support for other identity providers as well. After user registration and signing-in, the fourth orchestration step calls the Multi Factor Sub Journey.

Add the following sub journey to TrustFrameworkExtensions_TOTP.xml.

<UserJourney Id="SignUpOrSignMultiMFA">
    <OrchestrationSteps>

        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
            <ClaimsProviderSelections>
                <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
            </ClaimsProviderSelections>
            <ClaimsExchanges>
                <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                    <Value>objectId</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>
            <ClaimsExchanges>
                <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="3" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
            </ClaimsExchanges>
        </OrchestrationStep>

        <OrchestrationStep Order="4" Type="InvokeSubJourney">
            <JourneyList>
                <Candidate SubJourneyReferenceId="MultiFactorSubJourney" />
            </JourneyList>
        </OrchestrationStep>

        <OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />

    </OrchestrationSteps>
</UserJourney>

Conclusion

By default, the SocialAndLocalAccountsWithMFA starter pack uses the PhoneFactorProtocolProvider. It provides a user interface to interact with the user to verify or enroll a phone number. Because the PhoneFactorProtocolProvider returns a phone number without space between country prefix and phone number, you can never update it using the Graph API. To work around this problem, you used the AzureMfaProtocolProvider and save the phone number in a custom claim. If you ever want to update the phone number as part of a reset flow, you can do so by updating the custom claim variable using the Graph API.

The discussed claim types, transformations and technical profiles enable the use of both SMS and TOTP methods. By using the self asserted technical profiles, you allow the user to choose and configure his preferred MFA method. The starter pack already defined the sub journeys and technical profiles for TOTP. What was missing was a sub journey that allowed interaction with the SMS MFA method using the AzureMfaProtocolProvider. To finalize, the Multi Factor Sub Journey is the overarching journey that calls the SMS or TOTP sub journeys depending on the user’s decision.

4 thoughts on “Integrate SMS And TOTP In Azure AD B2C Custom Policy”

  1. Thanks for the writeup, it’s really helpful!
    Unfortunately, I’m stuck at the SelfAssertedChooseMfa part.

    I needed to add

    api.selfasserted

    Otherwise, the page wouldn’t load. It’s now blocked on the input: if I choose sms or TOTP, a request will go to B2C, but it responds with 400 (Bad Request). No logs available in Application Insights… Any ideas how to resolve this?

    Thanks!

    1. Hello Lennart,

      The content definition references were missing for all self asserted technical profiles. I had removed those because I am using custom templates, but they are indeed required. I therefore added the default api.selfasserted content definition to: SelfAssertedChooseMfa, PhoneFactor-Configure, PhoneFactor-SendCode, PhoneFactor-EnterVerificationCode.

      Please check if you are missing the content definition reference id in one of these self asserted technical profiles. Because after choosing the SMS method, the PhoneFactor-Configure or PhoneFactor-SendCode is called. If these don’t have a content definition reference id, B2C might return a 400 bad request.

      What also might be a problem is if the custom claims starting with the extension prefix are not configured under user attributes in B2C. Please add these as well, but without the prefix: mfaScenario, mfaPhoneNumber.

      1. Yes, that fixed that problem. Another one was that I got a 400 when selecting a MFA method (SMS or TOTP). Turned out that B2C expected ‘mfaSelected’ (as it is defined as an outputclaim). Solution: adding Required=’false’.

        Now everything is up & running, you saved me a lot of time!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top