Azure AD B2C ClaimsTransformation to non null input claim - azure-ad-b2c

In Azure AD B2C I am having multiple technical profiles.
In one technical profile, I may or may not have value for an output claim. But I can get it from another claim. Here for example let's take email.
So, it is possible that the user may not have an email claim, which is needed in the output. I can get it from the signInName as well.
What I want is to achieve is
if email claim has value, pass email claim in output
else use the claim signInName with PartnerClaimType="email"
How can I achieve this?

Use in AAD-OIDC:
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="email"/>
If email has a value at this stage, it will be output into the claimbag, else itll be null.
It will always try to map signInName claim to the email claim coming from AAD federation.
You cannot explicitly control what will get output based on another claim within the same technical profile.
What you can do is "if email exists, set signInName to null" after AAD-OIDC completes.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/string-transformations#nullclaim
<ClaimsTransformation Id="SetSignInNameToNull" TransformationMethod="NullClaim">
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="claim_to_null" />
</OutputClaims>
</ClaimsTransformation>
<TechnicalProfile Id="CT-SetSignInNameToNull">
<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="signInName" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="SetSignInNameToNull" />
</OutputClaimsTransformations>
</TechnicalProfile>
<OrchestrationStep Order="X" Type="ClaimsExchange">
<Preconditions>
<!-- precondition here to only run this step if Email exists, and it was AAD Federated auth -->
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SetSignInNameToNull" TechnicalProfileReferenceId="CT-SetSignInNameToNull" />
</ClaimsExchanges>

Related

AADB2C Custom Policy - Local and Social Account Sign policy with split email verification and sign up

I am trying to create an Azure AD B2C custom policy that has the following user journey -
Sign-in / Sign-up with Local Account and Social Accounts wherein the sign-up flow must split the email verification and the actual sign-up page.
To do this, I started with the sample policy - https://github.com/azure-ad-b2c/samples/tree/master/policies/sign-up-with-social-and-local-account
and added the EmailVerification and LocalAccountSignUpWithReadOnlyEmail technical profiles from the sample policy - https://github.com/azure-ad-b2c/samples/tree/master/policies/split-email-verification-and-signup
In order to trigger the split email verification and signup flow, I have set the SignUpTarget to EmailVerification.
I am able to see the sign-in/sign-up page and clicking on the sign up link triggers the email verification flow. However, I am not sure how to get the LocalAccountSignUpWithReadOnlyEmail technical profile triggered after the email verification. Adding this as part of a ClaimsExchange orchestration step causes validation errors while uploading my custom policy.
Here is how my user journey configuration looks like -
<UserJourneys>
<UserJourney Id="SignUpOrSignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection TargetClaimsExchangeId="FacebookExchange" />
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Check if the user has selected to sign in using one of the social providers -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="FacebookExchange" TechnicalProfileReferenceId="Facebook-OAUTH" />
<ClaimsExchange Id="EmailVerification" TechnicalProfileReferenceId="EmailVerification" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSignUpWithReadOnlyEmail" TechnicalProfileReferenceId="LocalAccountSignUpWithReadOnlyEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId).
This can only happen when authentication happened using a social IDP. If local account was created or authentication done
using ESTS in step 2, then an user account must exist in the directory by this time. -->
<OrchestrationStep Order="5" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent
in the token. -->
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect
from the user. So, in that case, create the user in the directory if one does not already exist
(verified using objectId which would be set from the last step if account was created in the directory. -->
<OrchestrationStep Order="7" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="8" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
Here is what the technical profiles look like -
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>Email Verification</DisplayName>
<TechnicalProfiles>
<!--Sample: Email verification only-->
<TechnicalProfile Id="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>
</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>
</TechnicalProfile>
<!-- This technical profile uses a validation technical profile to authenticate the user. -->
<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="SignUpTarget">EmailVerification</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" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<!--Sample: Sign-up self-asserted technical profile without Email verification-->
<TechnicalProfile Id="LocalAccountSignUpWithReadOnlyEmail">
<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>
<!-- Sample: Remove sign-up email verification -->
<Item Key="EnforceEmailVerification">False</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateReadonlyEmailClaim" />
</InputClaimsTransformations>
<InputClaims>
<!--Sample: Set input the ReadOnlyEmail claim type to prefilled the email address-->
<InputClaim ClaimTypeReferenceId="readOnlyEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<!-- Sample: Display the ReadOnlyEmail claim type (instead of email claim type)-->
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" 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" />
<!-- Optional claims, to be collected from the user -->
<!--OutputClaim ClaimTypeReferenceId="displayName" /-->
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<!-- Sample: Disable session management for sign-up page -->
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
Here is the error that I get when I tried to upload the policy -
Validation failed: 4 validation error(s) found in policy "B2C_1A_CUSTOM_SIGNUP_SIGNIN" of tenant "testtenant.onmicrosoft.com".User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 3 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 4 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 5 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.User journey "SignUpOrSignIn" in policy "B2C_1A_custom_signup_signin" of tenant "testtenant.onmicrosoft.com" has step 6 with 2 claims exchanges. It must be preceded by a claims provider selection in order to determine which claims exchange can be used.
Looking for some advice here...
The reason why you are getting this error is because you might have written User Journey ID SignUpOrSignIn in 2 files: Base/Extension and Replying Party Policy.
If the count of steps and ClaimsExchange ID is unique, then it will accept or else it will treat as 2 different ClaimsExchange and error will occur while uploading the RP Policy. Please make sure to not duplicate the User Journey, keep only one copy of the User Journey Steps or if you want to extend the Journey steps, then add the steps. For Example: In the Base Policy you have total 5 Steps, then in the Extension or in RP you can start adding new ClaimsExchange from 5th Step and the last step will be JwtIssuer/SamlIssuer.

azure ad b2c custompolicy,givenname and surname is removed from UI form,if AAD-UserReadUsingObjectId is included as ValidationTechnicalProfile

After social login, I want to capture givenname, surname, and some other information from the user and then store the user in the directory and then pass these details to external API.
But the moment I include "AAD-UserReadUsingObjectId" in the validation profile, the givenname and surname is removed from the user form. I am not sure why does this occur as I have to include the AAD-UserReadUsingObjectId to read other custom fields
self asserted technical profile :
<TechnicalProfile Id="SelfAsserted-ObtainUserInfo">
<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.selfasserted</Item>
<Item Key="language.button_continue">Next</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId"/>
<InputClaim ClaimTypeReferenceId="givenname"/>
<InputClaim ClaimTypeReferenceId="surName" />
<InputClaim ClaimTypeReferenceId="email"/>
<InputClaim ClaimTypeReferenceId="alternativeSecurityId"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId"/>
<OutputClaim ClaimTypeReferenceId="givenname" Required="true"/>
<OutputClaim ClaimTypeReferenceId="surName" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingAlternativeSecurityId" ContinueOnError="false">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingObjectId" ContinueOnError="false">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
<Precondition Type="ClaimsExist" ExecuteActionsIf="false">
<Value>objectId</Value>
<Action>SkipThisValidationTechnicalProfile</Action>
</Precondition>
</Preconditions>
</ValidationTechnicalProfile>
<ValidationTechnicalProfile ReferenceId="API-RegisterUserInExternalSystem" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
Observation: If I comment out the givenname, surname from the output claim of AAD-UserReadUsingObjectId these fields will be visible on the form.
Use Case: User Journey where User will do Social Login and then User will be checked in AAD B2C and if not present then will prompt user to provide few more details and then add it to AAD B2C and then call REST API to add Users in External System.
Here in your solution ValidationTechnical Profile of "AAD-UserReadUsingObjectId" where you
included GivenName, SurName etc, which is the Reason "SelfAsserted-ObtainUserInfo" technical profile considering the givenName and surName claim as Read Data but not considering as UserInputType which is the reason you are not able to see it .
Let me try to Provide you Solution how you can implement:
Orchestration Step1 : Technical Profile to Login with Social.
Orchestration Step2 : Technical Profile to ReadUsingAlternativeSecurityID, from here if your social account data exist in AAD B2C then you will get objectID.
Orchestration Step3 : Precondition to check if ObjectID present or not (You can add more precondition if you want), if it is not present then run the step where you run the TechnicalProfile Id="SelfAsserted-ObtainUserInfo" and in the validation profile include "AAD-UserWriteUsingAlternativeSecurityId" and "API-RegisterUserInExternalSystem" without any PreRequisite in the ValidationTechnicalProfile.
Refer this link to get the starter pack and refer UserJourney "SignUpOrSignIn".

Azure AD B2C Validation Profile for non-SelfAsserted Technical profiles

From testing, it appears Validation Technical Profiles are only used when added to SelfAssserted Technical Profiles
E.g the following:
<TechnicalProfile Id="ExternalIDP">
<DisplayName>Some External IdP</DisplayName>
<Protocol Name="OpenIdConnect" />
<Metadata>
<!-- ... -->
</Metadata>
<OutputClaims>
<!-- ... -->
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="FETCH-MORE-CLAIMS" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
does not appear to call the FETCH-MORE-CLAIMS profile after authenticating to the external identity provider.
Is this correct, and if so, is there another way to always force a second technical profile to be called whenever a particular technical profile is called?
One possible way would be to set an output claim that indicates that was done, and then have an orchestration step after that with a condition on that claim, which then runs your TP as a claims exchange.
So an output claim like:
<OutputClaim ClaimTypeReferenceId="idp" DefaultValue="ThisIdp" AlwaysUseDefaultValue="true" />
You'd need to define that claim if it isn't already defined, or you can use another one you already have.
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="false">
<Value>idp</Value>
<Value>ThisIdp</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="FetchMoreClaimsExchange" TechnicalProfileReferenceId="FETCH-MORE-CLAIMS" />
</ClaimsExchanges>
</OrchestrationStep>
This orchestration step is skipped if idp != ThisIdp, so it would only run if your external idp was used.

How to give a custom error message if on Forgotten Password Page of Azure B2C?

I want to put a custom message on when a user visits the forgotten password page of B2C. When they enter their email address and their "phonenumber" is not found then it should simply display an error message under the "Continue / Cancel" button saying something like "Not registered, contact Support" (Can be over the email text box as well where normal error messages come if its too much to do under it)
I have the user journey and orchestration steps to detect a precondition if phonenumber exists or not. But not sure how to do this custom error message. It is the order 2 of the below journey that needs that step to display that error message and finish (not run further steps)
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddress" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddress" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>phonenumber</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="LocalAccountDiscoveryUsingEmailAddressOTP" />
</ClaimsExchanges>
</OrchestrationStep>
..........
I think we can do it by using things like UserMessageIfClaimsPrincipalDoesNotExist and RaiseErrorIfClaimsPrincipalDoesNotExist in cobination as found in the custom profiles. But just looking for a tied up example to put the pieces together.
You can build claims transformations to:
Determine whether the phone number claim does exist
Ensure that it does exist and if not then show an error message
You must reference these claims transformations when the user account is retrieved so that the error message is shown in the first step.
To determine whether the phone number claim does exist, you use a DoesClaimExist claims transformation:
<ClaimsTransformation Id="DoesPhoneNumberExist" TransformationMethod="DoesClaimExist">
<InputClaims>
<InputClaim ClaimTypeReferenceId="phoneNumber" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="phoneNumberDoesExist" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
To ensure that the phone number does exist, you use a AssertBooleanClaimIsEqualToValue claims transformation:
<ClaimsTransformation Id="EnsurePhoneNumberDoesExist" TransformationMethod="AssertBooleanClaimIsEqualToValue">
<InputClaims>
<InputClaim ClaimTypeReferenceId="phoneNumberDoesExist" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="valueToCompareTo" DataType="boolean" Value="true" />
</InputParameters>
</ClaimsTransformation>
To show the error message, you must invoke the claims transformations from the AAD-UserReadUsingEmailAddress technical profile:
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress">
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="phoneNumber" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="DoesPhoneNumberExist" />
<OutputClaimsTransformation ReferenceId="EnsurePhoneNumberDoesExist" />
</OutputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
And then you must include the UserMessageIfClaimsTransformationBooleanValueIsNotEqual metadata in the LocalAccountDiscoveryUsingEmailAddress technical profile:
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
...
<Metadata>
...
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Whoops, you aren't registered, contact Support.</Item>
</Metadata>
...
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress" />
</ValidationTechnicalProfiles>
</TechnicalProfile>

Azure AD B2C Local Account Password stops working after a few hours

I have created custom policies for social and local accounts based on the example from the Active Directory B2C custom policy starter pack for social and local accounts. I have enabled the login with Microsoft and Google and tested that both work, I have also enabled logging in with a local account.
The problem I am seeing is the local account. I can create one and the password works fine for a few hours (not sure exactly how long), then starts giving a generic "Invalid username or password." error. When I type in the wrong password for the same user I get a different message "Your password is incorrect" (this corresponds to a relevant log entry).
I have enabled application insights and can only find the following exceptions.
Any help on how to clear up these 2 errors would be great.
""Statebag"": {
""Complex-CLMS"": {},
""ValidationRequest"": {
""ContentType"": ""Unspecified"",
""Created"": ""2017-10-04T19:17:49.2510644Z"",
""Key"": ""ValidationRequest"",
""Persistent"": true,
""Value"": ""client_id=307&resource=cf87&username=user%domain.com&password=fakep#ss!123&grant_type=password&scope=openid&nca=1;1;login-NonInteractive;False""
},
""ValidationResponse"": {
""ContentType"": ""Json"",
""Created"": ""2017-10-04T19:17:49.2510644Z"",
""Key"": ""ValidationResponse"",
""Persistent"": true,
""Value"": ""{\""error\"":\""invalid_grant\"",\""error_description\"":\""AADSTS65001: The user or administrator has not consented to use the application with ID '307' named 'IdentityExperienceFramework'. Send an interactive authorization request for this user and resource.\\r\\nTrace ID: 7c4\\r\\nCorrelation ID: 3cc\\r\\nTimestamp: 2017-10-04 19:17:49Z\"",\""error_codes\"":[65001],\""timestamp\"":\""2017-10-04 19:17:49Z\"",\""trace_id\"":\""7c4\"",\""correlation_id\"":\""3cc\""};1;login-NonInteractive;False""
},
""ComplexItems"": ""_MachineEventQ, REPRM, TCTX, M_EXCP""
}
Here is the 2nd exception
""Key"": ""Exception"",
""Value"": {
""Kind"": ""Handled"",
""HResult"": ""80131500"",
""Message"": ""The technical Profile with id \""AAD-UserWriteUsingLogonEmail\"" in Policy id \""B2C_1A_signup_signin of Tenant id \""xxx.onmicrosoft.com\"" requires that an error be raised if a claims principal record already exists for storing claims. A claims principal of type \""User\"" with identifier claim type id \""signInNames.emailAddress\"" does already exist."",
""Data"": {
""IsPolicySpecificError"": true,
""TenantId"": ""xxx.onmicrosoft.com"",
""PolicyId"": ""B2C_1A_signup_signin"",
""TechnicalProfile.Id"": ""AAD-UserWriteUsingLogonEmail"",
""ClaimsPrincipal.IdentifierClaim.ClaimTypeId"": ""signInNames.emailAddress"",
""ClaimsPrincipal.PrincipalType"": ""User"",
""CreateClaimsPrincipalIfItDoesNotExist"": ""True"",
""RaiseErrorIfClaimsPrincipalAlreadyExists"": ""True"",
""RaiseErrorIfClaimsPrincipalDoesNotExist"": ""False""
}
}
Here is the content of the TrustFrameworkExtensions.xml file. The only difference between it and the example is that I am using 2 providers instead of 1.
<?xml version="1.0" encoding="utf-8" ?>
<TrustFrameworkPolicy
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="xxx.onmicrosoft.com"
PolicyId="B2C_1A_TrustFrameworkExtensions"
PublicPolicyUri="http://xxx.onmicrosoft.com/B2C_1A_TrustFrameworkExtensions">
<BasePolicy>
<TenantId>xxx.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkBase</PolicyId>
</BasePolicy>
<BuildingBlocks>
</BuildingBlocks>
<ClaimsProviders>
<ClaimsProvider>
<DisplayName>Local Account SignIn</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="login-NonInteractive">
<Metadata>
<Item Key="client_id">307</Item>
<Item Key="IdTokenAudience">cf8</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="client_id" DefaultValue="307" />
<InputClaim ClaimTypeReferenceId="resource_id" PartnerClaimType="resource" DefaultValue="cf8" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<Domain>Employee SignIn with Azure AD</Domain>
<DisplayName>Employee Login</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AzureADProfile">
<DisplayName>Employee Login</DisplayName>
<Description>Login with your GP account</Description>
<Protocol Name="OpenIdConnect"/>
<OutputTokenFormat>JWT</OutputTokenFormat>
<Metadata>
<Item Key="METADATA">https://login.windows.net/yyy.onmicrosoft.com/.well-known/openid-configuration</Item>
<Item Key="ProviderName">https://sts.windows.net/7de/</Item>
<Item Key="client_id">f19</Item>
<Item Key="IdTokenAudience">f19</Item>
<Item Key="response_types">id_token</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_AzureADAppSecret"/>
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" 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="authenticationSource" DefaultValue="contosoAuthentication" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="AzureADContoso" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<Domain>google.com</Domain>
<DisplayName>Google</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="Google-OAUTH">
<DisplayName>Employee Login</DisplayName>
<Protocol Name="OAuth2" />
<Metadata>
<Item Key="ProviderName">google</Item>
<Item Key="authorization_endpoint">https://accounts.google.com/o/oauth2/auth</Item>
<Item Key="AccessTokenEndpoint">https://accounts.google.com/o/oauth2/token</Item>
<Item Key="ClaimsEndpoint">https://www.googleapis.com/oauth2/v1/userinfo</Item>
<Item Key="scope">email</Item>
<Item Key="HttpBinding">POST</Item>
<Item Key="UsePolicyInRedirectUri">0</Item>
<Item Key="client_id">zzz.apps.googleusercontent.com</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_GoogleSecret" />
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="id" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="family_name" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="google.com" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName" />
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName" />
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId" />
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId" />
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin" />
<ErrorHandlers>
<ErrorHandler>
<ErrorResponseFormat>json</ErrorResponseFormat>
<ResponseMatch>$[?(##.error == 'invalid_grant')]</ResponseMatch>
<Action>Reauthenticate</Action>
<!--In case of authorization code used error, we don't want the user to select his account again.-->
<!--AdditionalRequestParameters Key="prompt">select_account</AdditionalRequestParameters-->
</ErrorHandler>
</ErrorHandlers>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignUpOrSignInUsingAzureAD">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
<ClaimsProviderSelection TargetClaimsExchangeId="GoogleExchange" />
<ClaimsProviderSelection TargetClaimsExchangeId="AzureADExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Check if the user has selected to sign in using one of the social providers -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="GoogleExchange" TechnicalProfileReferenceId="Google-OAUTH" />
<ClaimsExchange Id="AzureADExchange" TechnicalProfileReferenceId="AzureADProfile" />
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- For social IDP authentication, attempt to find the user account in the directory. -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>localAccountAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId).
This can only happen when authentication happened using a social IDP. If local account was created or authentication done
using ESTS in step 2, then an user account must exist in the directory by this time. -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent
in the token. -->
<OrchestrationStep Order="5" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimEquals" ExecuteActionsIf="true">
<Value>authenticationSource</Value>
<Value>socialIdpAuthentication</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect
from the user. So, in that case, create the user in the directory if one does not already exist
(verified using objectId which would be set from the last step if account was created in the directory. -->
<OrchestrationStep Order="6" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
</UserJourneys>
</TrustFrameworkPolicy>
Since the actual error could be due to a combination of policy and application configuration in the directory, I'll explain why these applications are required to be setup this way, and what could potentially be the issue. I am hoping that even if my guess on the root cause is wrong, this explanation will allow you to debug the issue further.
Think of an application that needs to validate a password for a user. Since the application does not have access to the password, one of the ways is to send an authentication request to login.microsoftonline.com with user's ID and password, and see if a token is successfully issued.
However, when this application sends a request, it must specify some resource, and must also have permissions on user's behalf to access that resource. A common workaround is for the administrator to grant permissions (i.e. consent) on behalf of all users.
IEF uses the same model. Your policy has two applications, one acts as client that gets a token on user's behalf to the resource. If you followed instructions on the "Custom Policies Get Started Page", then ProxyIdentityExperienceFramework is the client, and IdentityExperienceFramework is the resource. As per those instructions, only ProxyIdentityExperienceFramework can get a token to IdentityExperienceFramework on user's behalf and not vice versa.
Based on the error message in your logs, it seems that you may have reversed the IDs for the client and the resource:
The user or administrator has not consented to use the application
with ID '307' named 'IdentityExperienceFramework'
That is, the client called IdentityExperienceFramework does not have access to get a token.
So you will need to check in the Azure AD portal which application has access to which application and update client ID and resource ID.
The second exception may really be by design, it's just telling you that the user that is being created in the directory already exists. This typically happens in sign up and, based on your policy settings, it will typically render an error message to the user indicating that the account already exists and they should instead sign in.
This error
The user or administrator has not consented to use the application with ID '307' named 'IdentityExperienceFramework'.
have you pressed the Grant Permissions button on this app within the AZURE AD blade within Azure
2nd exception
It looks like you are trying to write the user principal again and in your metadata it has the RaiseErrorIfUserPrincipalExists set to rue
The strange thing is neither really helps me to understand why your password doesnt work after a few hours
Best bet is to copy the policy here and we can attempt to tell you why your trying a write instead of a read

Resources