Hello Team,
We've deployed a user authentication flow using Azure AD B2C Custom policy.
Our process entails sending an email notification to new Local B2C users, prompting them to set a password and enable MFA (Multi-Factor Authentication).
Here's the expected behavior for this flow:
- User sets password.
- User is prompted with a QR code, which they scan using their authenticator app.
- User enters the code generated by the authenticator app.
- Successful authentication and redirection to redirect_uri.
However, we've encountered an issue that needs addressing:
- User sets password.
- User is prompted with a QR code, which they scan using their authenticator app.
- User is prompted to enter the code. After entering the code, they are prompted again to enter another code.
- User waits for the app to generate a new code and enters the new code (as the same code doesn't work again).
- Successful authentication and redirection to redirect_uri.
Additionally, we've implemented a feature allowing users to reset MFA. However, when users attempt to set up new MFA, they encounter the same behavior of entering the MFA code twice.
Ideally, we aim to design our flows in a manner that if MFA is not set up or not found, users are prompted to scan the QR code and enter the code. Otherwise, they should only be asked to enter the code.
Could you please assist us in rectifying this flow?
<UserJourneys>
<!-- START UserJourney | Set new password - MFA - JWT -->
<UserJourney Id="SetNewUserPassword">
<OrchestrationSteps>
<!-- Read the input claims from the id_token_hint-->
<OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />
<!-- Check if user tries to run the policy without valid id_token -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingPresetEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Setting new password -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Call the TOTP validation sub journey-->
<OrchestrationStep Order="5" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>isKnownCustomer</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Input"/>
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="6" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>isKnownCustomer</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<JourneyList>
<Candidate SubJourneyReferenceId="TotpFactor-Verify"/>
</JourneyList>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="RESTGetPortalUserClaims" TechnicalProfileReferenceId="REST-GetPortalUserClaims"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackRESTGetPortalUserClaims" TechnicalProfileReferenceId="AppInsights-REST-GetPortalUserClaims" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="9" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
<OrchestrationStep Order="10" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSetNewUserPassword" TechnicalProfileReferenceId="AppInsights-SetNewUserPassword" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
Here are the sub journeys -
<SubJourneys>
<!-- START SubJourney | TOTP -->
<!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
<SubJourney Id="SetTotpInitialValue" Type="Call">
<OrchestrationSteps>
<!-- If number of available device claim not exists, set the value to 0-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpDefaultValue" TechnicalProfileReferenceId="SetTotpDefaultValue"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- The following orchestration steps try to get the user identifier for different
type of authentication, such as local and social account.-->
<!-- Try to get the identifier from UserId-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>totpIdentifier</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>UserId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpIdentifierAsUserId" TechnicalProfileReferenceId="CreateTotpIdentifier-UserId"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Get the identifier from email-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>totpIdentifier</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpIdentifierAsEmail" TechnicalProfileReferenceId="CreateTotpIdentifier-Email"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Get the identifier from the user's emails -->
<OrchestrationStep Order="4" Type="InvokeSubJourney">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>totpIdentifier</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>emails</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<JourneyList>
<Candidate SubJourneyReferenceId="ExtractEmailFromEmailsForTotpIdentifier"/>
</JourneyList>
</OrchestrationStep>
<!-- Get the identifier from local account sign-in name-->
<OrchestrationStep Order="5" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>totpIdentifier</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>signInName</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpIdentifierAsSignInName" TechnicalProfileReferenceId="CreateTotpIdentifier-SignInName"/>
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
<!-- Get the identifier from the user's emails -->
<SubJourney Id="ExtractEmailFromEmailsForTotpIdentifier" Type="Call">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>emails</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="GetEmailAddress" TechnicalProfileReferenceId="GetEmailAddress"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>ReadOnlyEmail</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="GetEmailFromReadOnlyEmail" TechnicalProfileReferenceId="GetEmailFromReadOnlyEmail"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpIdentifierAsEmail" TechnicalProfileReferenceId="CreateTotpIdentifier-Email"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>signInName</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetTotpIdentifierAsSignInName" TechnicalProfileReferenceId="CreateTotpIdentifier-SignInName"/>
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
<!-- TOTP verification sub journey-->
<SubJourney Id="TotpFactor-Verify" Type="Call">
<OrchestrationSteps>
<!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
<OrchestrationStep Order="1" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="SetTotpInitialValue"/>
</JourneyList>
</OrchestrationStep>
<!-- If current user is not a new one (this is a sign-in flow, and not sign-up),
check the number of available devices. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>newUser</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- If the number of available devices isn't zero (user has enrolled before),
render the TOTP verification page -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AuthenticatorForSignIn" TechnicalProfileReferenceId="OTPVerification"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackTotpFactorVerify" TechnicalProfileReferenceId="AppInsights-TotpFactor-Verify" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
<!-- TOTP enrollment sub journey-->
<SubJourney Id="TotpFactor-Input" Type="Call">
<OrchestrationSteps>
<!-- Set the required claims numberOfAvailableDevices and totpIdentifier-->
<OrchestrationStep Order="1" Type="InvokeSubJourney">
<JourneyList>
<Candidate SubJourneyReferenceId="SetTotpInitialValue"/>
</JourneyList>
</OrchestrationStep>
<!-- If current user is not a new one (this is a sign-in flow, and not sign-up),
check the number of available devices. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>newUser</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="CheckAvailableDevices" TechnicalProfileReferenceId="AzureMfa-GetAvailableDevices"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- If the number of available devices is zero (user hasn't enrolled before),
render the TOTP enrollment page to scan the QR code that starts the enrollment process -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AuthenticatorForSignUp" TechnicalProfileReferenceId="EnableOTPAuthentication"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- If the number of available devices is zero (user hasn't enrolled before),
render the TOTP verification page. -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>numberOfAvailableDevices</Value>
<Value>0</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AuthenticatorForSignIn" TechnicalProfileReferenceId="OTPVerification"/>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackTotpFactorInput" TechnicalProfileReferenceId="AppInsights-TotpFactor-Input" />
</ClaimsExchanges>
</OrchestrationStep>
</OrchestrationSteps>
</SubJourney>
<!-- END SubJourney | TOTP -->
</SubJourneys>