Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Password forget link in "Dynamic sign up or sign in" sample #390

Closed
Tvde1 opened this issue Apr 4, 2022 · 9 comments
Closed

Add Password forget link in "Dynamic sign up or sign in" sample #390

Tvde1 opened this issue Apr 4, 2022 · 9 comments

Comments

@Tvde1
Copy link

Tvde1 commented Apr 4, 2022

I'd like to add the "Forgot your password?" link to the dynamic sign up or sign in example.

I have tried adding

<Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
<Item Key="setting.forgotPasswordLinkLocation">AfterInput</Item>

to the selfasserted technical profile (and other possible forgotPasswordLinkLocation values). I also tried using the api.signuporsignin ContentDefinitionReferenceId instead of the api.selfasserted.

Example:

<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Dynamic">
	<DisplayName>Local Account Signin</DisplayName>
	<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
	<Metadata>
		<Item Key="SignUpTarget">SignUpWithLogonEmailExchange</Item>
		<Item Key="setting.operatingMode">Email</Item>
		<Item Key="ContentDefinitionReferenceId">api.signuporsignin</Item>
		<Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
		<Item Key="setting.showSignupLink">true</Item>
		<Item Key="setting.forgotPasswordLinkLocation">AfterInput</Item>
	</Metadata>
	<IncludeInSso>false</IncludeInSso>

Any idea how I can add the forgot password link to the main page and to the log-in page when the user's email exists?

@LuisDev99
Copy link

LuisDev99 commented Apr 4, 2022

Related to #389 (waiting for solution)

@JasSuri
Copy link
Contributor

JasSuri commented Apr 5, 2022

That would only work if the orchestration step is switched from type: claimsexchange to type: combinedsignupsignin. This would look like the starter pack example.

After doing that, it’ll work, but you’ll have to use CSS to hide the password field.

This is a policy design question, so I suggest continuing on stackoverflow.

@LuisDev99
Copy link

Also @JasSuri, I tried your suggestion, like so:

 <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="signuporsignin-phone-email">
          <ClaimsProviderSelections>
            <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninPhoneEmailExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="SignUpWithEmail" />
            <ClaimsProviderSelection TargetClaimsExchangeId="SignUpWithPhone" />
            <ClaimsProviderSelection TargetClaimsExchangeId="ChangePhoneNumber" />
            <ClaimsProviderSelection TargetClaimsExchangeId="GoogleExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
            <ClaimsProviderSelection TargetClaimsExchangeId="AppleExchange" />
          </ClaimsProviderSelections>
          <ClaimsExchanges>
            <ClaimsExchange Id="LocalAccountSigninPhoneEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Phone-Email" />
          </ClaimsExchanges>
        </OrchestrationStep>

But it did not have any effect. Here's the technical profile that the LocalAccountSigninPhoneEmailExchange is calling:

<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Phone-Email">
          <DisplayName>Local Account Signin Using Phone Email</DisplayName>
          <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
          <Metadata>
            <Item Key="setting.operatingMode">Username</Item>
            <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Please enter a valid phone number or email address.</Item>
            <Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
            <Item Key="setting.forgotPasswordLinkLocation">AfterInput</Item>
          </Metadata>
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="signInName" DefaultValue="{OIDC:LoginHint}" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
            <OutputClaim ClaimTypeReferenceId="phoneNumber" />
            <OutputClaim ClaimTypeReferenceId="email" />
            <OutputClaim ClaimTypeReferenceId="isLocalAccountSignIn" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="ValidateUsernameType" />
          </ValidationTechnicalProfiles>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
        </TechnicalProfile>

@LuisDev99
Copy link

I was able to fix the issue by following the suggested solution. tho it required alot of extra work and steps. Will be working later on to post the solution in here

@JasSuri
Copy link
Contributor

JasSuri commented Apr 10, 2022

Feel free to make a PR with your sample to this repo.

@JasSuri JasSuri closed this as completed Apr 10, 2022
@Tvde1
Copy link
Author

Tvde1 commented Apr 14, 2022

@LuisDev99 What did you end up doing to make it appear?

@LuisDev99
Copy link

LuisDev99 commented Apr 15, 2022

So, first thing first, I used the phonenumber-passwordless sample. This sample has the PhoneEmailBase.xml, which I ended up modyfing to add the link. Yours might be different (but it should not matter since I'm hopefully going to explain the little bits you need to do to make it work on any sample).

1. First thing first, you want to add the api.signuporsignin Content Definition under ContentDefinitions if you don't have it already (because in one of the orchestration steps below, we will add this content definition which has the link) and make sure the DataUri has unifiedsssp:2.1.5 (this is important because this is the HTML that has the link), like so:

<ContentDefinitions>
  <ContentDefinition Id="api.signuporsignin"> 
    <LoadUri>~/tenant/templates/AzureBlue/selfAsserted.cshtml</LoadUri>
    <RecoveryUri>~/common/default_page_error.html</RecoveryUri>
    <DataUri>urn:com:microsoft:aad:b2c:elements:contract:unifiedssp:2.1.5</DataUri>
    <Metadata>
      <Item Key="DisplayName">Collect information from user page</Item>
      <Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
      <!-- <Item Key="setting.forgotPasswordLinkLocation">AfterInput</Item> -->
    </Metadata>
  </ContentDefinition>
</ContentDefinitions>

Notice the <Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item> part, this is how we tell b2c to add the link BUT there's more steps because if not, it wont work and it wont appear.

2. Secondly, we will add the following claim:

<ClaimsSchema>
  <ClaimType Id="isForgotPassword">
    <DisplayName>isForgotPassword</DisplayName>
    <DataType>boolean</DataType>
    <AdminHelpText>Whether the user has selected Forgot your Password</AdminHelpText>
  </ClaimType>
</ClaimsSchema>

This will be used for walking the user journey later on. Embedded password example line reference

3. Add the following Local Account claim provider block (which contains two technical profiles we need):

<ClaimsProviders>
  <ClaimsProvider>
    <DisplayName>Local Account</DisplayName>
    <TechnicalProfiles>

      <!-- Set the isForgotPassword to true-->
      <TechnicalProfile Id="ForgotPassword">
        <DisplayName>Forgot your password?</DisplayName>
        <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="isForgotPassword" DefaultValue="true" AlwaysUseDefaultValue="true" />
        </OutputClaims>
      </TechnicalProfile>

      <!-- Update the sign-up or sign-in page with forgot password link target ClaimsExchange -->
      <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
        <DisplayName>Local Account Signin</DisplayName>
        <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
        <Metadata>
          <Item Key="setting.operatingMode">Email</Item>
          <Item Key="ContentDefinitionReferenceId">emailSignIn</Item>
          <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Please enter a valid email address.</Item>
          <Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>
        </Metadata>
        <IncludeInSso>false</IncludeInSso>
        <InputClaims>
          <InputClaim ClaimTypeReferenceId="email" />
        </InputClaims>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="email" Required="true" />
          <OutputClaim ClaimTypeReferenceId="password" Required="true" />
          <OutputClaim ClaimTypeReferenceId="objectId" />
        </OutputClaims>
        <ValidationTechnicalProfiles>
          <ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
        </ValidationTechnicalProfiles>
        <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
      </TechnicalProfile>

    </TechnicalProfiles>
  </ClaimsProvider>  
</ClaimsProviders>

In this ClaimsProvider block, we add two technical profiles.

One is <TechnicalProfile Id="ForgotPassword"> which sets the Claim isForgotPassword to true;
and the other one <TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email"> which has a very important line, which is <Item Key="setting.forgotPasswordLinkOverride">ForgotPasswordExchange</Item>. Important: since
my SelfAsserted-LocalAccountSignin-Email technical profile comes from the phonenumber-passwordless sample, this technical profile might be different than yours so be careful with the Metadata Items keys and values that you have. Embedded password example line reference

4. This is the trick part (and most likely the part you messed up since the docs does not explains this in detail for the mere mortals). ☠

Little explanation before giving out the code

The sample tells you that in the Orchestration step where you have the signin email exchange LocalAccountSigninEmailExchange, you have to add the property ContentDefinitionReferenceId="api.signuporsignin" and change the type to CombinedSignInAndSignUp, like so (ref):

<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
  <ClaimsProviderSelections>
    <ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange"/>
    <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
    <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
  </ClaimsProviderSelections>
  <ClaimsExchanges>
    <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
  </ClaimsExchanges>
</OrchestrationStep>

Recall earlier that we added a content definition with the Id api.signuporsignin, this is how we start the fire.

BUT, in the embedded password sample (watch link), they have the SignUpOrSignIn user journey, but in my case (which is the case for everyone else), the email signin page is not on this user journey. The user journey I had to modify was the SignInWithPhoneOrEmail which is the journey that has the email signin page (with the email and password input). Also, it's a subjourney. This is not explained very well on the doc because I think they assume you'll know where to make the changes but it wasn't clear to me at all (but thanks to @JasSuri, I got the idea that I was modifying the wrong journey).

Originally, my problem was that I was changing the wrong user journey (which was the SignUpOrSignInWithPhoneOrEmail). You need to look and find the correct journey that has the email signin logic. Most likely it will be a journey that has the following line of code TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" in one of the orchestration steps.

The other thing is that once you find the user journey that you need to modify, you need to walk the journey correctly, in other words, using the isForgotPassword claim to skip orchestration steps that are not relevant to the forgot password steps, which in our case, different orchestration steps that do different things inside them. If you don't do this correctly, you might have some nasty bugs and this is not explained very well, which again, I think they assume we will know how to do this and I dont blame them.

Now, back to the solution. As I mentioned earlier, you need to find the orchestration step that has the following line of code TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" inside of it, and add the content definition we added before and the correct type, like this:

<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
  <Preconditions>
    <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
      <Value>phoneNumber</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
    <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
      <Value>email</Value>
      <Action>SkipThisOrchestrationStep</Action>
    </Precondition>
  </Preconditions>
  <ClaimsProviderSelections>
    <ClaimsProviderSelection ValidationClaimsExchangeId="EmailInputExchange" />
    <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
  </ClaimsProviderSelections>
  <ClaimsExchanges>
    <ClaimsExchange Id="EmailInputExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
  </ClaimsExchanges>
</OrchestrationStep>

If you compare the orchestration step code that the embedded password reset example shows and the code I'm giving above, you'll see that they are different, yours will most likely be different as well. The key part to understand here is to add the ContentDefinitionReferenceId, changing the type to CombinedSignInAndSignup and <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" /> (not explained in docs...).

Now please pay attention that the subjourney I'm showing next might not be the same as yours (because again, as I explained earlier, the journey/subjourney that has your email signin can be another journey). in other words, just dont simply copy/paste the following block of code. I just want you to see how I correctly walk the journey (i.e, how I use the isForgotPassword claim to skip steps that have nothing to do with forgot password).

Journey:

<SubJourney Id="SignInWithPhoneOrEmail-WithForgotPassword" Type="Call">
  <OrchestrationSteps>

    <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>phoneNumber</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>email</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsProviderSelections>
        <ClaimsProviderSelection ValidationClaimsExchangeId="EmailInputExchange" />
        <ClaimsProviderSelection TargetClaimsExchangeId="ForgotPasswordExchange" />
      </ClaimsProviderSelections>
      <ClaimsExchanges>
        <ClaimsExchange Id="EmailInputExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="2" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>phoneNumber</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>objectId</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="ForgotPasswordExchange" TechnicalProfileReferenceId="ForgotPassword" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="3" Type="InvokeSubJourney">
     <Preconditions>
       <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
         <Value>phoneNumber</Value>
         <Action>SkipThisOrchestrationStep</Action>
       </Precondition>
       <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
         <Value>isForgotPassword</Value>
         <Action>SkipThisOrchestrationStep</Action>
       </Precondition>
     </Preconditions>
     <JourneyList>
       <Candidate SubJourneyReferenceId="PasswordReset" />
     </JourneyList>
    </OrchestrationStep>

    <OrchestrationStep Order="4" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>phoneNumber</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="PhoneVerificationExchangePart1" TechnicalProfileReferenceId="PhoneVerificationPage1" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="5" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>phoneNumber</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="PhoneVerificationExchangePart2" TechnicalProfileReferenceId="PhoneVerificationPage2" />
      </ClaimsExchanges>
    </OrchestrationStep>

    <OrchestrationStep Order="6" Type="ClaimsExchange">
      <Preconditions>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>isForgotPassword</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
          <Value>strongAuthenticationEmailAddress</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
        <Precondition Type="ClaimsExist" ExecuteActionsIf="false">
          <Value>phoneNumber</Value>
          <Action>SkipThisOrchestrationStep</Action>
        </Precondition>
      </Preconditions>
      <ClaimsExchanges>
        <ClaimsExchange Id="SignUpWithPhone_CollectEmailAddress" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonPhoneNumber_CollectEmailAddress" />
      </ClaimsExchanges>
    </OrchestrationStep>
  </OrchestrationSteps>
</SubJourney>

5. Add the Forgot Password subjourney:

  <SubJourney Id="PasswordReset" Type="Call">
    <OrchestrationSteps>

      <!-- 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>

      <!-- 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>

Embedded password example line reference

You might not get it on the first try but I give every clue possible that you need in order to solve this problem.

Also, if you get errors like Missing technical profile or Missing something, please refer to the embedded password reset to fill the missing things you might need to copy.

@abridger
Copy link

Thanks so much for such a detailed walk-through @LuisDev99. I've followed it through and applied everything to my own custom profile, in which I've split out the email entry and the password entry - we need to be able to check the email domain to see if it belongs to an SSO user and should use an external identity provider, or whether it's a local user and should be authenticated locally. While everything else seems to be working well, the forgotten password link on the password entry page stubbornly refuses to display, even with the embedded password reset as you've implemented above.

I've ensured that the Orchestration Step Type is CombinedSignInAndSignUp and the ContentDefinitionReferenceId is api.signuporsignin, as well as ensuring that the Claims Provider Selection is in place. Is there anything else that I could be missing for why this is still missing?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants