azure ad b2c Prevent Password reuse - azure-ad-b2c

I am trying to send the password as part of output claims but it never comes up. I tried to create a new extension variable and do a tranformation copy claim but that didnt work either.
I would like to hash the password and store it in external system to work around the issue of not repeating password as thats a security requirement.
If I try to add
<OutputClaim ClaimTypeReferenceId="password" />
as part of
<TechnicalProfile Id="login-NonInteractive">
and the whole login screen stops working.
Please let me know if I am missing something and also on how to hash the password to send it to an external RestAPI Technical profile
REST API that I used
<ClaimsProvider>
<DisplayName>REST API</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-API-ValidatePassword">
<DisplayName>Encrypt the password and validate it against previous passwords</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://test********</Item>
<!--<Item Key="ServiceUrl">https://your-azfunc-api.azurewebsites.net/api/EncryptClaims</Item>-->
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="password" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="passEncrypted" PartnerClaimType="passEncrypted"/>
<OutputClaim ClaimTypeReferenceId="isPasswordUsed" PartnerClaimType="isPasswordUsed"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>

As per this:
"AAD and AAD B2C do not maintain a password history. Currently therefore you can only prevent the last password from being used.
We don’t recommend using external systems to maintain a password history."
Have a look at this sample.
You could perhaps copy the password to an extension attribute and output that but then you are exposing the password.
A better way is to use a REST API that saves the password so everything is done server-side.

You can try doing this by copying password claim when it is available. You just need to remember that copying password to another claim makes it vulnerable to logging etc. You need to do it with proper care.
To have a copied password value you need another claim:
<ClaimType Id="pwd">
<DisplayName>Password</DisplayName>
<DataType>string</DataType>
<AdminHelpText>password</AdminHelpText>
<UserHelpText>password</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
then, a claim transformation:
<ClaimsTransformation Id="CopyPass" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="password" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="pwd" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
and that claim transformation added to a proper technical profile:
<TechnicalProfile Id="eSTS-NonInteractive">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyPass" />
</InputClaimsTransformations>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="pwd" />
</OutputClaims>
</TechnicalProfile>
You should be able to use pwd claim value (containing actual password) for whatever you need then.

Related

Azure B2C Custom Reset Password Policy

I have a some custom policies in B2C that are working and I'm not trying to get the Reset Password to work. One of the issues I have is that I call a Restful API to check if the email address provided is a local user or if we are signing them in from a Microsoft AAD. This works fine, so the user is signed in via a Microsoft Organisation Identity if they are an SSO user, if not they are signed in locally via the B2C.
My issue is that I'm trying to do something similar with reset password. I use the following Technical Profile to get the email address
<TechnicalProfile Id="SelfAsserted-Signin-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="DisplayName">Signin To Tax Systems</Item>
<Item Key="setting.operatingMode">Email</Item>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
Once the email address is obtained, we call a rest api to check the email address. This check tells us if the user is a federated user, or a local user. If they are a federated user I want to error as they can't reset their password via B2C.
If they are a local user then we want to do the reset password. This is done via the following Technical Profile. The problem is that they have to enter their email address again, I want the email address to be pre-populated with the email address obtained previously.
<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>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<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>
Any idea how I can do this? I can't seem to find much detail about api.localaccountpasswordreset in regards to the metadata etc.
Since you obtained the email in the claim signInName at step 1, in step 2, you can pre-populate it as follows:
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email"/>
</InputClaims>
For the Email Verification buttons to appear, you must have the email claim as readOnly. Otherwise, AAD B2C will pre-populate the field assuming its already validated, and only show the Verify button if the email address is changed.
So a true solution would be:
<ClaimsSchema>
<!-- Sample: Read only email address to present to the user-->
<ClaimType Id="readonlyEmail">
<DisplayName>E-mail Address</DisplayName>
<DataType>string</DataType>
<UserInputType>Readonly</UserInputType>
</ClaimType>
</ClaimsSchema>
<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>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="readonlyEmail"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readonlyEmail" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
You'd need to modify AAD-UserReadUsingEmailAddress such that its input claim is
<InputClaims>
<InputClaim ClaimTypeReferenceId="readonlyEmail" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
I would copy the whole technical profile and give it a new Id with above modification.
And any further references to email in the joruney should be replaced with readOnlyEmail in the same way.
You need to add these:
<Metadata>
<Item Key="IncludeClaimResolvingInClaimsHandling">True</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" DefaultValue="{OAUTH-KV:email}" AlwaysUseDefaultValue="true"></InputClaim>
</InputClaims>
So you can set the email calling the policy authorize URL appending an email query param (&email=someemail#somedomain.com)
Did you get this working? I had a similar setup for one of my customers and fixed this issue by adding 2 validation profiles (https://learn.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile) and changed the input claim:
Call the REST API as a Validation Technical Profile (https://learn.microsoft.com/en-us/azure/active-directory-b2c/restful-technical-profile) in your SelfAsserted-Signin-Email Technical Profile to check the Email address, display an error on the screen if the email address is not federated;
Call a ClaimsTransformation Validation Technical Profile (https://learn.microsoft.com/en-us/azure/active-directory-b2c/claims-transformation-technical-profile) that references a ClaimsTransformation to copy signInName to email (https://learn.microsoft.com/en-us/azure/active-directory-b2c/general-transformations#copyclaim);
Change the Input Claim in your LocalAccountDiscoveryUsingEmailAddress Technical Profile to email.
This provides a clean UI - an error is displayed on the first form if the user is federated and the second screen pre-populates the email field using the default claim.

Any way to provide default values for inputs in an Azure AD B2C custom policy?

I am working on integrating Azure AD B2C into an existing shopping cart application, replacing an existing user identity solution.
I have already created a custom policy to implement user registration / sign-up and integrated it into the normal account creation process. However, I am having a problem with integrating registration during the checkout process.
With the old IDP, the checkout process first collects the user's name and email address. After that has been collected, the user is given the option to create an account with that information (if they are not already logged in). In order to avoid confusing double-entry of the email address, I would like to pass the email address that the user already entered to the B2C sign-up policy and have it fill in the email address input on the form.
Is there any way to do this? I don't find anything like this being addressed in the B2C documentation.
You can use a sign up policy with a claims resolver. Send the email in the query parameter. It’ll prepopulate the email into the text box.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/claim-resolver-overview
Based on the documentation linked by #JasSuri, I was able to come up with a solution.
To implement this, you modify the sign-up technical profile. You must add three things:
Add item IncludeClaimResolvingInClaimsHandling with value true to the metadata
Add a DefaultValue attribute to the email input claim with an appropriate claims resolver notation as the value
Add an AlwaysUseDefaultValue attribute to the email input claim with true as the value
I used an Oauth2 key-value claims resolver (which supports arbitrary query string parameters) and a query parameter named register_email.
The resulting technical profile looks like this:
<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>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
<Item Key="language.button_continue">Create</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item> <!-- ADD THIS -->
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<!-- ADD DefaultValue AND AlwaysUseDefaultValue ATTRIBUTES BELOW -->
<InputClaim ClaimTypeReferenceId="email"
DefaultValue="{OAUTH-KV:register_email}"
AlwaysUseDefaultValue="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="newUser" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>

Rest API call in custom policy in Azure AD B2C

I am new and have tried to build custom policy for sign-in by invoking a REST API call to legacy.
The API expects below parameters as input:
client_id=fixed value, client_secret=fixed value, grant_type=fixed value, scope=fixed value, username=variable, password=variable
The above parameters are not for Azure valued but some fixed values that need to be sent in the request.
I have added few portions in the extensions custom policy but seems the request body is not forming correctly and I am getting the error The claims exchange User Migration Via Legacy specified in step '1' returned HTTP error response with Code Bad Request and Reason 'Bad Request'.
What am I doing wrong here? Please help.
Added my portion :
<BuildingBlocks>
<ClaimsSchema>
<ClaimType Id="RequestBody">
<DisplayName>Request body</DisplayName>
<DataType>string</DataType>
<UserHelpText>RequestBody</UserHelpText>
</ClaimType>
</ClaimsSchema>
<ClaimsTransformations>
<ClaimsTransformation Id="GenerateRequestBody" TransformationMethod="GenerateJson">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="username" />
<InputClaim ClaimTypeReferenceId="password" TransformationClaimType="password" />
</InputClaims>
<InputParameters>
<InputParameter Id="client_id" DataType="string" Value="client" />
<InputParameter Id="client_secret" DataType="string" Value="sec" />
<InputParameter Id="grant_type" DataType="string" Value="grant" />
<InputParameter Id="scope" DataType="string" Value="scope" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="RequestBody" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
</ClaimsTransformations>
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>REST API to communicate with Legacy IdP</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="UserMigrationViaLegacyIdp">
<DisplayName>REST API call to communicate with Legacy IdP</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://</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
<Item Key="ClaimUsedForRequestPayload">RequestBody</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GenerateRequestBody" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="RequestBody" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="tokenSuccess" DefaultValue="false" />
<OutputClaim ClaimTypeReferenceId="migrationRequired" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
Can you share the actual JSON created by the claim transformation? Is it well formatted? You can make troubleshooting easy by displaying the output of <InputClaimsTransformation ReferenceId="GenerateRequestBody" /> before you actually use it as input claim inside the REST API technical profile. This will reveal what's going on with your JSON format.
To do that, first create a self asserted technical profile and add <InputClaimsTransformation ReferenceId="GenerateRequestBody" /> as an output claim transformation to it. Your output claim should be the claim which you will use later in next step in the journey.
In the user journey, add a step that calls the self asserted technical profile. Add next step with App Insights to write the output claim to App Insights( https://learn.microsoft.com/en-us/azure/active-directory-b2c/analytics-with-application-insights)
This will document claim value that REST API is receiving.

In Azure B2C Custom Policies, how do I display more than 2 distinct validation messages for validations that depend on Validation Technical Profiles?

Scenario
In my sign up flow, I have the requirement to show:
an email address field and a confirm email address field
a password field and a confirm password field
I have to validate that:
the text entered in the email address field matches the text entered in the confirm email address field
the text entered in the password field matches the text entered in the confirm password field
the email address entered for a user doesn't already exist in Azure B2C
These validations should display in the existing form, rather than a new page.
The only way I've been able to display validation messages inline as a result of the output from Validation Technical Profiles, is by using the two built in asserts, AssertStringClaimsAreEqual (for the email and password comparisons) and AssertBooleanClaimIsEqualToValue (for the existing user check). These cause messages, defined in the calling Claims Provider Technical Profile, to be displayed when the assert fails.
Problem
I have three error cases that require distinct messages, and I'm only able to define and display messages for two. Currently, I'm working around this by displaying "Email and confirm email, or password and confirm password do not match." for both the failed text comparisons, and "This email address is already in use, please sign in." for the existing user case. The existing user case is fine, but I would describe the single text comparison message as 'Okay, but doesn't really meet the acceptance criteria' and 'I can't believe there's not a better way to do this'.
Any help would be gratefully received at this point.
Current Code Snippets
The relevant Validation Technical Profiles:
<TechnicalProfile Id="EmailMatchValidator">
<DisplayName>Check if email and confirm email match</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="DoEmailsMatch"/>
</InputClaimsTransformations>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailsMatch" Required="true"/>
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="PasswordMatchValidator">
<DisplayName>Check if password and confirm password match</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="DoPasswordsMatch"/>
</InputClaimsTransformations>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="passwordsMatch" Required="true"/>
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="UserExistsValidator">
<DisplayName>Asset if user exists</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
<InputClaims>
<InputClaim ClaimTypeReferenceId="userExists"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="userExists" Required="true"/>
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="DoesUserExist"/>
</OutputClaimsTransformations>
</TechnicalProfile>
These Technical Profiles reference Output Claims Transformations, which look like this:
<ClaimsTransformation Id="DoEmailsMatch" TransformationMethod="AssertStringClaimsAreEqual">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim1"/>
<InputClaim ClaimTypeReferenceId="confirmEmail" TransformationClaimType="inputClaim2"/>
</InputClaims>
<InputParameters>
<InputParameter Id="stringComparison" DataType="string" Value="ordinalIgnoreCase"/>
</InputParameters>
</ClaimsTransformation>
<ClaimsTransformation Id="DoPasswordsMatch" TransformationMethod="AssertStringClaimsAreEqual">
<InputClaims>
<InputClaim ClaimTypeReferenceId="password" TransformationClaimType="inputClaim1"/>
<InputClaim ClaimTypeReferenceId="confirmpassword" TransformationClaimType="inputClaim2"/>
</InputClaims>
<InputParameters>
<InputParameter Id="stringComparison" DataType="string" Value="ordinalIgnoreCase"/>
</InputParameters>
</ClaimsTransformation>
<ClaimsTransformation Id="DoesUserExist" TransformationMethod="AssertBooleanClaimIsEqualToValue">
<InputClaims>
<InputClaim ClaimTypeReferenceId="userExists" TransformationClaimType="inputClaim"/>
</InputClaims>
<InputParameters>
<InputParameter Id="valueToCompareTo" DataType="boolean" Value="false"/>
</InputParameters>
</ClaimsTransformation>
DoEmailsMatch and DoPasswordsMatch are straightforward enough, using the AssertStringClaimsAreEqual built-in assert to display a message defined in the Metadata of the calling Claim Provider Technical Profile:
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">Email and confirm email, or password and confirm password do not match.</Item>
DoesUserExist relies on AssertBooleanClaimIsEqualToValue, which means it needs a boolean to work with, to decide whether to display the other message defined in the Metadata:
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">This email address is already in use, please sign in.</Item>
The code that sets that boolean is probably out of scope for this question, but in short, it gets set by a read on the AAD database that returns a matching User Principal Name in a claim, or no claim at all. This undergoes a Claim Transformation using DoesClaimExist to set the boolean userExists claim, used by DoesUserExist above.
For the third one, do an AAD Read operation instead, and it has a built in error message for when the use exists.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#ui-elements
You can find this used in the AAD B2C starter pack for the default sign up flow.
For password matching, use password and reenter password, use the built in way
https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack/blob/master/LocalAccounts/TrustFrameworkBase.xml#L648
Now you should only need the 1 claims transform for email matching.

Can you pass password claims between steps in Azure AD B2C Custom Policies?

Can you pass password claims between steps in Azure AD B2C Custom Policies?
My symptom is that after signing up using a multiple page custom policy, a user cannot sign in until they reset their password.
I am asking this as I spent many hours debugging a problem that it turns out could not be fixed. I found the answer under another question (Azure AD B2C Multi steps custom policy) that was differently worded but had similar symptoms.
I am posting here in the hope it is more easily found and helpful to others. Apologies if you think this is a duplicate.
In short, no. See Azure AD B2C Multi steps custom policy.
A password claim is "scoped" to a given step. This means the orchestration step that collects the password claim from the end user must be the same step that writes it to the User object.
I was looking for the same and have found a way to do multi step sign up. I tried several methods, including a outputclaimstransformation to store the password in another claim, but that did not work out. Because I already needed to do some input validation against an API I found a way to also copy the password to a new claim (plaintextPassword) that is not scoped to one orchestration step. This claim can be used in a later step to create the user account with the password provided by the plaintextPassword claim.
Create a self asserted technical profile that has an inputclaim (the password) and a validation technical profile. In the validation technical profile you can copy the password to a claim of type string with an inputclaimstransformation. Then add the new claim as outputclaim to the validation profile and to the technical profile. See code below for an example:
<ClaimType Id="plaintextPassword">
<DisplayName>password</DisplayName>
<DataType>string</DataType>
<UserInputType>TextBox</UserInputType>
</ClaimType>
<ClaimType Id="password">
<DisplayName>Your password</DisplayName>
<DataType>string</DataType>
<UserHelpText>Your password</UserHelpText>
<UserInputType>Password</UserInputType>
</ClaimType>
<ClaimsTransformation Id="CopyPassword" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="password" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="plaintextPassword" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<TechnicalProfile Id="SignUp-PasswordValidation">
<DisplayName>Email signup</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">url</Item>
<Item Key="AuthenticationType">ClientCertificate</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
<CryptographicKeys>
<Key Id="ClientCertificate" StorageReferenceId="B2C_1A_ClientCertificate" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyPassword" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="claim_to_validate" PartnerClaimType="claim_to_validate" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="plaintextPassword" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<TechnicalProfile Id="SignUp">
<DisplayName>Email signup</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.localaccountsignup</Item>
<Item Key="setting.retryLimit">3</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="claim_to_validate" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="plaintextPassword" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="SignUp-Validation" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
In the technical profile where you create the user in AAD add this line:
<PersistedClaim ClaimTypeReferenceId="plaintextPassword" PartnerClaimType="password"/>
I was able to solve the problem. When we have to add the user information (LocalAccountSignUpWithLogonEmail) I removed the validation that called the technical profile that is written in AAD, I changed it to a validation that calls another technical profile. This will copy our password through a ClaimsTransformation that will save our password in another InputClaim to be visible in any other step of our flow.
<ClaimType Id="plaintextPassword">
<DisplayName>password</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="passwordTransformation">
<DisplayName>requestSocialRestSecondCall</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="TransformationPassword" />
</ValidationTechnicalProfiles>
<ClaimsProvider> <!-- Copy Password-->
<DisplayName>Copy Password</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="TransformationPassword">
<DisplayName>Copy Pass</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<InputClaims>
<InputClaim ClaimTypeReferenceId="passwordTransformation" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="passwordTransformation" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="Generate_TranformationPassword" />
</OutputClaimsTransformations>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsTransformation Id="Generate_TranformationPassword" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="newPassword" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="plaintextPassword" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
I removed the validation that called the technical profile that is written in AAD and changed it to a validation that calls another technical profile.
This will copy the password through a ClaimsTransformation that will save our password in another InputClaim to be visible in any other step of our flow.
<TechnicalProfile Id="SignUp-PasswordValidation">
<Metadata>
<Item Key="ServiceUrl">url</Item>
<Item Key="AuthenticationType">ClientCertificate</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
What is the url you used?
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
What was you application to create the Secret?

Resources