Arjun
Arjun

Reputation: 643

Azure AD B2C Custom Policies to support Email, Phone and TOTP options together

I am working with Azure AD B2c Custom policies for the following purpose. I want to provide EMail, Phone and Authenticator App(TOTP) MFA options. User can chose any one of the option among those 3 options and AD B2C user flow should work according to selected MFA Choice.

We have PhoneOrEMail XML configuration from the following sample:

https://github.com/azure-ad-b2c/samples/blob/master/policies/mfa-email-or-phone/policy/SignUpOrSignin_PhoneOrEmailMFA.xml

We have TOTP XML configuration from the following sample:

https://github.com/azure-ad-b2c/samples/blob/master/policies/totp/policy/TrustFrameworkExtensions_TOTP.xml

I tried to add TOTP configuration also inside SignUpOrSignin_PhoneOrEmailMFA.xml file by adding additional option for "extension_mfaByPhoneOrEmail" Claim Type like following

<ClaimType Id="extension_mfaByPhoneOrEmail">
        <DisplayName>Please select your preferred MFA method</DisplayName>
        <DataType>string</DataType>
        <UserInputType>RadioSingleSelect</UserInputType>
        <Restriction>
          <Enumeration Text="Phone" Value="phone" SelectByDefault="true" />
          <Enumeration Text="Email " Value="email" SelectByDefault="false" />
          <Enumeration Text="Authenticator App " Value="TOTP" SelectByDefault="false" />
        </Restriction>
</ClaimType>

after adding this, i had modified user journey like below:

<UserJourney Id="SignUpOrSignInMFAOption" DefaultCpimIssuerTechnicalProfileReferenceId="JwtIssuer">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
          <ClaimsProviderSelections>
            <!-- <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange"/> -->
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
          </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 -->
        <!-- ContentDefinitionReferenceId="api.signin" -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>objectId</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <!-- New precondition to check for TOTP selection -->
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <!-- <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH"/> -->
            <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
            <!-- <ClaimsExchange Id="TOTPExchange" TechnicalProfileReferenceId="TOTP-Technical-Profile" /> -->
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- For social IDP authentication, attempt to find the user account in the directory. -->
        <!-- <OrchestrationStep Order="3" 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="4" 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="3" 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="4" 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> -->
        <!--Sample: If uses is enrolled for MFA, ask the user to select the preferred method-->
        <OrchestrationStep Order="4" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="SelfAsserted-Select-MFA-Method" TechnicalProfileReferenceId="SelfAsserted-Select-MFA-Method" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Throw error if control was bypassed -->
        <OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="Return-MFA-Method-Incorrect-Error">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>email</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>phone</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
        </OrchestrationStep>
        <!-- Phone verification: If MFA is not required, the next three steps (#5-#7) should be removed.
             This step checks whether there's a phone number on record,  for the user. If found, then the user is challenged to verify it. -->
        <OrchestrationStep Order="6" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isActiveMFASession</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <!--Sample: If the preferred MFA method is not 'phone' skip this orchestration step-->
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>phone</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="PhoneFactor-Verify" TechnicalProfileReferenceId="PhoneFactor-InputOrVerify" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Save MFA phone number: The precondition verifies whether the user provided a new number in the 
             previous step. If so, then the phone number is stored in the directory for future authentication 
             requests. -->
        <OrchestrationStep Order="7" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>newPhoneNumberEntered</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="AADUserWriteWithObjectId" TechnicalProfileReferenceId="AAD-UserWritePhoneNumberUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!--Sample: MFA with email-->
        <OrchestrationStep Order="8" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>email</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="Email-Verify" TechnicalProfileReferenceId="EmailVerifyOnSignIn" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="9" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>TOTP</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="SignUpOrSignInTOTP" />
          </JourneyList>
        </OrchestrationStep>
        <OrchestrationStep Order="10" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>newUser</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>newUser</Value>
              <Value>false</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="TrackUserSignUp" TechnicalProfileReferenceId="AzureInsights-UserSignup" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="11" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
        <!-- Handles the user clicking the sign up link in the local account sign in page -->
        <OrchestrationStep Order="12" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="TrackSignInComplete" TechnicalProfileReferenceId="AzureInsights-SignInComplete" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

I am invoking TOTP flow via SubJourney from Orchestration Step 9.

I am able to successfully uploaded the file to azure ad b2c able to register and login successfully with email, phone and authenticator app.

But, encountering an issue where the MFA selection screen keeps appearing for the authenticator app option, even after the initial setup

I want to hide the MFA screen for authenticator app also similar to phone and email post initial selection. It should not show selection screen again.

I know this is possibly extension_mfaByPhoneOrEmail claim persistency for TOTP option selection.

Need help to fix this issue.

Upvotes: 0

Views: 291

Answers (2)

Anton Yelchin
Anton Yelchin

Reputation: 1

In case anyone faces similar issue, I found out why MFA selection screen keeps appearing for TOTP. The reason is that, once you select your desired MFA on orchestration step 4, using SelfAsserted-Select-MFA-Method it doesn't persist the value. It happens after, for example, if you have selected email as your MFA option, Technical Profile EmailVerifyOnSignIn used during code verification has a following reference:

<ValidationTechnicalProfiles>
  <ValidationTechnicalProfile ReferenceId="AAD-UserWriteMFAMethod"></ValidationTechnicalProfile>
</ValidationTechnicalProfiles>

UserWriteMFAMethod actually saves your value and is not being referenced by TOTP related flow. So the fix would be following, since I want to save selected MFA method only after user has made a verification, I edited OTPVerification like this:

        <TechnicalProfile Id="OTPVerification">
          <DisplayName>Sign in with Authenticator app</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.totp</Item>
            <Item Key="language.button_continue">Verify</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaims></InputClaims>
          <DisplayClaims>
            <DisplayClaim ClaimTypeReferenceId="QrCodeVerifyInstruction" />
            <DisplayClaim ClaimTypeReferenceId="otpCode" Required="true" />
          </DisplayClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="otpCode" Required="true" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AzureMfa-VerifyOTP" />
<!-- THIS WAS ADDED TO SAVE SELECTED MFA METHOD -->
            <ValidationTechnicalProfile ReferenceId="AAD-UserWriteMFAMethod" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-MFA-TOTP" />
        </TechnicalProfile>

Then you can reference your extension claim to skip TOTP steps:

        <!-- 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">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>totpApp</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Input" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Call the TOTP validation sub journey-->
        <OrchestrationStep Order="9" Type="InvokeSubJourney">
        <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>extension_mfaByPhoneOrEmail</Value>
              <Value>totpApp</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="TotpFactor-Verify" />
          </JourneyList>
        </OrchestrationStep>

Provided examples are mainly from Microsoft Authenticator TOTP and MFA with either Phone (Call/SMS) or Email verification, merged together.

Upvotes: 0

Arjun
Arjun

Reputation: 643

Basically, TOTP Flow is using following numberOfAvailableDevices which will have value only after user TOTP successful registration. So, i have added this check as part of orchestration step and able to skip phone and email orchestration steps and able to complete the user flow.

Following is the orchestration step

 <OrchestrationStep Order="4" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
          <Value>newUser</Value>
          <Value>True</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="GetAvailableDevicesForUser" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
      </ClaimsExchanges>
    </OrchestrationStep>

And AzureMfa-GetAvailableDevices would have following configuration.

<TechnicalProfile Id="AzureMfa-GetAvailableDevices">
      <DisplayName>Get Available Devices</DisplayName>
      <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureMfaProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      <Metadata>
        <Item Key="Operation">GetAvailableDevices</Item>
      </Metadata>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="userPrincipalName" />
      </InputClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="numberOfAvailableDevices" />
      </OutputClaims>
    </TechnicalProfile>

This will ensure logged in user have numberOfAvailableDevices>0 and we can make necessary changes according to our orchestration steps.

Upvotes: 0

Related Questions