I'm using the guide here to perform Just-In-Time migration of a user from a legacy Idp to azure ad b2c: https://github.com/azure-ad-b2c/user-migration/tree/master/jit-migration-v2. I have this working properly on its own with a service I am using to query the legacy IdP and returning expected claims.
However, I would like to modify the above to first check if the user exists in AD before trying to migrate. I have tried declaring a ValidationTechnicalProfile, but it doesn't really seem to be working:
<TechnicalProfile Id="AAD-UserCheckUsingEmailAddress">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
Below, I use the above defined profile to check if the objectId exists in the claim before migration:
<!-- SIGN-IN -->
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Email">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="needToMigrate" />
</OutputClaims>
<ValidationTechnicalProfiles>
<!--First check if user exists in AD-->
<ValidationTechnicalProfile ReferenceId="AAD-UserCheckUsingEmailAddress" />
<!--Demo: Add user migration validation technical profile before login-NonInteractive.
Only execute migration if user does not exist in AD-->
<ValidationTechnicalProfile ReferenceId="REST-UserMigration-LocalAccount-SignIn" ContinueOnError="false" >
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
Change this
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
To
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.emailAddress" Required="true" />
And this
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
To
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
This assumes that the user enters their email into a textbox (or otherwise acquired) with claim name signInName, and the users identifier is stored in signInNames.emailAddress.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#inputclaims
PartnerClaimType username Is not valid. There is no attribute on the user called username.
This sample does something similar
https://github.com/azure-ad-b2c/user-migration/tree/master/seamless-account-migration
https://github.com/azure-ad-b2c/user-migration/blob/master/seamless-account-migration/policy/TrustFrameworkExtensionsSeamlessMigration.xml#L52
Related
I have a B2C sign-up only custom policy, with a custom email display control. Currently, the user is notified AFTER validating the email, that the email is already in use. (profile and password information are collected on a later step)
Here is some relevant code to accomplish this:
<TechnicalProfile Id="PartnerSignUpVerifyEmailPage">
<DisplayName>Local Email Verification</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.partners.signUpVerifyEmailPage</Item>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">A user with this email address already exists.</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GetLocalizedStringsForEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" DefaultValue="{OIDC:LoginHint}" AlwaysUseDefaultValue="true" />
</InputClaims>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="localizedSignUpEmailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<!-- this validation asserts the email provided isn't already in use -->
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists" />
</ValidationTechnicalProfiles>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<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="GenerateOtp"/>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp"/>
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" />
<!-- blank the object id, in the case that it was used, so we can let the user change the email and retest -->
<OutputClaimsTransformation ReferenceId="SetObjectIdToNull" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
My goal is: Upon pressing the 'Send Verification Code' button, validate that the email is not already in use. Then, if the email is in use: put up an error message, don't send an email and don't progress the control.
I have tried 3 different approaches and none seem to do the job.
Approach 1: Validate, Error and Stop
Details:
At the top of my SendCode ValidationClaimsExchanges add a ValidationClaimsExchangeTechnicalProfile for AAD-UserReadUsingEmailAddress-RaiseIfExists
On the other ValidationClaimsExchangeTechnicalProfiles add ContinueOnError="false"
Results:
Displays an error to the user when using an email already in use
The control still prompts for sending a code
However, the other validation steps still execute and an email is still sent.
In this case, it looks like failing the validation and sending an error message is not the same kind of error that the ContinueOnError looks for.
Approach 2: Evaluate New Claim, Use Precondition
Details:
Create a boolean claim emailAlreadyRegistered
At the top of my SendCode ValidationClaimsExchanges add a ValidationClaimsExchangeTechnicalProfile for AAD-UserReadUsingEmailAddress-RaiseIfExists
In AAD-UserReadUsingEmailAddress-RaiseIfExists add a CompareClaims claims transformation to set emailAlreadyRegistered = True if the emails match
Add preconditions to the other ValidationClaimsExchangeTechnicalProfiles using ClaimEquals to see if emailAlreadyRegistered is True
Results:
Displays an error to the user when using an email already in use
The control still prompts for sending a code
However, the other validation steps still execute and an email is still sent.
I think in this case the precondition is not seeing the update to the claim (if I set the claim from my PartnerSignUpVerifyEmailPage technical profile then the preconditions seem to respect it).
Approach 3: Nested Validation Steps
Details:
Create a boolean claim emailAlreadyRegistered
Create a new claims transformation technical profile
The new profile takes the email as input and uses a claims transformation to set emailAlreadyRegistered in the input claim transformation
The new technical profile ALSO has 3 validationtechincalprofiles: AAD-UserReadUsingEmailAddress-RaiseIfExists, GenerateOtp, SendLocalizedOtp
In SendCode ValidationClaimsExchanges replace all ValidationClaimsExchangeTechnicalProfiles for the one new technical profile
Results:
Does not display the error when the email is in use
The control proceeds to the code verification step
No email is sent
In this case it looks like none of the nested validation technical profiles work at all.
Solution: (Credit: Christian Le Breton)
Create a boolean claim emailAlreadyRegistered
Split AAD-UserReadUsingEmailAddress-RaiseIfExists into two separate Technical Profiles
The first is the same as described without the output transformations
The second is a proper ClaimsTransformationProtocolProvider technical profile with the two OutputClaimsTransformations described above, plus a third in between them, This transformation sets emailAlreadyRegistered using a CompareClaims transform. This technical profile also outputs the claim emailAlreadyRegistered with DefaultValue = "false"
At the top of my SendCode ValidationClaimsExchanges add the two new Technical Profiles
Add preconditions to the other ValidationClaimsExchangeTechnicalProfiles using ClaimEquals to see if emailAlreadyRegistered is 'true'
Basically, what I failed to understand was that an AAD Technical Profile can't set new claims into the bag (I suppose) and a Claims Technical Profile can't read AAD, so these two activities needed to be separated, once I did that the 2nd approach worked.
Here are some of the tricky bits from the final result.
<DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<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-RaiseIfExists-Pt1" ContinueOnError="false" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2" ContinueOnError="false" />
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" ContinueOnError="false" >
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>emailAlreadyRegistered</Value>
<Value>true</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationClaimsExchangeTechnicalProfile>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp" ContinueOnError="false">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>emailAlreadyRegistered</Value>
<Value>true</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationClaimsExchangeTechnicalProfile>
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt1">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" />
<OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2">
<DisplayName>Check If Email Is Registered</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="objectIdNotFound" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailAlreadyRegistered" />
<OutputClaim ClaimTypeReferenceId="objectId" />
</OutputClaims>
<OutputClaimsTransformations>
<!-- ensure that the object id isn't already used -->
<OutputClaimsTransformation ReferenceId="SetEmailAlreadyRegistered" />
<!-- blank the object id, in the case that it was used, so we can let the user change the emamil and retest -->
<OutputClaimsTransformation ReferenceId="SetObjectIdToNull" />
</OutputClaimsTransformations>
</TechnicalProfile>
I think approach 1 is likely your best approach, with some slight modifications. The main issue is that claims transformations need to be called from a ClaimsTransformations technical profile that is a validation technical profile, not by calling the ClaimsTransformations profile from within a validation technical profile.
Instead of trying to do everything in one step, it should be broken up into a few steps.
Have your user read technical profile just output the objectId and objectIdNotFound, without the claims transformation, and have your send code step use the read technical profile, the assert technical profile and the set to null technical profile as verification profiles in that order, prior to generating or sending the OTP.
I'm trying to use custom technical profile for Local Account in SignUpOrSignIn user journey.
I have Created the following technical profile in my customtrustframeworkextensions.xml (base:trustframeworkextensions.xml):
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="CustomLocalAccountSignUpWithLogonEmail">
<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>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</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" />
<OutputClaim ClaimTypeReferenceId="extension_XXX" />
<!-- Optional claims, to be collected from the user -->
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-ValidateProfile" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
REST-ValidateProfile looks like the following:
<ClaimsProvider>
<DisplayName>REST APIs</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-ValidateProfile">
<DisplayName>Check yyy and zzz Rest API</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Set the ServiceUrl with your own REST API endpoint -->
<Item Key="ServiceUrl">https://asd</Item>
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production environments -->
<Item Key="AuthenticationType">ApiKeyHeader</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="Api-key" StorageReferenceId="B2C_1A_key" />
</CryptographicKeys>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="extension_xxx" PartnerClaimType="xxx" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="extension_yyy" PartnerClaimType="yyy" />
<OutputClaim ClaimTypeReferenceId="extension_zzz" PartnerClaimType="zzz" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</ClaimsProvider>
I have modified the OrchestrationStep to use custom technical profile in user journey:
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="CustomLocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
When I run my Custom policy and select SignUp the browser shows error: "The page cannot be displayed because an internal server error has occurred."
There are some more spesific details in Application insights:
Exception Message:Output claim type "objectId" specified in the technical profile with id "CustomLocalAccountSignUpWithLogonEmail" in policy "B2C_1A_DEV_signup_signin" of tenant does not specify a UserInputType or a DefaultValue, and is not retrieved from a ValidationTechnicalProfile either., Exception Type:InvalidDefinitionException
Everything works ok when I use name "LocalAccountSignUpWithLogonEmail" for edited technical profile and the ClaimsExChange Looks like this:
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="CustomLocalAccountSignUpWithLogonEmail" />
But when I change name of the modified technical profile, policy doesn't work anymore. It seems to me that claims exchange doesn't work or something. I don't get why because I can't find any other places that refers to LocalAccountSignUpWithLogonEmail.
I want to use custom technical profile because want want to remove some outputclaims whitout touching the base policies.
The technicalProfile "CustomLocalAccountSignUpWithLogonEmail" has an output claim of the objectID which is common when you write something. The most common patterns is:
Technical profile
-> Validation Technical 1 profile
-> Validation Technical 2 profile
Being, validation technical profile 1 maybe calls your REST to validate the profile, then you call validation technical profile 2 to perform a write operation into the directory. When you write into the directory, it outputs an objectID which will output it into your technical profile and that error will go away.
I have a business requirement but I've failed to figure out how to implement it.
The very first time a user signs-in, they are challenged with email OTP followed by a forced password reset.
All subsequence sign-in attempts will prompt the user only for email address and password (OTP is a one-time user flow).
My problem is that Orchestration step 1 collects the email address, then I query AAD by the email address provided and read an extension attribute called extension_EmailValidated. If the attribute is not TRUE, B2C will force the user to do email OTP verification, however, Orchestration step 2 won't let me pre-populate the previously entered email address input box AND show the OTP buttons. It will only let me do one or the other (hope that makes sense).
My UserJourney looks like this
<UserJourneys>
<UserJourney Id="B2CSignIn">
<OrchestrationSteps>
<!--User enters their email address-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADEmailDiscovery" TechnicalProfileReferenceId="AAD-EmailDiscovery" />
</ClaimsExchanges>
</OrchestrationStep>
<!--User enters their OTP-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_EmailVerified</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADEmailVerification" TechnicalProfileReferenceId="AAD-EmailVerification" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Set extension_EmailVerified to TRUE-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>extension_EmailVerified</Value>
<Value>True</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWriteEmailVerifiedUsingEmail" TechnicalProfileReferenceId="AAD-UserWriteEmailVerifiedUsingEmail" />
</ClaimsExchanges>
</OrchestrationStep>
Here are the technical profiles for Step 1 and Step 2.
<TechnicalProfile Id="AAD-EmailDiscovery">
<DisplayName>Initiate Email Address Verification For Local Account</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.localaccountsignup</Item>
<Item Key="language.button_continue">Continue</Item>
<Item Key="setting.showCancelButton">False</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="extension_EmailVerified" DefaultValue="false" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<TechnicalProfile Id="AAD-EmailVerification">
<DisplayName>Initiate Email Address Verification For Local Account</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.localaccountsignup</Item>
<Item Key="language.button_continue">Continue</Item>
<Item Key="setting.showCancelButton">False</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
Another very confusing issue is that the only way I can get the OTP buttons to appear on Step 2 is to include "ParterClaimType=Verified.Email" for both the InputClaim and OutputClaims -- and that makes no sense.
If I omit "ParterClaimType=Verified.Email" from InputClaims and OutputClaims, I can get the email address to pre-populate from Step 1.
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
</OutputClaims>
Advice and guidance is very much appreciated.
Thanks!
The solution here is to make the input text box readOnly.
Send the email collected at the sign in screen through a claim transform to copy it to a new claim, called readOnlyEmail. The claim should be defined as read only.
<ClaimType Id="readOnlyEmail">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<UserHelpText/>
<UserInputType>Readonly</UserInputType>
</ClaimType>
Copy the email claim into the readOnly claim
<ClaimsTransformation Id="CopySignInNameToReadOnly" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
Then make a call to the claims transform from your sign up technical profile using an OutputClaimsTransformations node.
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopySignInNameToReadOnly" />
</OutputClaimsTransformations>
Finally on the email verification technical profile (selfAsserted technical profile), pass in the readOnly email to be validated:
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlyEmail"/>
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" PartnerClaimType="Verified.Email"/>
</OutputClaims>
The concept is demonstrated here:
https://github.com/azure-ad-b2c/samples/tree/master/policies/signin-email-verification
You just need to add your conditional logic on top of that sample.
I'm building an Azure AD B2C configuration based on custom policies. Sign in, profile edit, password change, etc. are already working as wanted.
But currently I'm struggling with the password forgot policy. I want to achieve that the new password does not equal to old one. Google and the Microsoft docs always give me examples for password changes. When I change the password, I have to enter the old one and the new one. Then I'm able to compare the old and the new one. For example like the way discribed here
But when a user has forgotten his password, then he is - of course - not able to enter the old password to compare it with the new one.
Is there any way to build a real password forgot policy without entering the old password but nevertheless ensure that the new password does not equal the old password?
Thanks in advance!
Alex
I was having the same problem and the answer from Jas Suri helped me a lot. However, I had some problems getting it to work. So, that's why I am sharing my final solution in case someone else is facing the same problem.
I have used the following as a base: https://github.com/azure-ad-b2c/samples/tree/master/policies/password-reset-not-last-password/policy but made some adjustments. Here is what I had to add to my existing policy to make it work:
ClaimSchema:
These claims will be used later.
<ClaimsSchema>
<ClaimType Id="SamePassword">
<DisplayName>samePassword</DisplayName>
<DataType>boolean</DataType>
<UserHelpText />
</ClaimType>
<ClaimType Id="resetPasswordObjectId">
<DisplayName>User's Object ID</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="oid" />
<Protocol Name="OpenIdConnect" PartnerClaimType="oid" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.microsoft.com/identity/claims/objectidentifier" />
</DefaultPartnerClaimTypes>
<UserHelpText>Object identifier (ID) of the user object in Azure AD.</UserHelpText>
</ClaimType>
</ClaimsSchema>
ClaimsTransformation:
The first claim transformation checks whether resetPasswordObjectId is set, if not the previous attempted login with the new password apparently didn't work and thus the newPassword is not the same as the old / current password. The second claim transformation checks whether the claim SamePassword is equal to false. If not it throws an error and 'You can't use an old password' is displayed. More information on this later.
<ClaimsTransformations>
<ClaimsTransformation Id="CheckPasswordEquivalence" TransformationMethod="DoesClaimExist">
<InputClaims>
<InputClaim ClaimTypeReferenceId="resetPasswordObjectId" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="SamePassword" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="AssertSamePasswordIsFalse" TransformationMethod="AssertBooleanClaimIsEqualToValue">
<InputClaims>
<InputClaim ClaimTypeReferenceId="SamePassword" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="valueToCompareTo" DataType="boolean" Value="false" />
</InputParameters>
</ClaimsTransformation>
</ClaimsTransformations>
ClaimsProvider:
The overall idea is to use the newly entered password and attempt to login the user. In case the login is successful, the newPassword is the same as the old / current password and is not allowed. When logging in, we create an output claim and call it resetPasswordObjectId which is either unset or equal to the object id of the logged in user. We then check whether the resetPasswordObjectId exists (done in the claims transformation part), if not the newPassword can be used as it is not the same as the old / current password.
To display the correct error message in case the user entered the old password, we need to override UserMessageIfClaimsTransformationBooleanValueIsNotEqual in the TrustFrameworkLocalization.xml in the <LocalizedResources Id="api.localaccountpasswordreset.en"> like this <LocalizedString ElementType="ErrorMessage" StringId="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">You must not use your old password.</LocalizedString>.
When using the claim provider below, ensure to replace all parts marked with a TODO.
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>Password Reset without same password</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive-PasswordChange">
<DisplayName>Local Account SignIn</DisplayName>
<Protocol Name="OpenIdConnect" />
<Metadata>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">We can't seem to find your account</Item>
<Item Key="UserMessageIfInvalidPassword">Your password is incorrect</Item>
<Item Key="UserMessageIfOldPasswordUsed">Looks like you used an old password</Item>
<Item Key="ProviderName">https://sts.windows.net/</Item>
<!-- TODO replace YOUR-TENANT-ID -->
<Item Key="METADATA">https://login.microsoftonline.com/YOUR-TENANT.onmicrosoft.com/.well-known/openid-configuration</Item>
<!-- TODO replace YOUR-TENANT-ID -->
<Item Key="authorization_endpoint">https://login.microsoftonline.com/YOUR-TENANT.onmicrosoft.com/oauth2/token</Item>
<Item Key="response_types">id_token</Item>
<Item Key="response_mode">query</Item>
<Item Key="scope">email openid</Item>
<!-- TODO ensure this line is commented out-->
<!-- <Item Key="grant_type">password</Item> -->
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="HttpBinding">POST</Item>
<!-- TODO -->
<!-- ProxyIdentityExperienceFramework application / client id -->
<Item Key="client_id">YOUR-PROXY-CLIENT-ID</Item>
<!-- Native App -->
<!-- TODO -->
<!-- IdentityExperienceFramework application / client id -->
<Item Key="IdTokenAudience">YOUR-IDENTITY-CLIENT-ID</Item>
<!-- Web Api -->
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="username" Required="true" />
<!-- INFO: replaced oldPassword with newPassword, that way we try logging in with the new password. If the login is successful, we know the newPassword is the same as the old / current password-->
<InputClaim ClaimTypeReferenceId="newPassword" PartnerClaimType="password" Required="true" />
<InputClaim ClaimTypeReferenceId="grant_type" DefaultValue="password" />
<InputClaim ClaimTypeReferenceId="scope" DefaultValue="openid" />
<InputClaim ClaimTypeReferenceId="nca" PartnerClaimType="nca" DefaultValue="1" />
<!-- TODO -->
<!-- ProxyIdentityExperienceFramework application / client id -->
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="YOUR-PROXY-CLIENT-ID" />
<!-- TODO -->
<!-- IdentityExperienceFramework application / client id -->
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="YOUR-IDENTITY-CLIENT-ID" />
</InputClaims>
<OutputClaims>
<!-- INFO: assign the objectId (oid) to resetPasswordObjectId, since the claim objectId might already be set. In that case there would be no way of knowing whether it was set due to the attempted login with the newPassword-->
<OutputClaim ClaimTypeReferenceId="resetPasswordObjectId" PartnerClaimType="oid" />
<OutputClaim ClaimTypeReferenceId="tenantId" PartnerClaimType="tid" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
<OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" PartnerClaimType="upn" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
</OutputClaims>
</TechnicalProfile>
<!--Logic to check new password is not the same as old password
Validates old password before writing new password-->
<TechnicalProfile Id="LocalAccountWritePasswordUsingObjectId">
<DisplayName>Reset password</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.localaccountpasswordreset</Item>
<!-- set in the TrustFrameworkLocalization.xml -->
<!-- <Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">You must not use your old password.</Item> -->
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="login-NonInteractive-PasswordChange" ContinueOnError="true" />
<ValidationTechnicalProfile ReferenceId="ComparePasswords" />
<ValidationTechnicalProfile ReferenceId="AAD-UserWritePasswordUsingObjectId">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>SamePassword</Value>
<Value>True</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
</ValidationTechnicalProfiles>
</TechnicalProfile>
<!-- Runs claimsTransformations to make sure new and old passwords differ -->
<TechnicalProfile Id="ComparePasswords">
<DisplayName>Compare Email And Verify Email</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="SamePassword" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CheckPasswordEquivalence" />
<OutputClaimsTransformation ReferenceId="AssertSamePasswordIsFalse" />
</OutputClaimsTransformations>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
You can do it with some logic with Validation Technical profiles:
Call login-noninteractive with continueOnError=true
Call a claimTransform to generate a boolean if a claim (like objectId) is null
Use the boolean for the proceeding logic, lets call it pwdIsLastPwd
Call a claimTransform to assert pwdIsLastPwd = false
If it is true, throw an error - "you cannot use this password" using the claimTransform error handler
Continue with the rest of reset password flow
References:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile#validationtechnicalprofiles
Call Claim transform from VTP, Boolean ClaimTransform check if claim exists
Assert boolean is true/false
"The UserMessageIfClaimsTransformationBooleanValueIsNotEqual self-asserted technical profile metadata controls the error message that the technical profile presents to the user. "
I'm currently testing out a theory with Azure AD B2C that requires an update of a value on a user profile with a static value that is not input by a user. I have attempted to try and follow the path of a profile edit example but can't seem to make it work. At this point it does not matter what value I update I just want to be able to update a value, I have chosen the "state" attribute for this experiment.
I have the following orchestration step in a user journey:
<OrchestrationStep Order="5" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="UpdateStateValue" TechnicalProfileReferenceId="LocalAccountUpdateStateValue" /
</ClaimsExchanges>
</OrchestrationStep>
The technical profiles are as follows:
<TechnicalProfile Id="LocalAccountUpdateStateValue">
<DisplayName>Update Password Set Value</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</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" />
</InputClaims>
<OutputClaims>
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserUpdateStateValue" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserUpdateStateValue">
<Metadata>
<Item Key="Operation">Write</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">false</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" Required="true" />
</InputClaims>
<PersistedClaims>
<!-- Required claims -->
<PersistedClaim ClaimTypeReferenceId="objectId" />
<!-- Optional claims -->
<PersistedClaim ClaimTypeReferenceId="state" DefaultValue="CA"/>
</PersistedClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
What I am basically attempting to do is each time through the user journey update the "state" value to "CA" but it doesn't seem to be working. I first tried to just reference the "AAD-UserUpdateStateValue" technical profile directly without success and then moved towards something more similar to the profile edit example.
We are admittedly new to Azure B2C so any help or reference to articles that could help is appreciated.