Azure B2C custom MFA implementation

This blog post shows how to configure custom MFA implementation to Azure B2C with custom policies (SignIn). Azure MFA can be easily configured to the custom policies but sometimes custom implementation is required ex. if you want to use your service provider to deliver SMS-based MFA events. The article doesn't cover details about the custom REST API implementation but the overall implementation looks like this:

Azure B2C custom policy configuration

1. User Journey configuration

First, configure a new step to your User Journey which starts the MFA (SMS) process.

<UserJourneys>
    <UserJourney Id="SignInWithMfa">
        <OrchestrationSteps>
            <OrchestrationStep Order="1"></OrchestrationStep>
            <OrchestrationStep Order="2"></OrchestrationStep>
            <OrchestrationStep Order="3" Type="ClaimsExchange">
                <ClaimsExchanges>
                    <!--Custom MFA (SMS) process starts-->
                    <ClaimsExchange Id="VerifyPhone" TechnicalProfileReferenceId="PhoneVerify-Profile" />
                </ClaimsExchanges>
            </OrchestrationStep>
            <OrchestrationStep Order="4"></OrchestrationStep>
        </OrchestrationSteps>
    </UserJourney>
</UserJourneys>

2. Configure claims schema

<BuildingBlocks>
    <ClaimsSchema>
       <!--ReadOnlyPhoneNumber is shown in the Display Control (MFA)-->
       <ClaimType Id="ReadOnlyPhoneNumber">
            <DisplayName>Phone number</DisplayName>
            <DataType>string</DataType>
            <Mask Type="Simple">XXX-XXX-</Mask>
            <UserHelpText>Your mobile number</UserHelpText>
            <UserInputType>Readonly</UserInputType>
      </ClaimType>
      <!--StrongAuthenticationPhoneNumber is existing verified phone number-->
      <ClaimType Id="StrongAuthenticationPhoneNumber">
            <DisplayName>Phone Number</DisplayName>
            <DataType>string</DataType>
            <Mask Type="Simple">XXX-XXX-</Mask>
            <UserHelpText>Your telephone number</UserHelpText>
      </ClaimType>
      <!--VerificationCode is shown in the Display Control (MFA)-->
      <ClaimType Id="VerificationCode">
            <DisplayName>Verification Code</DisplayName>
            <DataType>string</DataType>
            <UserHelpText>Enter your SMS verification code</UserHelpText>
            <UserInputType>TextBox</UserInputType>
      </ClaimType>
      <!--To whom code is sent-->
      <ClaimType Id="To">
        <DataType>string</DataType>
        <UserHelpText/>
      </ClaimType>
      <!--User interface Culture-->
      <ClaimType Id="Culture">
        <DisplayName>Culture ID</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <!--Actual verify endpoint status-->
      <ClaimType Id="StatusText">
        <DisplayName>Status from SMS code verify</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <!--Expected verify endpoint status-->
      <ClaimType Id="ExpectedStatusText">
        <DisplayName>Static status for SMS code verify</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <!--Length of the verification code-->
      <ClaimType Id="CodeLength">
        <DataType>string</DataType>
        <UserHelpText/>
      </ClaimType>
    </ClaimsSchema>
</BuildingBlocks>

3. Technical Profile for handling the MFA process

This technical profile transforms the existing phone number (User's AD profile) to a read-only field and initializes Display Control. Display control defines the UI for MFA functionality.

<ClaimsProvider>
    <DisplayName>Custom SMS provider</DisplayName>
    <TechnicalProfiles>
        <!--Custom MFA (SMS) technical profile-->
        <TechnicalProfile Id="PhoneVerify-Profile">
            <DisplayName>PhoneVerify-Profile</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>
            </Metadata>
            <InputClaimsTransformations>
                <!--This transforms existing phone number to read only claim-->
                <InputClaimsTransformation ReferenceId="CopyStrongAuthenticationPhoneNumberToReadOnlyPhoneNumber" />
            </InputClaimsTransformations>
            <DisplayClaims>
                <!--Custom Display Control which shows existing phone number and verification code input field-->
                <DisplayClaim DisplayControlReferenceId="PhoneVerificationControlForSignIn" />
            </DisplayClaims>
            <OutputClaims>
                <!--List of claims which will be returned to the next orchestration step-->
                <OutputClaim ClaimTypeReferenceId="StrongAuthenticationPhoneNumber" PartnerClaimType="ReadOnlyPhoneNumber" />
            </OutputClaims>
            <UseTechnicalProfileForSessionManagement ReferenceId="SM-MFA" />          
        </TechnicalProfile>
    </TechnicalProfiles>
</ClaimsProvider>

4. Claims transformation

CopyStrongAuthenticationPhoneNumberToReadOnly transformation method uses the "FormatStringClaim" method to copy the existing phone numbers to read only field which is used in the Display Control. VerifyStatus transformation verifies that the verify API call has returned the expected status.

<BuildingBlocks>
    <ClaimsTransformations>
        <!--Copies StrongAuthenticationPhoneNumber claim value to ReadOnlyPhoneNumber-->
        <ClaimsTransformation Id="CopyStrongAuthenticationPhoneNumberToReadOnlyPhoneNumber" TransformationMethod="FormatStringClaim">
            <InputClaims>
                <InputClaim ClaimTypeReferenceId="StrongAuthenticationPhoneNumber" TransformationClaimType="inputClaim" />
            </InputClaims>
            <InputParameters>
                <InputParameter Id="stringFormat" DataType="string" Value="{0}" />
            </InputParameters>
            <OutputClaims>
                <OutputClaim ClaimTypeReferenceId="ReadOnlyPhoneNumber" TransformationClaimType="outputClaim" />
            </OutputClaims>          
        </ClaimsTransformation>
        <!--Checks that verify request is approved-->
        <ClaimsTransformation Id="VerifyStatus" TransformationMethod="AssertStringClaimsAreEqual">
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="StatusText" TransformationClaimType="inputClaim1" />
            <InputClaim ClaimTypeReferenceId="ExpectedStatusText" TransformationClaimType="inputClaim2" />
          </InputClaims>
          <InputParameters>
            <InputParameter Id="stringComparison" DataType="string" Value="ordinalIgnoreCase" />
          </InputParameters>
        </ClaimsTransformation>
    </ClaimsTransformations>
</BuildingBlocks>

5. MFA user interface

Display Control presents the phone number (read-only field) and verification code input field where the verification code from SMS is inputted. VerificationControl-based Display Control has Send and Verify Code actions which call custom REST API.

<DisplayControls>
        <!--MFA verification display control-->
       <DisplayControl Id="PhoneVerificationControlForSignIn" UserInterfaceControlType="VerificationControl">
            <InputClaims>
                <!--Incoming claims-->
                <InputClaim ClaimTypeReferenceId="ReadOnlyPhoneNumber" />
            </InputClaims>          
            <DisplayClaims>
                <!--Fields which are shown in the UI-->
                <DisplayClaim ClaimTypeReferenceId="ReadOnlyPhoneNumber" Required="true" />
                <DisplayClaim ClaimTypeReferenceId="VerificationCode" ControlClaimType="VerificationCode" Required="true" />
            </DisplayClaims>
            <OutputClaims>
                <!--Outgoing claims-->
                <OutputClaim ClaimTypeReferenceId="StrongAuthenticationPhoneNumber" />
            </OutputClaims>
            <Actions>
            <Action Id="SendCode">
                <ValidationClaimsExchange>
                    <!--Execute Send REST API call-->
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendMfaRequest" />
                </ValidationClaimsExchange>
            </Action>
            <Action Id="VerifyCode">
                <ValidationClaimsExchange>
                    <!--Execute Verify REST API call-->
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyMfaRequest" />
                </ValidationClaimsExchange>
            </Action>
            </Actions>
      </DisplayControl>
</DisplayControls>

With this configuration send verification code UI looks this. Clicking the "Send verification code" button executes the REST API call which is configured in the next step:

After a successful send operation, the verification code input field is shown like this:

6. REST API call for sending and verifying the code

This technical profile declares your API endpoints for sending and verifying the code.

<ClaimsProvider>
      <DisplayName>Custom MFA REST APIs</DisplayName>
      <TechnicalProfiles>
            <TechnicalProfile Id="SendMfaRequest">
            <DisplayName>Custom MFA (SMS) send implementation</DisplayName>
            <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
            <Metadata>
                <!--Your REST endpoint URL-->
                <Item Key="ServiceUrl">https://myapi.fi/api/send</Item>
                <Item Key="SendClaimsIn">Body</Item>
                <!--Set AuthenticationType to Basic or ClientCertificate in production environments -->
                <Item Key="AuthenticationType">Basic</Item>
            </Metadata>
            <CryptographicKeys>
                <!--Basic authentication username and password will be fetched from the B2C Policy keys-->
                <Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_ApiUsername" />
                <Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_ApiPassword" />
            </CryptographicKeys>
            <InputClaims>
                <!-- Claims sent to your REST API -->
                <InputClaim ClaimTypeReferenceId="ReadOnlyPhoneNumber" PartnerClaimType="To"/>
                <InputClaim ClaimTypeReferenceId="Culture" DefaultValue="{Culture:RFC5646}"/>
                <InputClaim ClaimTypeReferenceId="Ip" DefaultValue="{Context:IPAddress}"/>            
                <InputClaim ClaimTypeReferenceId="CodeLength" DefaultValue="6"/>            
            </InputClaims>
            <OutputClaims>
                <!-- Claims parsed from your REST API -->
            </OutputClaims>
            <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
            </TechnicalProfile>
      </TechnicalProfiles>
      <TechnicalProfile Id="VerifyMfaRequest">
          <DisplayName>Custom MFA (SMS) verify implementation</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <!--Your REST endpoint URL-->
            <Item Key="ServiceUrl">https://myapi.fi/api/verify</Item>
            <Item Key="SendClaimsIn">Body</Item>
            <!--Set AuthenticationType to Basic or ClientCertificate in production environments -->
            <Item Key="AuthenticationType">Basic</Item>
          </Metadata>
          <CryptographicKeys>
            <!--Basic authentication username and password will be fetched from the B2C Policy keys-->
            <Key Id="BasicAuthenticationUsername" StorageReferenceId="B2C_1A_ApiUsername" />
            <Key Id="BasicAuthenticationPassword" StorageReferenceId="B2C_1A_ApiPassword" />
          </CryptographicKeys>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="ReadOnlyPhoneNumber" PartnerClaimType="To"/>
            <InputClaim ClaimTypeReferenceId="VerificationCode" PartnerClaimType="Code"/>
            <InputClaim ClaimTypeReferenceId="Ip" DefaultValue="{Context:IPAddress}"/>
            <InputClaim ClaimTypeReferenceId="Culture" DefaultValue="{Culture:RFC5646}"/>
          </InputClaims>
          <OutputClaims>
            <!--Expected status from the REST API is "OK"-->
            <OutputClaim ClaimTypeReferenceId="ExpectedStatusText" DefaultValue="OK" />
            <!--Actual status-->
            <OutputClaim ClaimTypeReferenceId="StatusText"/>
          </OutputClaims>
          <OutputClaimsTransformations>
            <!--Verify that actual and expected status are same-->
            <OutputClaimsTransformation ReferenceId="VerifyStatus"/>
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
     </TechnicalProfile>
</ClaimsProvider>

7. Localization

With the LocalizedString element, you can localize DisplayControl and ClaimType texts.

<LocalizedResources Id="api.localaccountsignup.en">  
    <LocalizedStrings>
        <LocalizedString ElementType="ClaimType" ElementId="ReadOnlyPhoneNumber" StringId="DisplayName">Phone number</LocalizedString>
        <LocalizedString ElementType="ClaimType" ElementId="VerificationCode" StringId="DisplayName">Verify code</LocalizedString> 
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="intro_msg"></LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="success_send_code_msg">Verification code has been sent to</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="failure_send_code_msg">Cannot use MFA service, please try again later.</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="success_verify_code_msg">Phone number is verified. You can now continue.</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="failure_verify_code_msg">Cannot use MFA service, please try again later.</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="but_send_code">Send code</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="but_verify_code">Verify code</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="but_send_new_code">Send new code</LocalizedString>
        <LocalizedString ElementType="DisplayControl" ElementId="PhoneVerificationControlForSignIn" StringId="but_change_claims">Change phone number</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="DefaultUserMessageIfRequestFailed">Failed to establish connection to restful service end point.</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfCircuitOpen">Unable to connect to the restful service end point.</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfDnsResolutionFailed">Failed to resolve the hostname of the restful service endpoint.</LocalizedString>
        <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfRequestTimeout">Failed to establish connection to restful service end point within timeout limit.</LocalizedString>
    </LocalizedStrings>
</LocalizedResources>

That's it, now the custom MFA functionality is done from the custom policy perspective!

Other things

REST API validation errors

Check this article. According to the documentation: "If the validation failed, the REST API must return an HTTP 409 (Conflict), with the userMessage JSON element. The IEF expects the userMessage to claim that the REST API returns. This claim will be presented as a string to the user if the validation fails."

{
    "version": "1.0.1",
    "status": 409,
    "userMessage": "Code is expired."
}

UI shows the validation error like this:

Phone number change button

By default, VerificationControl has the functionality to change the delivery channel address to where the code is sent. The delivery channel can be ex. a phone number or an email address. In our case, this is not a working option because our phone number field is in read-only mode. The only way to hide this "Change" button was to use CSS. I didn't find any other ways to handle this.

I verified that if you remove the read-only attribute from the field by using Developer tools you cannot send the verification code to the other number that which is configured to the User's AD profile.

Comments