Menu

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 own service provider to deliver SMS based MFA events. Article doesn't cover details about the custom REST API implementation but overall implementation looks like this:

undefined

Azure B2C custom policy configuration

1. User Journey configuration

First configure a new step to your User Journey which starts 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 MFA process

This technical profile transforms existing phone number (User's AD profile) to 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 "FormatStringClaim" method to copy existing phone number to read only field which is used in the Display Control. VerifyStatus transformation verifies that 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 phone number (read only field) and verification code input field where 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 REST API call which is configuration in the next step:

undefined

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

undefined

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

undefined

Phone number change button

By default VerificationControl has the functionality to change delivery channel address where code is sent to. 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. Only way to hide this "Change" button was to use CSS. I didn't find any other ways to handle this.

undefined

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

undefined

Comments