McBodge
McBodge

Reputation: 111

AD B2C - How to set up custom email verification in Password Reset flow

I have a requirement of customizing the email sent to the user, from AD B2C, when she/he resets the password.

I followed this documentation to set up the self-service password reset flow, and it works fine: https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-password-reset-policy?pivots=b2c-custom-policy

To provide a branded email for the password reset, I'm following this code, since it looks like that the only other option is to use Display Controls, which are currently in public preview (so I cannot use them in production): https://github.com/azure-ad-b2c/samples/tree/master/policies/custom-email-verifcation

The readme clearly states that it can be used also for the password reset, but the code only provides an example for the sign in email verification.

I tried to add the verificationCode OutputClaim in various TechnicalProfiles, but I'm unable to visualize the custom verificationCode textbox that is needed by the provided javascript code.

I'm thinking that maybe I should use a specific ContentDefinition, but I'm really struggling to find the correct way to update the custom policy xml.

Update to clarify: In the sign up example, the verification code is added to the LocalAccountSignUpWithLogonEmail TechnicalProfile:

<ClaimsProvider>
        <DisplayName>Local Account</DisplayName>
        <TechnicalProfiles>
          <TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
              <DisplayName>Email signup</DisplayName>
              <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
              <Metadata>
                <!-- Demo: Disable the email verification-->
                <Item Key="EnforceEmailVerification">False</Item>
              </Metadata>
              <OutputClaims>
                <OutputClaim ClaimTypeReferenceId="objectId"/>
                <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true"/>
                
                <!--Demo: Add the verification code claim type-->
                <OutputClaim ClaimTypeReferenceId="verificationCode" Required="true"/>

Since I'm working on the password reset (orchestrated by the following SubJourney), we can see that it references the LocalAccountDiscoveryUsingEmailAddress TechnicalProfile in the first step:

    <SubJourney Id="PasswordReset" Type="Call">
          <OrchestrationSteps>
            <!--Sample: Validate user's email address. Run this step only when user resets the password-->
            <OrchestrationStep Order="1" Type="ClaimsExchange">
              <ClaimsExchanges>
                <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
              </ClaimsExchanges>
            </OrchestrationStep>
    
            <!--Sample: Collect and persist a new password. Run this step only when user resets the password-->
            <OrchestrationStep Order="2" Type="ClaimsExchange">
              <ClaimsExchanges>
                <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
              </ClaimsExchanges>
            </OrchestrationStep>
          </OrchestrationSteps>
        </SubJourney>

Therefore, I added verificationCode to the LocalAccountDiscoveryUsingEmailAddress TechnicalProfile:

    <!-- This technical profile forces the user to verify the email address that they provide on the UI. Only after email is verified, the user account is
    read from the directory. -->
    <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>
      </Metadata>
      <CryptographicKeys>
        <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
      </CryptographicKeys>
      <IncludeInSso>false</IncludeInSso>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="verificationCode" Required="true"/>
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
        <OutputClaim ClaimTypeReferenceId="objectId" />
        <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
        <OutputClaim ClaimTypeReferenceId="authenticationSource" />
      </OutputClaims>
      <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="REST-EmailVerification"/>
        <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
      </ValidationTechnicalProfiles>
    </TechnicalProfile>

But the related TextBox is not rendered in the page.

Update 2: I found out why the text box is not rendered. It's related to the used ContentDefinition. By using the api.selfasserted.profileupdate content definition despite the api.localaccountpasswordreset one, the field is displayed. Now I'm still working on it.

Update 3: I was able to make it work using the api.selfasserted.profileupdatecontent definition. I'll post the complete solution as soon as I finish the integration with the validation APIs.

Upvotes: 2

Views: 2962

Answers (2)

McBodge
McBodge

Reputation: 111

The solution was to use the api.selfasserted.profileupdate content definition (instead of api.localaccountpasswordreset) for the LocalAccountDiscoveryUsingEmailAddress technical profile.

        <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.selfasserted.profileupdate</Item>
            <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
            <Item Key="EnforceEmailVerification">false</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="verificationCode" Required="true" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="REST-EmailVerification" />
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>

It looks mostly as a workaround, but it's the only option not to use the preview features of the display controls.

To further protect the validation, there is an API-side check in the REST-EmailVerification validation technical profile, that re-checks the code previously validated client-side:

          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="REST-EmailVerification" />
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>

Additionally I'm currently adding a captcha to avoid abuses of the sending logics.

As soon as the display controls will be generally available, I'll reccomend my client to use them.

The reason why it didn't work with the password-reset-specific content definition, is that it doesn't support other custom fields: https://learn.microsoft.com/it-it/azure/active-directory-b2c/contentdefinitions enter image description here

Upvotes: 1

Jas Suri - MSFT
Jas Suri - MSFT

Reputation: 11335

Swap verified.email output claim with the reference to your displayControl in the technical profile for password reset, which is LocalAccountDiscoveryUsingEmailAddress. https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-email-sendgrid#make-a-reference-to-the-displaycontrol

Its essentially the exact same steps, except you make the "make a reference" change to the LocalAccountDiscoveryUsingEmailAddress technical profile to show the display control on this specific page, which is referenced in Step 1 of the password reset journey to collect and verify the users email.

        <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>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <IncludeInSso>false</IncludeInSso>
          <DisplayClaims>
            <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
          </DisplayClaims>
          <OutputClaims>
            <!--<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />-->
            <OutputClaim ClaimTypeReferenceId="email" />
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />

And if you want a different email template for password reset compared to Sign Up, recreate a new displayControl and reference a different template.

Upvotes: 1

Related Questions