I used a custom policy to create the login screen. And we want to run MFA on a per user basis. For example, I have two user account.(user1, user2) User 1 wants to log in without using MFA. User 2 applies MFA and wants to log in. Both users then access the same URL for login. Where can we set up an MFA for each user in this way?
2 Answers
I've been able to implement self-service per user TOTP MFA enrollment/unenrollment via custom policies (one policy for enrollment, second one for unenrollment).
The idea in my scenario was that it is up to the end user to decide if he would like to use TOTP MFA or not.
So far this has been only a PoC implementation but it was working fine during my tests.
It was based on a TOTP MFA sample available on GitHub - https://github.com/azure-ad-b2c/samples/tree/master/policies/totp.
Relevant parts of my UserJourneys:
- Enable MFA UserJourney
<UserJourney Id="EnableMFA" DefaultCpimIssuerTechnicalProfileReferenceId="JwtIssuer">
<OrchestrationSteps>
//Default steps for signing in user with Local/Social account
[...]
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CreateErrorResponse-MfaAlreadyEnabled" TechnicalProfileReferenceId="CreateErrorResponse-MfaAlreadyEnabled" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="ReturnOAuth2Error">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Input" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Verify" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Step 5 reads numberOfAvailableDevices
which indicates wheter user is already enrolled (numberOfAvailableDevices=1) or not (numberOfAvailableDevices=0).
If user is already enrolled, steps 6 & 7 are creating and returning an error to notify user he cannot enable MFA as it is already enabled.
Steps 8 & 9 are enrolling user. These steps are taken from GitHub sample.
- Disable MFA UserJourney
Here, the AzureMfaProtocolProvider
technical provider doesn't support unenrollment of TOTP MFA. List of available operations can be found here - https://learn.microsoft.com/en-us/azure/active-directory-b2c/multi-factor-auth-technical-profile#totp-mode.
In order to unenroll user you can use Microsoft Graph (custom REST technical profile to your API is needed).
Using these endpoints, you can list softwareOathMethods
for specific user and delete them if needed. If user has TOTP MFA enabled, then MS Graph should return an array with 1 softwareOathMethod
.
UserJourney:
<UserJourney Id="DisableMFA" DefaultCpimIssuerTechnicalProfileReferenceId="JwtIssuer">
<OrchestrationSteps>
//Default steps for signing in user with Local/Social account
[...]
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CreateErrorResponse-MfaNotEnabled" TechnicalProfileReferenceId="CreateErrorResponse-MfaNotEnabled" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="ReturnOAuth2Error">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Verify" />
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="REST-DisableMfa" TechnicalProfileReferenceId="REST-DisableMfa" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="10" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Steps 5,6 & 7 are similar to corresponding steps in previously mentioned EnableMFA
UserJourney.
Step 8 forces user to input TOTP code from his authenticator app before disabling MFA so we are sure he is the owner of the account.
In step 9, REST-DisableMfa
technical profile is my custom REST api which then calls Microsoft Graph api.

- 168
- 1
- 12
Using the build-in user flows, it's not possible. You'll need to use a custom policy and allow the MFA based on some custom property (e.g. role admin / regular user).
Here's a link that can help you with some guidance related to implement custom MFA:
https://www.kallemarjokorpi.fi/blog/azure-b2c-custom-mfa-implementation.html

- 17,332
- 6
- 45
- 90
-
thank you! And, I found this page. https://learn.microsoft.com/en-us/azure/active-directory/authentication/howto-mfa-userstates Is this irrelevant? – Pepe Mar 07 '22 at 15:00
-
that's for Azure AD, I'm not sure if it's valid for b2c too as I never had to implement such thing – Thiago Custodio Mar 07 '22 at 15:02