Azure AD B2C - 忘记密码用户之旅 - 不允许使用旧密码?

Azure AD B2C - Forgot Password User Journey - Don't Allow old password?

我正在基于自定义策略构建 Azure AD B2C 配置。登录、个人资料编辑、密码更改等都已按需运行。

但目前我正在努力解决密码忘记政策。我想实现新密码不等于旧密码。 Google 并且 Microsoft 文档总是为我提供更改密码的示例。当我更改密码时,我必须输入旧密码和新密码。然后我就可以比较旧的和新的了。例如像描述的方式 here

但是当用户忘记密码时,他当然无法输入旧密码与新密码进行比较。

有什么方法可以建立一个真正的密码忘记策略,无需输入旧密码,但仍能确保新密码不等于旧密码?

提前致谢!

亚历克斯

您可以使用验证技术配置文件的一些逻辑来完成此操作:

  1. continueOnError=true
  2. 调用login-noninteractive
  3. 如果声明(如 objectId)为 null
  4. ,则调用 claimTransform 生成 boolean
  5. 使用boolean作为处理逻辑,我们称之为pwdIsLastPwd
  6. 调用 claimTransform 断言 pwdIsLastPwd = false
  7. 如果是 true,则使用 claimTransform 错误处理程序抛出错误 - “您不能使用此密码”
  8. 继续重置密码流程的其余部分

参考文献:

  1. https://docs.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile#validationtechnicalprofiles
  2. Call Claim transform from VTP, Boolean ClaimTransform check if claim exists
  3. Assert boolean is true/false
  4. “UserMessageIfClaimsTransformationBooleanValueIsNotEqual 自断言技术配置文件元数据控制技术配置文件向用户显示的错误消息。”

我遇到了同样的问题,Jas Suri 的回答对我帮助很大。但是,我在让它工作时遇到了一些问题。所以,这就是为什么我要分享我的最终解决方案,以防其他人遇到同样的问题。

我使用以下内容作为基础:https://github.com/azure-ad-b2c/samples/tree/master/policies/password-reset-not-last-password/policy 但做了一些调整。以下是我必须添加到我的现有政策中才能使其生效的内容:

ClaimSchema:

稍后将使用这些声明。

    <ClaimsSchema>
      <ClaimType Id="SamePassword">
        <DisplayName>samePassword</DisplayName>
        <DataType>boolean</DataType>
        <UserHelpText />
      </ClaimType>
      <ClaimType Id="resetPasswordObjectId">
        <DisplayName>User's Object ID</DisplayName>
        <DataType>string</DataType>
        <DefaultPartnerClaimTypes>
          <Protocol Name="OAuth2" PartnerClaimType="oid" />
          <Protocol Name="OpenIdConnect" PartnerClaimType="oid" />
          <Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/objectidentifier" />
        </DefaultPartnerClaimTypes>
        <UserHelpText>Object identifier (ID) of the user object in Azure AD.</UserHelpText>
      </ClaimType>
    </ClaimsSchema>

声明转换:

第一个声明转换检查是否设置了 resetPasswordObjectId,如果未设置,则先前尝试使用新密码登录显然无效,因此 newPassword 与旧/当前密码不同。第二个声明转换检查声明 SamePassword 是否等于 false。如果不是,它会抛出错误并显示“您不能使用旧密码”。稍后会详细介绍。

 <ClaimsTransformations>
  <ClaimsTransformation Id="CheckPasswordEquivalence" TransformationMethod="DoesClaimExist">
    <InputClaims>
      <InputClaim ClaimTypeReferenceId="resetPasswordObjectId" TransformationClaimType="inputClaim" />
    </InputClaims>
    <OutputClaims>
      <OutputClaim ClaimTypeReferenceId="SamePassword" TransformationClaimType="outputClaim" />
    </OutputClaims>
  </ClaimsTransformation>
  <ClaimsTransformation Id="AssertSamePasswordIsFalse" TransformationMethod="AssertBooleanClaimIsEqualToValue">
    <InputClaims>
      <InputClaim ClaimTypeReferenceId="SamePassword" TransformationClaimType="inputClaim" />
    </InputClaims>
    <InputParameters>
      <InputParameter Id="valueToCompareTo" DataType="boolean" Value="false" />
    </InputParameters>
  </ClaimsTransformation>
</ClaimsTransformations>

索赔提供商:

总体思路是使用新输入的密码并尝试登录用户。如果登录成功,newPassword 与旧/当前密码相同且不允许。登录时,我们创建一个输出声明并将其称为 resetPasswordObjectId,它未设置或等于已登录用户的对象 ID。然后我们检查 resetPasswordObjectId 是否存在(在声明转换部分完成),如果不存在则可以使用 newPassword 因为它与旧/当前密码。

为了在用户输入旧密码时显示正确的错误消息,我们需要在 <LocalizedResources Id="api.localaccountpasswordreset.en"> 中的 TrustFrameworkLocalization.xml 中重写 UserMessageIfClaimsTransformationBooleanValueIsNotEqual这个<LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">You must not use your old password.</LocalizedString>.

使用下面的声明提供程序时,请确保更换所有标有 TODO 的部件。

   <ClaimsProviders>
<ClaimsProvider>
  <DisplayName>Password Reset without same password</DisplayName>
  <TechnicalProfiles>
    <TechnicalProfile Id="login-NonInteractive-PasswordChange">
      <DisplayName>Local Account SignIn</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <Metadata>
        <Item Key="UserMessageIfClaimsPrincipalDoesNotExist">We can't seem to find your account</Item>
        <Item Key="UserMessageIfInvalidPassword">Your password is incorrect</Item>
        <Item Key="UserMessageIfOldPasswordUsed">Looks like you used an old password</Item>
        <Item Key="ProviderName">https://sts.windows.net/</Item>
        <!-- TODO  replace YOUR-TENANT-ID -->
        <Item Key="METADATA">https://login.microsoftonline.com/YOUR-TENANT.onmicrosoft.com/.well-known/openid-configuration</Item>
        <!-- TODO replace YOUR-TENANT-ID -->
        <Item Key="authorization_endpoint">https://login.microsoftonline.com/YOUR-TENANT.onmicrosoft.com/oauth2/token</Item>
        <Item Key="response_types">id_token</Item>
        <Item Key="response_mode">query</Item>
        <Item Key="scope">email openid</Item>
        <!-- TODO ensure this line is commented out-->
        <!-- <Item Key="grant_type">password</Item> -->
        <Item Key="UsePolicyInRedirectUri">false</Item>
        <Item Key="HttpBinding">POST</Item>
        <!-- TODO -->
        <!-- ProxyIdentityExperienceFramework application / client id -->
        <Item Key="client_id">YOUR-PROXY-CLIENT-ID</Item>
        <!-- Native App -->
        <!-- TODO -->
        <!-- IdentityExperienceFramework application / client id -->
        <Item Key="IdTokenAudience">YOUR-IDENTITY-CLIENT-ID</Item>
        <!-- Web Api -->
      </Metadata>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
        <!-- INFO: replaced oldPassword with newPassword, that way we try logging in with the new password. If the login is successful, we know the newPassword is the same as the old / current password-->
        <InputClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" Required="true" />
        <InputClaim ClaimTypeReferenceId="grant_type" DefaultValue="password" />
        <InputClaim ClaimTypeReferenceId="scope" DefaultValue="openid" />
        <InputClaim ClaimTypeReferenceId="nca" PartnerClaimType="nca" DefaultValue="1" />
        <!-- TODO -->
        <!-- ProxyIdentityExperienceFramework application / client id -->
        <InputClaim ClaimTypeReferenceId="client_id" DefaultValue="YOUR-PROXY-CLIENT-ID" />
        <!-- TODO -->
        <!-- IdentityExperienceFramework application / client id -->
        <InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="YOUR-IDENTITY-CLIENT-ID" />
      </InputClaims>
      <OutputClaims>
        <!-- INFO: assign the objectId (oid) to resetPasswordObjectId, since the claim objectId might already be set. In that case there would be no way of knowing whether it was set due to the attempted login with the newPassword-->
        <OutputClaim ClaimTypeReferenceId="resetPasswordObjectId" PartnerClaimType="oid" />
        <OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
        <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
        <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
        <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
        <OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
        <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
      </OutputClaims>
    </TechnicalProfile>
    <!--Logic to check new password is not the same as old password
                    Validates old password before writing new password-->
    <TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
      <DisplayName>Reset password</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.localaccountpasswordreset</Item>
        <!-- set in the TrustFrameworkLocalization.xml  -->
        <!-- <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">You must not use your old password.</Item> -->
      </Metadata>
      <CryptographicKeys>
        <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
      </CryptographicKeys>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
        <OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
      </OutputClaims>
      <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="login-NonInteractive-PasswordChange" ContinueOnError="true" />
        <ValidationTechnicalProfile ReferenceId="ComparePasswords" />
        <ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectId">
          <Preconditions>
            <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
              <Value>SamePassword</Value>
              <Value>True</Value>
              <Action>SkipThisValidationTechnicalProfile</Action>
            </Precondition>
          </Preconditions>
        </ValidationTechnicalProfile>
      </ValidationTechnicalProfiles>
    </TechnicalProfile>
    <!-- Runs claimsTransformations to make sure new and old passwords differ -->
    <TechnicalProfile Id="ComparePasswords">
      <DisplayName>Compare Email And Verify Email</DisplayName>
      <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="SamePassword" />
      </OutputClaims>
      <OutputClaimsTransformations>
        <OutputClaimsTransformation ReferenceId="CheckPasswordEquivalence" />
        <OutputClaimsTransformation ReferenceId="AssertSamePasswordIsFalse" />
      </OutputClaimsTransformations>
    </TechnicalProfile>
  </TechnicalProfiles>
</ClaimsProvider>