Jack A.
Jack A.

Reputation: 4453

Validation error uploading Azure AD B2C policy

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>

Upvotes: 3

Views: 1707

Answers (2)

Jack A.
Jack A.

Reputation: 4453

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>

Upvotes: 2

Jumpy
Jumpy

Reputation: 53

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

Upvotes: 1

Related Questions