Detailed Help Needed for Azure AD B2C JIT Migration Password Reset Flow: Claims Transformation Issue

Ray Garg 20 Reputation points
2024-12-30T16:41:43.2333333+00:00

Context and Use Case

I’m setting up an Azure AD B2C custom policy for a Just-In-Time (JIT) migration password reset scenario, and I’m encountering persistent issues that I cannot resolve. My scenario is as follows:

  1. A user from a legacy Identity Provider (IdP) tries to reset their password.
  2. The user does not yet exist in Azure AD B2C.
  3. The expected flow:
  • The user clicks "Forgot Password" on the sign-in screen.
  • The user enters their email and verifies it.
    • If the user exists in the legacy IdP (but not in B2C), they are prompted to reset their password and are migrated to B2C.

However, I consistently encounter an error during the validation step when trying to determine if the accountEnabled claim exists. Below, I’ve detailed the steps I’ve followed, configurations, and the issues I’ve encountered, along with relevant logs and snippets.


Issue Details

The following error is observed in the b2c sign in screen after clicking forgot my password, verifying my email, and trying to click continue:

Boolean claim value comparison failed for claim type "inputClaim".

The following error is captured in Application Insights when the user attempts to verify their email and continue:

Relevant Application Insights JSON:

{

"ErrorCodes": ["UserMessageIfClaimsTransformationBooleanValueIsNotEqual"],

"ValidationTechnicalProfiles": ["CheckAccountEnabledExists", "AAD-UserReadUsingEmailAddress"],

"Exception": {

"Message": "ErrorCodes: UserMessageIfClaimsTransformationBooleanValueIsNotEqual",

"Data": {

  "IsPolicySpecificError": false

}

}

}

Additionally, I’ve previously encountered this error:


A Claim of ClaimType with id "accountEnabled" was not found, which is required by the ClaimsTransformationImpl of Type "Microsoft.Cpim.Data.Transformations.AssertBooleanClaimIsEqualToValueTransformation" for TransformationMethod "AssertBooleanClaimIsEqualToValue".

My Setup

Relevant User Journey


<UserJourneys>
  <UserJourney Id="SignInWithMigration">
  <OrchestrationSteps>

    <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
      <ClaimsProviderSelections>
        <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
        <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
        <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
        <ClaimsProviderSelection TargetClaimsExchangeId="GoogleExchange" />
      </ClaimsProviderSelections>

      <ClaimsExchanges>
        <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="2" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>objectId</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
        <ClaimsExchange Id="ForgotPasswordExchange" TechnicalProfileReferenceId="ForgotPassword" />
        <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
        <ClaimsExchange Id="GoogleExchange" TechnicalProfileReferenceId="Google-OAuth2" />
        <!-- <ClaimsExchange Id="FacebookorGoogleExchange" TechnicalProfileReferenceId="SocialAccountUserExists" />
        <ClaimsExchange Id="SocialAccountSignInExchange" TechnicalProfileReferenceId="SocialAccountSignIn" /> -->

      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="3" Type="InvokeSubJourney">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>isForgotPassword</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <JourneyList>
        <Candidate SubJourneyReferenceId="PasswordResetSub" />
      </JourneyList>
    </OrchestrationStep>

Sub-Journey for Password Reset



<SubJourneys>

  <SubJourney Id="PasswordResetSub" Type="Call">
    <OrchestrationSteps>

      <!-- Step 1: Discover if the user exists in B2C -->
      <OrchestrationStep Order="1" Type="ClaimsExchange">
        <ClaimsExchanges>
          <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" /> 
        </ClaimsExchanges>
      </OrchestrationStep>

Technical Profiles

  1. Parent Profile: LocalAccountDiscoveryUsingEmailAddress

<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
          <DisplayName>Reset password using email address</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="IpAddressClaimReferenceId">IpAddress</Item>
            <Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <IncludeInSso>false</IncludeInSso>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
            <OutputClaim ClaimTypeReferenceId="accountEnabledExists" />
            <OutputClaim ClaimTypeReferenceId="debugMessage" DefaultValue="N/A" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="CheckAccountEnabledExists" />
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" >
              <Preconditions>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
                  <Value>accountEnabledExists</Value>
                  <Action>SkipThisValidationTechnicalProfile</Action>
                </Precondition>
              </Preconditions>
            </ValidationTechnicalProfile>
          </ValidationTechnicalProfiles>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

  1. Validation Profile: CheckAccountEnabledExists

<TechnicalProfile Id="CheckAccountEnabledExists">
          <DisplayName>Check if accountEnabled claim exists</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine" />
          <!-- <InputClaims>
              <InputClaim ClaimTypeReferenceId="accountEnabled" />
          </InputClaims> -->
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="accountEnabled" DefaultValue="false" />
            <OutputClaim ClaimTypeReferenceId="accountEnabledExists" />
            <OutputClaim ClaimTypeReferenceId="debugMessage" DefaultValue="Debug: AccountEnabledExists Value = {accountEnabledExists}" />
          </OutputClaims>
          <OutputClaimsTransformations>
              <OutputClaimsTransformation ReferenceId="CheckIfAccountEnabledExists" />
          </OutputClaimsTransformations>
        </TechnicalProfile>
  1. Claims Transformation: CheckIfAccountEnabledExists

<ClaimsTransformation Id="CheckIfAccountEnabledExists" TransformationMethod="DoesClaimExist">
        <InputClaims>
            <InputClaim ClaimTypeReferenceId="accountEnabled" TransformationClaimType="inputClaim" />
        </InputClaims>
        <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="accountEnabledExists" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>

  1. Validation Profile: AAD-UserReadUsingEmailAddress

<TechnicalProfile Id="AAD-UserReadUsingEmailAddress">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
          </InputClaims>
          <OutputClaims>

            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />

            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="displayName" />
            <OutputClaim ClaimTypeReferenceId="accountEnabled" />
            <OutputClaim ClaimTypeReferenceId="otherMails" />
            <OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="AssertAccountEnabledIsTrue" />
          </OutputClaimsTransformations>

          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>

What I’ve Tried

Added Default Values: Added DefaultValue="false" for accountEnabled and accountEnabledExists to avoid null issues.

  1. Introduced Precondition: Attempted to skip AAD-UserReadUsingEmailAddress by introducing the precondition:
       
       <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
    
    Checked Claims Bag: Used CheckAccountEnabledExists to confirm if accountEnabled exists and output its result into accountEnabledExists. Tried to Debug: Enabled App Insights logging and tracked accountEnabledExists but still receive the error during the AssertBooleanClaimIsEqualToValue transformation.

Documentation Followed

I followed Microsoft’s documentation to set up the password reset flow. Specifically:

  • Step 3 invokes the PasswordResetSub sub-journey.
  • The sub-journey calls the LocalAccountDiscoveryUsingEmailAddress technical profile.

Help Needed

I’m seeking guidance on the following:

Why does the AssertBooleanClaimIsEqualToValue transformation fail with accountEnabled even though CheckIfAccountEnabledExists should skip AAD-UserReadUsingEmailAddress if the claim is missing?

How can I ensure that accountEnabledExists correctly reflects the presence of accountEnabled in the claims bag, and the policy handles this gracefully when the user is not found in B2C?

Any assistance or insights would be greatly appreciated!

ASP.NET
ASP.NET
A set of technologies in the .NET Framework for building web applications and XML web services.
3,552 questions
0 comments No comments
{count} votes

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.