AAD Custom Policies - Password change confirmation email

Isaac Zolana 90 Reputation points
2024-04-22T22:23:17.7166667+00:00

Context

We have configured a journey where the user would be able to change his password if he wishes to do so.

We use Sendgrid to have a custom email verification according to the documentation provided here : https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-email-sendgrid?pivots=b2c-custom-policy

Once the email has been verified, the user can change his password.

Objective

For security reasons, we would like to send a password change email to the user so that he is aware that the changes has been applied.

Configuration

*Note: Not to overload the post and for reasons of confidentiality, some elements are hidden using "[]"

Relying party policy

<?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="[tenant].onmicrosoft.com"
  PolicyId="[change password policy id]"
  PublicPolicyUri="http://[tenant].onmicrosoft.com/[change password policy id]"
  DeploymentMode="Development"
  UserJourneyRecorderEndpoint="urn:journeyrecorder:applicationinsights"
  >

  <BasePolicy>
    <TenantId>[tenant].onmicrosoft.com</TenantId>
    <PolicyId>[extension policy id]</PolicyId>
  </BasePolicy>
  
  <ClaimsProviders>
    <ClaimsProvider>
      <DisplayName>Change password</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
          <Metadata>
            <Item Key="includeClaimResolvingInClaimsHandling">true</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="email" DefaultValue="{OAUTH-KV:EmailHint}" AlwaysUseDefaultValue="true" />
          </InputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="ChangePassword" />
    <Endpoints>
      <!--points to refresh token journey when app makes refresh token request-->
      <Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
    </Endpoints>
    <UserJourneyBehaviors>
      <JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="[...]" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
      <ScriptExecution>Allow</ScriptExecution>
    </UserJourneyBehaviors>
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="oid"/>
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
        <OutputClaim ClaimTypeReferenceId="trustFrameworkPolicy" Required="true" DefaultValue="{policy}" PartnerClaimType="tfp" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>
</TrustFrameworkPolicy>

Extension policy

<?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="[tenant].onmicrosoft.com"
  PolicyId="[extension policy id]"
PublicPolicyUri="http://[tenant].onmicrosoft.com/[extension policy id]">
  <BasePolicy>
    <TenantId>[tenant].onmicrosoft.com</TenantId>
    <PolicyId>[extension policy id]</PolicyId>
  </BasePolicy>
  <BuildingBlocks>
    <ClaimsSchema>
      <ClaimType Id="trustFrameworkPolicy">
        <DisplayName>Trust framework policy name</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="emailRequestBody">
        <DisplayName>SendGrid request body</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
      <ClaimType Id="subject">
        <DisplayName>The subject of the email</DisplayName>
        <DataType>string</DataType>
      </ClaimType>
    </ClaimsSchema>
    <ClaimsTransformations>
	
	  [GenerateEmailRequestBody]		

      <ClaimsTransformation Id="GenerateChangedPasswordEmailRequestBody" TransformationMethod="GenerateJson">
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />
          <InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.dynamic_template_data.email" />
          <InputClaim ClaimTypeReferenceId="subject" TransformationClaimType="personalizations.0.dynamic_template_data.subject" />
        </InputClaims>
        <InputParameters>
          <!-- Update the template_id value with the ID of your SendGrid template. -->
          <InputParameter Id="template_id" DataType="string" Value="[...]" />
          <InputParameter Id="from.email" DataType="string" Value="[...]" />
          <InputParameter Id="from.name" DataType="string" Value="[...]" />
          <!-- Update with a subject line appropriate for your organization. -->
          <InputParameter Id="personalizations.0.dynamic_template_data.subject" DataType="string" Value="Verification code" />
          <InputParameter Id="personalizations.0.dynamic_template_data.support_team_link" DataType="string" Value="[...]" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="emailRequestBody" TransformationClaimType="outputClaim" />
        </OutputClaims>
      </ClaimsTransformation>
    </ClaimsTransformations>
    <DisplayControls>
      <!-- Display control for sendgrid -->
      <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
        <DisplayClaims>
          <DisplayClaim ClaimTypeReferenceId="email" Required="true" />
          <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode"
            Required="true" />
        </DisplayClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="email" />
        </OutputClaims>
        <Actions>
          <Action Id="SendCode">
            <ValidationClaimsExchange>
              <!-- <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress" ContinueOnError="false" /> -->
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" />
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendOtp" />
            </ValidationClaimsExchange>
          </Action>
          <Action Id="VerifyCode">
            <ValidationClaimsExchange>
              <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
            </ValidationClaimsExchange>
          </Action>
        </Actions>
      </DisplayControl>
    </DisplayControls>
  </BuildingBlocks>
  <ClaimsProviders>
    <ClaimsProvider>
      <DisplayName>Local Account SignIn</DisplayName>
      <TechnicalProfiles>
		
		<!-- Handles the verification code step -->
        <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.forgotpassword</Item>
            <!-- Remove the cancel button -->
            <Item Key="setting.showCancelButton">false</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="objectId" />
            <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>
		
		<!-- Handles the password reset step -->
        <TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
          <DisplayName>Change password (username)</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.forgotpassword</Item>
            <!-- Remove the cancel button -->
            <Item Key="setting.showCancelButton">false</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
          </CryptographicKeys>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="objectId" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
            <OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectId" />
          </ValidationTechnicalProfiles>
        </TechnicalProfile>
		
		<TechnicalProfile Id="SendPasswordChangeConfirmationMessage">
          <DisplayName>Use SendGrid's email API to send a password change confirmation message</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="ServiceUrl">https://api.sendgrid.com/v3/mail/send</Item>
            <Item Key="AuthenticationType">Bearer</Item>
            <Item Key="SendClaimsIn">Body</Item>
            <Item Key="ClaimUsedForRequestPayload">emailRequestBody</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="BearerAuthenticationToken" StorageReferenceId="[SendgridSecret]" />
          </CryptographicKeys>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="GenerateChangedPasswordEmailRequestBody" />
          </InputClaimsTransformations>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="emailRequestBody" />
          </InputClaims>
        </TechnicalProfile>
		
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <UserJourneys>
    <UserJourney Id="ChangePassword">
      <OrchestrationSteps>
        <!-- Validate user's email address. -->
        <OrchestrationStep Order="1" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <!-- Collect and persist a new password. -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="LocalAccountWritePasswordUsingObjectId" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
          <ClaimsExchanges>
            <ClaimsExchange Id="PasswordChangeConfirmationExchange" TechnicalProfileReferenceId="SendPasswordChangeConfirmationMessage" />
          </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
      </OrchestrationSteps>
    </UserJourney>

  </UserJourneys>
</TrustFrameworkPolicy>

Problem

In the step 3 of our orchestration step, we are unable to receive the email that was inputted by the user in step 1 to construct the body of the email.

Question

How can we make sure that the email is passed along the orchestration steps, knowing that it might also be prepopulated using an email hint "{OATH-KV:EmailHint}" ?

Extra information

Thanks to application insights as well as Azure AD B2C Visual Studio Code extension, I was able to see the claims in step 3 of my user journey.

With this, I know that only the email is missing.

User's image

Microsoft Security | Microsoft Entra | Microsoft Entra ID
{count} votes

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.