Search code examples
azure-ad-b2cidentity-experience-framework

B2C/IEF Password reset with username


I am creating a custom B2C policy and I am trying to replicate the password reset journey for local accounts created with a username.

I can read the username from AD but I am unsure how to validate the verified email address against the account.

Currently if the username is correct any email address can be used to verify.

Technical profile:

<TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
      <DisplayName>Reset password using logon name</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>
      </Metadata>
      <IncludeInSso>false</IncludeInSso>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
        <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
        <OutputClaim ClaimTypeReferenceId="objectId" />
        <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
      </OutputClaims>
      <ValidationTechnicalProfiles>
        <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
      </ValidationTechnicalProfiles>
    </TechnicalProfile>

Validation Technical Profile:

<TechnicalProfile Id="AAD-UserReadUsingLogonName">
      <Metadata>
        <Item Key="Operation">Read</Item>
        <Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
      </Metadata>
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.userName" Required="true" />
      </InputClaims>
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="objectId" />
        <OutputClaim ClaimTypeReferenceId="userPrincipalName" />
      </OutputClaims>
      <IncludeTechnicalProfile ReferenceId="AAD-Common" />
      <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
    </TechnicalProfile>

User Journey:

<UserJourney Id="PasswordReset">
  <OrchestrationSteps>
    <!--Get user by username-->
    <OrchestrationStep Order="1" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="SA-LocalAccountDiscoveryUsingLogonName" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!--Reset password-->
    <OrchestrationStep Order="2" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="SA-LocalAccountPasswordReset" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!--Read remaining attributes of user-->
    <OrchestrationStep Order="3" Type="ClaimsExchange">
      <ClaimsExchanges>
        <ClaimsExchange Id="ReadUser" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <!--Create token-->
    <OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
  </OrchestrationSteps>
  <ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>

Solution

  • If you write the email address to both the "otherMails" and "strongAuthenticationEmailAddress" properties during the sign-up policy, then you can verify that the email address is associated with the user name during the password reset policy using a REST API.

    This REST API must be declared as a claims provider:

    <ClaimsProvider>
        <DisplayName>REST APIs</DisplayName>
        <TechnicalProfiles>
            <TechnicalProfile Id="RestApi-CheckUser">
                <DisplayName>Check User REST API</DisplayName>
                <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
                <Metadata>
                    <Item Key="ServiceUrl">Insert the REST API endpoint URL</Item>
                    <Item Key="AuthenticationType">None</Item>
                    <Item Key="SendClaimsIn">Body</Item>
                </Metadata>
                <InputClaims>
                    <InputClaim ClaimTypeReferenceId="signInName" />
                    <InputClaim ClaimTypeReferenceId="email" />
                </InputClaims>
                <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
            </TechnicalProfile>
        </TechnicalProfiles>
    </ClaimsProvider>
    

    The REST API can query the user object by the "signInNames" and "otherMails" properties using the Azure AD Graph API (you can't read the "strongAuthenticationEmailAddress" property using this Graph API) and, as described in the REST API walkthrough, then return 200 OK if the email address is associated with the user name or 409 Conflict if not so.

    The REST API technical profile can then be invoked as a validation technical profile from the "SA-LocalAccountDiscoveryUsingLogonName" technical profile:

    <TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
        <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="RestApi-CheckUser" />
            <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
        </ValidationTechnicalProfiles>
    </TechnicalProfile>
    

    If the "RestApi-CheckUser" technical profile returns 200 OK, then the "AAD-UserReadUsingLogonName" technical profile is invoked and the end user can continue with the password reset. If the "RestApi-CheckUser" technical profile returns 409 Conflict, then the "AAD-UserReadUsingLogonName" technical profile isn't invoked and the end user can't continue.