azure-ad-b2cazure-ad-b2c-custom-policy

Azure AD B2C Custom Policy Exception on ClaimType


I'm fairly new to Custom Policies in general, and would appreciate some guidance so that I'm not just testing random things and hoping something works.

I have the below Claims Provider set up in my policy:

<ClaimsProvider>
  <DisplayName>Azure Active Directory</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="AAD-ReadCommon">
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="extension_PhoneNumber" />
      </OutputClaims>
    </TechnicalProfile>
    <TechnicalProfile Id="AAD-WriteCommon">
      <!-- Transform optional claims (given name, surname) to proper display name -->
      <InputClaimsTransformations>
        <InputClaimsTransformation ReferenceId="CreateDisplayNameFromFirstNameAndLastName" />
      </InputClaimsTransformations>
      <PersistedClaims>
        <PersistedClaim ClaimTypeReferenceId="givenName" />
        <PersistedClaim ClaimTypeReferenceId="surname" />
        <PersistedClaim ClaimTypeReferenceId="extension_PhoneNumber" />
      </PersistedClaims>
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>

Note that the CreateDisplayNameFromFirstNameAndLastName transformation is a custom transformation I created:

<ClaimsTransformation Id="CreateDisplayNameFromFirstNameAndLastName" TransformationMethod="FormatStringMultipleClaims">
  <InputClaims>
    <InputClaim ClaimTypeReferenceId="givenName" TransformationClaimType="inputClaim1" />
    <InputClaim ClaimTypeReferenceId="surName" TransformationClaimType="inputClaim2" />
  </InputClaims>
  <InputParameters>
    <InputParameter Id="stringFormat" DataType="string" Value="{0} {1}" />
  </InputParameters>
  <OutputClaims>
    <OutputClaim ClaimTypeReferenceId="displayName" TransformationClaimType="outputClaim" />
  </OutputClaims>
</ClaimsTransformation>

I'm getting the following error, specifically on expired password, inside the Technical Profile AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin:

A Claim of ClaimType with id "givenName" was not found, which is required by the ClaimsTransformationImpl of Type "Microsoft.Cpim.Data.Transformations.FormatStringMultipleClaimsTransformation" for TransformationMethod "FormatStringMultipleClaims" referenced by the ClaimsTransformation with id "CreateDisplayNameFromFirstNameAndLastName"```

Digging a bit further, I see that AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin has <IncludeTechnicalProfile ReferenceId="AAD-WriteCommon" /> listed in it, which I think explains what is happening - essentially, the chain of events dies on my custom transformation, because it is asking for two claims (givenName and surname), which the technical profile AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin does not provide.

I tried a few attempts at getting it to work, by adding those two fields as input claims to AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin, or to SelfAsserted-ForcePasswordReset-ExpiredPassword, which is the profile that is actually invoking it, but neither seemed to work.

Note that I'm not actually trying to change the experience for the end-user at all. On the screen where they are entering their password to update, there are no prompts asking for their name, nor is there any place where the name is actually intended to be displayed. If the solution involves making the CreateDisplayNameFromFirstNameAndLastName transformation somehow validate and skip the process if the information isn't available, I'm perfectly happy with that solution as well.

If there's any other information I can provide to help, please let me know. Thanks!

UPDATE 1 Okay, so I've done some more exploring, and I think I have a better understanding of where everything is.

My RP has this, from which I'm fairly sure the journey involved in this case would be IdentityProviderSelection_SignUpSignIn:

  <SubJourneys>
    <SubJourney Id="IdentityProviderSelection_SignUpSignIn" Type="Call">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signinandsignupwithpassword1.1">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPassword" />
          </ClaimsProviderSelections>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
    <SubJourney Id="IdentityProviderSelection_LocalAccountDiscovery" Type="Call">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="ClaimsProviderSelection">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection TargetClaimsExchangeId="PasswordResetUsingEmailAddressExchange" />
          </ClaimsProviderSelections>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>
  </SubJourneys>

IdentityProviderSelection_SignUpSignIn, in turn, looks like this, from which, if I understand right, step 2 would be skipped as we're not using a social provider, and steps 3-5 would be skipped due to their conditions:

    <SubJourney Id="IdentityProviderSelection_SignUpSignIn" Type="Call">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signinandsignupwithpassword1.1">
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email-V3" />
            <ClaimsExchange Id="LocalAccountSigninUsernameExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Username-V3" />
            <ClaimsExchange Id="LocalAccountSigninPhoneExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Phone-Only-V2" />
            <ClaimsExchange Id="LocalAccountSigninPhoneEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-PhoneEmail-V3" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Check if the user has selected to sign in using one of the social providers -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>authenticationSource</Value>
              <Value>socialIdpAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isLocalAccountAuthentication</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="MicrosoftAccountExchange" TechnicalProfileReferenceId="MSA-OIDC" />
            <ClaimsExchange Id="GoogleExchange" TechnicalProfileReferenceId="Google-OAUTH" />
            <ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
            <ClaimsExchange Id="LinkedInExchange" TechnicalProfileReferenceId="LinkedIn-OAUTH" />
            <ClaimsExchange Id="AmazonExchange" TechnicalProfileReferenceId="Amazon-OAUTH" />
            <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail-Selection" />
            <ClaimsExchange Id="SignUpWithLogonNameExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonName-Selection" />
            <ClaimsExchange Id="SignUpWithLogonPhoneExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonPhone" />
            <ClaimsExchange Id="ReadAndCreateSsoSession" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId-CreateSubject" />
            <ClaimsExchange Id="ChangePhoneNumber" TechnicalProfileReferenceId="PhoneInputPage_ChangePhoneNumber" />
            <ClaimsExchange Id="ForgotPassword" TechnicalProfileReferenceId="ForgotPassword" />
            <ClaimsExchange Id="WeiboExchange" TechnicalProfileReferenceId="Weibo-OAUTH" />
            <ClaimsExchange Id="QQExchange" TechnicalProfileReferenceId="QQ-OAUTH" />
            <ClaimsExchange Id="WeChatExchange" TechnicalProfileReferenceId="WeChat-OAUTH" />
            <ClaimsExchange Id="TwitterExchange" TechnicalProfileReferenceId="Twitter-OAUTH1" />
            <ClaimsExchange Id="GitHubExchange" TechnicalProfileReferenceId="GitHub-OAUTH2" />
            <ClaimsExchange Id="AppleManagedExchange" TechnicalProfileReferenceId="Apple-Managed-OIDC" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isForgotPassword</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>passwordExpired</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="LocalAccount_SignUp" />
          </JourneyList>
        </OrchestrationStep>
        <!-- Performs additonal steps for phone/combination based signin -->
        <OrchestrationStep Order="4" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isForgotPassword</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>passwordExpired</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="AdditionalUserJourneySteps_V2" />
          </JourneyList>
        </OrchestrationStep>
        <OrchestrationStep Order="5" Type="InvokeSubJourney">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>blockUserSignUp</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isForgotPassword</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>passwordExpired</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <JourneyList>
            <Candidate SubJourneyReferenceId="AdditionalVerification" />
          </JourneyList>
        </OrchestrationStep>
        <OrchestrationStep Order="6" Type="ClaimsExchange">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
              <Value>continueOnPasswordExpiration</Value>
              <Value>True</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
              <Value>passwordExpired</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
            <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
              <Value>isForgotPassword</Value>
              <Action>SkipThisOrchestrationStep</Action>
            </Precondition>
          </Preconditions>
          <ClaimsExchanges>
            <ClaimsExchange Id="ForcePasswordResetUponPasswordExpiration" TechnicalProfileReferenceId="SelfAsserted-ForcePasswordReset-ExpiredPassword" />
          </ClaimsExchanges>
        </OrchestrationStep>
      </OrchestrationSteps>
    </SubJourney>

Lastly, SelfAsserted-ForcePasswordReset-ExpiredPassword is the place which invokes the write:

        <TechnicalProfile Id="SelfAsserted-ForcePasswordReset-ExpiredPassword">
          <DisplayName>Password Expired</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.expiredpassword</Item>
            <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Please enter a different password</Item>
            <Item Key="TokenLifeTimeInSeconds">3600</Item>
            <Item Key="AllowGenerationOfClaimsWithNullValues">true</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_JwtTokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="SetResponseMessageForForcedPasswordReset" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="responseMsg" DefaultValue="Your password has expired, please change to a new password." />
          </InputClaims>
          <DisplayClaims>
            <DisplayClaim ClaimTypeReferenceId="responseMsg" />
            <DisplayClaim ClaimTypeReferenceId="password" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
            <DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
          </DisplayClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="eSTS-NonInteractive" />
            <ValidationTechnicalProfile ReferenceId="ThrowErrorWhenPassowrdIsSame" />
            <ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
        </TechnicalProfile>

I did a bunch of experimenting, and crucially, it looks like for whatever reason, step6 in my journey, where SelfAsserted-ForcePasswordReset-ExpiredPassword is being invoked, does not have objectId, even though it seems to be getting set in the first step of the journey. I tried adding it in as an input claim, but that didn't seem to do anything.

For completeness, the code for AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin is:

        <TechnicalProfile Id="AAD-UserWritePasswordUsingObjectIdWithoutStrongAuth-ResetNextLogin">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" />
            <PersistedClaim ClaimTypeReferenceId="forceChangePasswordNextLogin" PartnerClaimType="passwordProfile.forceChangePasswordNextLogin" DefaultValue="false" AlwaysUseDefaultValue="true" />
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-WriteCommon" />
        </TechnicalProfile>

Solution

  • Noticed that I'd never posted an answer for this. We ended up discovering that this problem is more-or-less already "solved" in the base version of the policy, however, because we had a custom policy, we didn't get those updates. In the end here is how we solved the problem:

    1. In the following Technical Profiles:

    We added the following entry to the section:

    <PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="None" />

    1. In the AAD-WriteCommon Technical Profile, we changed the following entry in the section: <PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="DisablePasswordExpiration" /> to: <PersistedClaim ClaimTypeReferenceId="passwordPolicies" DefaultValue="None" />

    2. In the SubJourney IdentityProviderSelection_SignUpSignIn, we added the following :

    <OrchestrationStep Order="6" Type="InvokeSubJourney">
      <Preconditions>
        <Precondition Type="ClaimEquals" ExecuteActionsIf="false">
          <Value>continueOnPasswordExpiration</Value>
          <Value>True</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>passwordExpired</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>isForgotPassword</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <JourneyList>
        <Candidate SubJourneyReferenceId="IdentityProviderSelection_LocalAccountDiscovery" />
      </JourneyList>
    </OrchestrationStep>
    
    1. Lastly, in the same SubJourney, we updated the existing OrchestrationStep with Order="6" to change its Order to 7.