azure-ad-b2cazure-ad-b2c-custom-policy

Azure B2C: Edit Profile via a single page


Use case/goal: allow users to change their personal info from a single "page".

A user self-registers for a local account with

Both phone number and email address are verified during initial registration. The verification process is a standard one-time code (OTP) sent to destination via SMS (phone) or email, then entered by the user. For the purpose of this question, assume that OTP implementation is based on out of the box technical profiles, e.g. PhoneFactor or Entra (AzureMFA) for SMS and Entra or AAD-SSPR for email verification.

Post-registration, the user would like to change this info. Changing the email and phone number requires verification on the new email/new phone number.

Target UX for Edit Profile page:

Our attempt:

<DisplayControls>
    <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
        <DisplayClaims>
            <DisplayClaim ClaimTypeReferenceId="email" Required="true"/>
            <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true"/>
        </DisplayClaims>
        <OutputClaims/>
        <Actions>
            <Action Id="SendCode">
                <ValidationClaimsExchange>
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AadSspr-SendCode"/>
                </ValidationClaimsExchange>
            </Action>
            <Action Id="VerifyCode">
                <ValidationClaimsExchange>
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AadSspr-VerifyCode"/>
                </ValidationClaimsExchange>
            </Action>
        </Actions>
    </DisplayControl>
    <DisplayControl Id="phoneVerificationControl" UserInterfaceControlType="VerificationControl">
        <DisplayClaims>
            <DisplayClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" Required="true"/>
            <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true"/>
        </DisplayClaims>
        <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" Required="true"/>
        </OutputClaims>
        <Actions>
            <Action Id="SendCode">
                <ValidationClaimsExchange>
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="PhoneFactor-InputOrVerify"/>
                </ValidationClaimsExchange>
            </Action>
            <Action Id="VerifyCode">
                <ValidationClaimsExchange>
                    <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="PhoneFactor-InputOrVerify"/>
                </ValidationClaimsExchange>
            </Action>
        </Actions>
    </DisplayControl>
</DisplayControls>

<TechnicalProfile Id="AAD-UserWriteProfileUsingObjectId">
    <Metadata>
        <Item Key="Operation">Write</Item>
        <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
    </Metadata>
    <IncludeInSso>false</IncludeInSso>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress"/>
        <InputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber"/>
        <InputClaim ClaimTypeReferenceId="surname"/>
        <InputClaim ClaimTypeReferenceId="givenName"/>
        <InputClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>
    </InputClaims>
    <PersistedClaims>
        <!-- Required claims -->
        <PersistedClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress"/>
        <PersistedClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password"/>
        <PersistedClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="strongAuthenticationPhoneNumber"/>
        <!-- Optional claims. -->
        <PersistedClaim ClaimTypeReferenceId="givenName"/>
        <PersistedClaim ClaimTypeReferenceId="surname"/>
    </PersistedClaims>
    <OutputClaims/>
    <IncludeTechnicalProfile ReferenceId="AAD-Common"/>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD"/>
</TechnicalProfile>

<TechnicalProfile Id="SelfAsserted-ProfileUpdate">
    <DisplayName>Edit Profile</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.selfasserted.profileupdate</Item>
    </Metadata>
    <IncludeInSso>false</IncludeInSso>
    <InputClaims>
        <InputClaim ClaimTypeReferenceId="objectId"/>
        <InputClaim ClaimTypeReferenceId="givenName"/>
        <InputClaim ClaimTypeReferenceId="surname"/>
    </InputClaims>
    <DisplayClaims>
        <DisplayClaim DisplayControlReferenceId="emailVerificationControl"/>
        <DisplayClaim DisplayControlReferenceId="phoneVerificationControl"/>
        <DisplayClaim ClaimTypeReferenceId="givenName" Required="true"/>
        <DisplayClaim ClaimTypeReferenceId="surName" Required="true"/>
        <DisplayClaim ClaimTypeReferenceId="newPassword" Required="true"/>
        <DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true"/>
    </DisplayClaims>
    <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId"/>
        <OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true"/>
        <OutputClaim ClaimTypeReferenceId="email"/>
        <OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber"/>
        <OutputClaim ClaimTypeReferenceId="givenName"/>
        <OutputClaim ClaimTypeReferenceId="surname"/>
    </OutputClaims>
    <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="AAD-UserWriteProfileUsingObjectId"/>
    </ValidationTechnicalProfiles>
    <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD"/>
</TechnicalProfile>

<UserJourney Id="EditProfile">
    <OrchestrationSteps>
        <OrchestrationStep Order="1" Type="ClaimsProviderSelection" ContentDefinitionReferenceId="api.idpselections">
            <ClaimsProviderSelections>
                <ClaimsProviderSelection TargetClaimsExchangeId="LocalAccountSigninEmailExchange"/>
            </ClaimsProviderSelections>
        </OrchestrationStep>
        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email"/>
            </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="3" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId"/>
            </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="4" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="B2CUserProfileUpdateExchange" TechnicalProfileReferenceId="SelfAsserted-ProfileUpdate"/>
            </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="5" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="AADUserWriteWithObjectId" TechnicalProfileReferenceId="AAD-UserWriteProfileUsingObjectId"/>
            </ClaimsExchanges>
        </OrchestrationStep>
        <OrchestrationStep Order="6" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
    </OrchestrationSteps>
    <ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>

Solution

  • You can achieve this by making your email address and phone number fields display controls. This allows updating them on a single screen while maintaining the one-time password verification.

    Create a display control for email and phone verification (example below is email verification)

    <DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
      <InputClaims></InputClaims>
      <DisplayClaims>
        <DisplayClaim ClaimTypeReferenceId="email" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" />
      </DisplayClaims>
      <OutputClaims></OutputClaims>
      <Actions>
        <Action Id="SendCode">
          <ValidationClaimsExchange>
            <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AadSspr-SendCode" />
          </ValidationClaimsExchange>
        </Action>
        <Action Id="VerifyCode">
          <ValidationClaimsExchange>
            <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AadSspr-VerifyCode" />
          </ValidationClaimsExchange>
        </Action>
      </Actions>
    </DisplayControl>
    

    Use Microsofts email provider or bring your own (like twilio or sendgrid)

    <Action Id="SendCode">
      <ValidationClaimsExchange>
        <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AadSspr-SendEmail" />
      </ValidationClaimsExchange>
    </Action>
    

    Reference the display controls on the screen you wish to display them while editing profile.

     <DisplayClaims>
        <DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
        <DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
        <DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
        <DisplayClaim DisplayControlReferenceId="PhoneVerificationControl" />
    

    You may wish for them to enter their old password to confirm it IS them and not someone else using a SSO session.

    Reference: Microsoft Learn: B2C Display Controls