3

I created an Azure AD B2C tenant with custom policies last year. Now I am trying to upload the same policies (with IDs changed as necessary) to a new tenant that we have just created and I get the following error when uploading the reset-password policy:

Validation failed: 1 validation error(s) found in policy "B2C_1A_PASSWORDRESET" of tenant "xxx.onmicrosoft.com".Persisted claims for technical profile "AAD-FlipMigratedFlag" in policy "B2C_1A_PasswordReset" of tenant "xxx.onmicrosoft.com" must have one of the following claims: userPrincipalName

These policies implement the Seamless Migration approach to user migration, based on samples in the following repositories:

https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack
https://github.com/azure-ad-b2c/samples
https://github.com/azure-ad-b2c/user-migration

As suggested by the error message, I have tried adding userPrincipalName to the PersistedClaims for the AAD-FlipMigratedFlag technical profile, but I get the same error when uploading the policy.

I have also tried re-uploading the existing, working reset-password policy to the existing, working tenant, and I get the same error. Note that in this case I am re-uploading the exact same policy that has already been successfully uploaded and has been working for a year.

So the question is: what has changed and what do I need to do to fix this error?

Here are the relevant parts of my custom policy files. If there are any other parts you need to see, just let me know and I'll add them.

PasswordReset.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="xxx.onmicrosoft.com"
  PolicyId="B2C_1A_PasswordReset"
  PublicPolicyUri="http://xxx.onmicrosoft.com/B2C_1A_PasswordReset">

  <BasePolicy>
    <TenantId>xxx.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>
  
  <RelyingParty>
    <DefaultUserJourney ReferenceId="PasswordReset" />
    <UserJourneyBehaviors>
      <ScriptExecution>Allow</ScriptExecution>
    </UserJourneyBehaviors>
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="emails" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

TrustFrameworkExtensions.xml

<?xml version="1.0" encoding="utf-8" ?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="xxx.onmicrosoft.com"
  PolicyId="B2C_1A_TrustFrameworkExtensions"
  PublicPolicyUri="http://xxx.onmicrosoft.com/B2C_1A_TrustFrameworkExtensions">

  <BasePolicy>
    <TenantId>xxx.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>
  </BasePolicy>

  <BuildingBlocks>
    <ClaimsSchema>
      <!-- Holds the value of the migration status on the Azure AD B2C account -->
      <ClaimType Id="extension_IsMigrationRequired">
        <DisplayName>extension_IsMigrationRequired</DisplayName>
        <DataType>boolean</DataType>
        <AdminHelpText>extension_IsMigrationRequired</AdminHelpText>
        <UserHelpText>extension_IsMigrationRequired</UserHelpText>
      </ClaimType>
      <!-- Holds the value of whether the authentication succeeded at the legacy IdP -->
      <ClaimType Id="tokenSuccess">
        <DisplayName>tokenSuccess</DisplayName>
        <DataType>boolean</DataType>
        <AdminHelpText>tokenSuccess</AdminHelpText>
        <UserHelpText>tokenSuccess</UserHelpText>
      </ClaimType>
      <!-- Holds the value 'false' when the legacy IdP authentication succeeded -->
      <ClaimType Id="migrationRequired">
        <DisplayName>migrationRequired</DisplayName>
        <DataType>boolean</DataType>
        <AdminHelpText>migrationRequired</AdminHelpText>
        <UserHelpText>migrationRequired</UserHelpText>
      </ClaimType>
    </ClaimsSchema>
  </BuildingBlocks>

  <ClaimsProviders>

    <ClaimsProvider>
      <DisplayName>Local Account Password Reset - Write Password</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="Get-requiresMigration-status-password-reset" ContinueOnError="false" />
            <ValidationTechnicalProfile ReferenceId="AAD-FlipMigratedFlag" ContinueOnError="false">
              <Preconditions>
                <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
                  <Value>extension_IsMigrationRequired</Value>
                  <Value>False</Value>
                  <Action>SkipThisValidationTechnicalProfile</Action>
                </Precondition>
              </Preconditions>
            </ValidationTechnicalProfile>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectId" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Local Account Password Reset - Read migration flag</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="Get-requiresMigration-status-password-reset">
          <Metadata>
            <Item Key="Operation">Read</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
            <Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="extension_IsMigrationRequired" DefaultValue="false" />
          </OutputClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Local Account Password Reset - Flip migration flag</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="AAD-FlipMigratedFlag">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="migrationRequired" PartnerClaimType="extension_IsMigrationRequired" DefaultValue="false" AlwaysUseDefaultValue="true"/>
            <!-- NOTE: I added this but still get the error -->
            <PersistedClaim ClaimTypeReferenceId="userPrincipalName" />
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Azure Active Directory</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="AAD-Common">
          <Metadata>
            <!--Insert b2c-extensions-app application ID here, for example: 11111111-1111-1111-1111-111111111111-->  
            <Item Key="ClientId">xxx</Item>
            <!--Insert b2c-extensions-app application ObjectId here, for example: 22222222-2222-2222-2222-222222222222-->
            <Item Key="ApplicationObjectId">xxx</Item>
          </Metadata>
        </TechnicalProfile>
      </TechnicalProfiles> 
    </ClaimsProvider>

  </ClaimsProviders>

</TrustFrameworkPolicy>

TrustFrameworkBase.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="xxx.onmicrosoft.com"
  PolicyId="B2C_1A_TrustFrameworkBase"
  PublicPolicyUri="http://xxx.onmicrosoft.com/B2C_1A_TrustFrameworkBase">

  <ClaimsProviders>
    <ClaimsProvider>
      <TechnicalProfiles>
        <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>
            <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
            <Item Key="IncludeClaimResolvingInClaimsHandling">true</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="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>

        <TechnicalProfile Id="AAD-UserWritePasswordUsingObjectId">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>

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

        <TechnicalProfile Id="AAD-Common">
          <DisplayName>Azure Active Directory</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.AzureActiveDirectoryProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>

          <!-- We need this here to suppress the SelfAsserted provider from invoking SSO on validation profiles. -->
          <IncludeInSso>false</IncludeInSso>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <UserJourneys>
    <UserJourney Id="PasswordReset">
      <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

  </UserJourneys>
</TrustFrameworkPolicy>
Jack A.
  • 4,245
  • 1
  • 20
  • 34
  • As per the policy that was working before and you can't update it anymore, looks like they have different contents. Have you tried to download the working one to compare it against the other one? – basquiatraphaeu Feb 19 '22 at 14:26
  • @basquiat Just to be sure I'm not crazy I went ahead and downloaded the custom policies for the existing, working tenant. There are only whitespace differences between what I downloaded and the original sources I'm working with. – Jack A. Feb 20 '22 at 14:04

2 Answers2

2

I was hoping someone from MS would chime in on this. Since that hasn't happened, I'll go ahead and post my solution.

MS has a bad habit of creating schema where the order of the nodes matter. That was part of the problem here. In addition, the displayName claim was also added to the required list.

So after some trial-and-error, this version of the AAD-FlipMigratedFlag claims provider ended up being the solution:

    <ClaimsProvider>
      <DisplayName>Local Account Password Reset - Flip migration flag</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="AAD-FlipMigratedFlag">
          <Metadata>
            <Item Key="Operation">Write</Item>
            <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
          </Metadata>
          <IncludeInSso>false</IncludeInSso>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" Required="true" />
          </InputClaims>
          <PersistedClaims>
            <PersistedClaim ClaimTypeReferenceId="objectId" />
            <PersistedClaim ClaimTypeReferenceId="displayName" />
            <PersistedClaim ClaimTypeReferenceId="userPrincipalName" />
            <PersistedClaim ClaimTypeReferenceId="migrationRequired" PartnerClaimType="extension_IsMigrationRequired" DefaultValue="false" AlwaysUseDefaultValue="true"/>
          </PersistedClaims>
          <IncludeTechnicalProfile ReferenceId="AAD-Common" />
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
Jack A.
  • 4,245
  • 1
  • 20
  • 34
  • The question remains -- why do the put out samples that fail validation? – Jakub Bochenski Jun 02 '22 at 16:23
  • Where are the userPrincipalName and displayName claims populated? – milorad Jun 23 '22 at 12:28
  • Also where do you set the password that the user enters? The point of the migration is to update the user with their password which is validated on another system. – milorad Jun 23 '22 at 12:32
  • 1
    @milorad The `userPrincipalName` and `displayName` claims are populated by the `LocalAccountDiscoveryUsingEmailAddress` technical profile (see the original question). The password is set by the `LocalAccountWritePasswordUsingObjectId` technical profile. – Jack A. Jun 23 '22 at 16:14
  • 1
    @milorad Keep in mind that the user flow being discussed here is "Reset Password". When the user invokes this flow, we are skipping the old password validation step. Once the password has been reset, it is important to clear the "is migration required" flag on the account so that the login flow uses the password stored in Azure instead of calling the migration API to validate the old password. – Jack A. Jun 23 '22 at 16:15
  • Makes sense, I also have this use case. For me what solved the issue when writing the password, was using objectId as the InputClaim instead of the signInName, for some reason AAD-Write cannot do an update of an user when using signInName – milorad Jun 23 '22 at 19:04
1

Have yout tried to add the userPrincipalName claim to the OutputClaims section of PasswordReset.xml?

Jumpy
  • 53
  • 5