Azure B2C Custom Policy - Custom technical profile doesn't work in SignUp - azure

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.

Related

Issue token for application access from ADB2C

Application 1 creates a url with id_token that contains all the necessary info to validate a claim (as of now simply validate the user with email & signed claim).
a. Create a self-signed certificate (New-SelfSignedCertificate )
b. For the same, used tokenbuilder to create a URL as :
https://tenant.b2clogin.com/tenant.onmicrosoft.com/oauth2/v2.0/authorize?p=B2C_1A_SIGNIN_WITH_EMAIL&client_id=9994db7f-2dc7-4bac-bf8a-93bf04067890&redirect_uri=https://myredirecturl.com/&nonce=80c307115..&scope=openid&response_type=id_token&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IlZWWWwwbUE0YlFYOXg0LVZVTGpfb0VUWHd0VSIsIng1dCI6I......nNpb25fYWdlbmN5
c. Hosted the application with self-signed certificate (Example :
https://b2cidtokenbuilder.azurewebsites.net/)
d. In policy blade created a token with the self-signed certificate (following steps in here)
e. In TrustFrameworkBase updated issuer_secret for JwtIssuer Technical Profile
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_AABIDToken" />
<Key Id="issuer_refresh_token_key" StorageReferenceId="B2C_1A_TokenEncryptionKeyContainer" />
</CryptographicKeys>
f. In B2C_1A_SIGNIN_WITH_EMAIL policy added the
<Metadata
<Item Key="METADATA">https://tenant.b2clogin.com/tenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1A_SIGNUP_SIGNIN</Item>
<Item Key="issuer">https://b2cidtokenbuilder.azurewebsites.net/</Item>
</Metadata>
g. Once I click the AD B2C url I am expecting that the policy B2C_1A_SIGNIN_WITH_EMAIL will allow me to login to the application with an access_token.
(Based on info here I should be able to login to my application once the accesstoken has been created by my policy.
But in my current application what I see is not access_token is getting created but an id_token is created with all the info and since there is no access-token my application is asking for AD B2C login (prompt).
https://myapplication.com/#id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiGVla2FiMmNkZXYuYjJjbG9naW4uY29tLzM4NGIzNTdiLTIzMWUtNGY3MS1iYThhLWUxNzBlZDFiMzA1ZC92Mi4....................kLTE5NDYtNGRkYy04YWViLWY5ZDFiMWVmNWMyMSIsImF1Z
Should I be setting some sessionmanagement TP?
My policy is
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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="tenant.onmicrosoft.com"
PolicyId="B2C_1A_signin_with_email"
PublicPolicyUri="http://tenant.onmicrosoft.com/B2C_1A_signin_with_email">
<BasePolicy>
<TenantId>tenant.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<BuildingBlocks>
<ClaimsSchema>
<!--Sample: Stores the error message for unsolicited request (a request without id_token_hint) and user not found-->
<ClaimType Id="errorMessage">
<DisplayName>Error</DisplayName>
<DataType>string</DataType>
<UserHelpText>Add help text here</UserHelpText>
<UserInputType>Paragraph</UserInputType>
</ClaimType>
</ClaimsSchema>
<ClaimsTransformations>
<!--Sample: Initiates the errorMessage claims type with the error message-->
<ClaimsTransformation Id="CreateUnsolicitedErrorMessage" TransformationMethod="CreateStringClaim">
<InputParameters>
<InputParameter Id="value" DataType="string" Value="You cannot sign-in without invitation. Please click the link in the email" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
</OutputClaims>
</ClaimsTransformation>
<!--Sample: Initiates the errorMessage claims type with the error message user not found-->
<ClaimsTransformation Id="CreateUserNotFoundErrorMessage" TransformationMethod="CreateStringClaim">
<InputParameters>
<InputParameter Id="value" DataType="string" Value="You aren't registered in the system! Please contact the Help Desk on 0800 xxx yyy and ask to be added to the system" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
</OutputClaims>
</ClaimsTransformation>
</ClaimsTransformations>
</BuildingBlocks>
<ClaimsProviders>
<!--Sample: This technical profile specifies how B2C should validate your token, and what claims you want B2C to extract from the token.
The METADATA value in the TechnicalProfile meta-data is required.
The “IdTokenAudience” and “issuer” arguments are optional (see later section)-->
<ClaimsProvider>
<DisplayName>My ID Token Hint ClaimsProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="IdTokenHint_ExtractClaims">
<DisplayName> My ID Token Hint TechnicalProfile</DisplayName>
<Protocol Name="None" />
<Metadata>
<!--Sample action required: replace with your endpoint location -->
<Item Key="METADATA">https://teanant.b2clogin.com/tenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1A_SIGNUP_SIGNIN</Item>
<Item Key="issuer">https://b2cidtokenbuilder.azurewebsites.net/</Item>
</Metadata>
<OutputClaims>
<!--Sample: Read the email cliam from the id_token_hint-->
<OutputClaim ClaimTypeReferenceId="email" />
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>Self Asserted</DisplayName>
<TechnicalProfiles>
<!-- Demo: Show error message-->
<TechnicalProfile Id="SelfAsserted-Error">
<DisplayName>Unsolicited error message</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>
<!-- Sample: Remove the continue button-->
<Item Key="setting.showContinueButton">false</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="errorMessage"/>
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="errorMessage"/>
</OutputClaims>
</TechnicalProfile>
<!-- Demo: Show unsolicited error message-->
<TechnicalProfile Id="SelfAsserted-Unsolicited">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUnsolicitedErrorMessage" />
</InputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-Error" />
</TechnicalProfile>
<!-- Demo: Show user not found error message-->
<TechnicalProfile Id="SelfAsserted-UserNotFound">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUserNotFoundErrorMessage" />
</InputClaimsTransformations>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-Error" />
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingEmailAddress-Hint">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item>
<Item Key="UserMessageIfClaimsPrincipalDoesNotExist">An account could not be found for the provided user ID.</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" />
</InputClaims>
<OutputClaims>
<!-- Required claims -->
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="localAccountAuthentication" />
<!-- Sample: Change the output email claims-->
<!--<OutputClaim ClaimTypeReferenceId="email" />-->
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="extension_userId" />
<OutputClaim ClaimTypeReferenceId="extension_tenantAbbreviation" />
<OutputClaim ClaimTypeReferenceId="extension_dateTimeFormat" />
</OutputClaims>
<!-- <OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="AssertAccountEnabledIsTrue" />
</OutputClaimsTransformations> -->
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
<UserJourneys>
<UserJourney Id="SignInWithEmail">
<OrchestrationSteps>
<!--Sample: Read the input claims from the id_token_hint-->
<OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />
<!-- Sample: Check if user tries to run the policy without invitation -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>email</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Read the user properties from the directory-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadUsingEmailAddress" TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-Hint"/>
</ClaimsExchanges>
</OrchestrationStep>
<!-- Sample: Check whether the user not existed in the directory -->
<OrchestrationStep Order="4" Type="ClaimsExchange">
<Preconditions>
<Precondition Type="ClaimsExist" ExecuteActionsIf="true">
<Value>objectId</Value>
<Action>SkipThisOrchestrationStep</Action>
</Precondition>
</Preconditions>
<ClaimsExchanges>
<ClaimsExchange Id="SelfAssertedUserNotFound" TechnicalProfileReferenceId="SelfAsserted-UserNotFound" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Sample: Issue an access token-->
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb"/>
</UserJourney>
</UserJourneys>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignInWithEmail" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<!--Sample: Set the input claims to be read from the id_token_hint-->
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="correlationId" DefaultValue="{Context:CorrelationId}" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="extension_userId" />
<OutputClaim ClaimTypeReferenceId="extension_tenantAbbreviation" />
<OutputClaim ClaimTypeReferenceId="extension_dateTimeFormat" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>

How to Implement Azure AD B2C Identity Experience Framework Sign-In with Optional Email OTP

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.

Azure B2C Rest API Error Still Creating Account

I have created a REST API for Azure B2C to return a claim or an error during the account creation flow.
In my Custom Policy I have hooked up the API and it gets called.
However if the API returns either a 400 or 409, the account is still created but the user is presented with the error message on the create page. The user's account is still created despite the error.
The user then fixes the error and clicks create again but can't create the account because it was already created.
I have followed the instructions here:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-rest-api-claims-validation
My Claims Provider looks like this and claim from the REST API is called VerifiedDateOfBirth:
<ClaimsProvider>
<DisplayName>REST API</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-Validation">
<DisplayName>Check date of birth</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">{REST URL}}</Item>
<Item Key="SendClaimsIn">Body</Item>
<!-- Set AuthenticationType to Basic or ClientCertificate in production environments -->
<Item Key="AuthenticationType">None</Item>
<!-- REMOVE the following line in production environments -->
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<!-- Claims sent to your REST API -->
<InputClaim ClaimTypeReferenceId="dateOfBirth" />
</InputClaims>
<OutputClaims>
<!-- Claims parsed from your REST API -->
<OutputClaim ClaimTypeReferenceId="VerifiedDateOfBirth" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
And the technical profile:
<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>
</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" />
<!-- Optional claims, to be collected from the user -->
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
<OutputClaim ClaimTypeReferenceId="dateOfBirth" Required="true" />
<OutputClaim ClaimTypeReferenceId="VerifiedDateOfBirth" Required="true" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
<ValidationTechnicalProfile ReferenceId="REST-Validation" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
When the error occurs I see the following error on the create page:
Do I need to add some additional configuration?
The order of your validation profiles matter in your LocalAccountSignUpWithLogonEmail technical profile. It looks like the first validation that was taking place was the writing of the user account.
Try this instead:
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-Validation" />
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>

Azure B2C custom policy with Rest API not sending objectId in request

I'm trying to setup a custom Azure B2C policy. The policy sends the request to the endpoint but doesn't populate the objectId in the body
<ClaimsProvider>
<DisplayName>REST API SignUp</DisplayName>
<TechnicalProfiles>
<!-- Custom Restful service -->
<TechnicalProfile Id="REST-API-SignUp">
<DisplayName>Validate user's input data and Save details to Web API</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://xxxxxxxxxxx.azurewebsites.net/api/Users/RegisterNewUser</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="email" />
<InputClaim ClaimTypeReferenceId="displayName"/>
<InputClaim ClaimTypeReferenceId="surname" />
<InputClaim ClaimTypeReferenceId="phoneNumber" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="roles" PartnerClaimType="roles" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
<!-- Add validation technical profile to LocalAccountSignUpWithLogonEmail technical profile -->
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.localaccountsignup</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="roles" PartnerClaimType="roles" DefaultValue="" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
</ClaimsProviders>
All the other InputClaims gets populated in the request body except objectId
{"email":"sakkiexxx#xxxxch","displayName":"Sakkie","surname":"NA","phoneNumber":"9876543215"}
I have the following orchestration steps
<UserJourney Id="SignUp">
<OrchestrationSteps>
<!-- Track that we have received a sign in request -->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSignUp-RequestReceived" TechnicalProfileReferenceId="AppInsights-SignUpRequest" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
...
and then the LocalAccountSignUpWithLogonEmail technical profile outputs the object Id
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<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">Sign Up</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" Required="true" />
<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" />
<!-- Optional claims, to be collected from the user -->
<OutputClaim ClaimTypeReferenceId="displayName" />
<!-- <OutputClaim ClaimTypeReferenceId="givenName" /> -->
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="phoneNumber" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
............
I update my user journey to include the API call as a separate step, this sorted out the objectId
<UserJourney Id="SignUp">
<OrchestrationSteps>
<!-- Track that we have received a sign in request -->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="TrackSignUp-RequestReceived" TechnicalProfileReferenceId="AppInsights-SignUpRequest" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="Web-API-Update" TechnicalProfileReferenceId="REST-API-SignUp" />
</ClaimsExchanges>
</OrchestrationStep>
Adding an extra step to your User Journey is one of the options.
You could also use a ValidationProfile as an alternative, in that case you would change the following XML
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
To this
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
There is one 'but' and that is that both solutions have a downside. The ObjectId is generated in technical profile AAD-UserWriteUsingLogonEmail. Which means that your account is created in Azure AD. If your next step or validation profile failed to call the API you might end up in a state where the account is registered but the object id is not stored in your API. This is also the other way around, if your API is providing additional claims to the user, these are not saved on the users' profile.
More info about the technical profile AAD-UserWriteUsingLogonEmail can be found on the official docs:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#azure-ad-technical-provider-operations
Same for Validation Technical Profiles:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/validation-technical-profile
This is exactly what happens for me too. I tried adding the
AAD-UserWriteUsingLogonEmail as a first node in the validation technical profiles in the TrustFrameworkExtensions.xml file (note that in the TrustFrameworkBase.xml, the AAD-UserWriteUsingLogonEmail already exists. I don't want to change the base as its not recommended.
In TrustFrameworkExtensions.xml (for LocalAccountsSignUpWithLogonEmail)
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
<ValidationTechnicalProfile ReferenceId="REST-API-SignUp" />
</ValidationTechnicalProfiles>
In TrustFrameworkBase.xml (for LocalAccountsSignUpWithLogonEmail)
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail"/>
</ValidationTechnicalProfiles>
but what happens is that I think the the AAD-UserWriteUsingLogonEmail gets called twice. once in the extension and then once in the base.
Does anyone actually know how this inheritance or overriding work? If you define something in the extension xml, does it overwrite or get merged? if so in what order? it seems that the base node gets called first, so that's why oid isn't populated. but then when you put it in the extension as a first node (you want it to overwrite the base), the base gets called again. is there anyway to change the order or say "I want this to replace (override) what is in the base? Is it merged? There is no documentation on how base policies work.
I will try changing the journey as recommended, but would still like to know how having a base policy works, if anyone can answer

B2C/IEF Password reset with username

I am creating a custom B2C policy and I am trying to replicate the password reset journey for local accounts created with a username.
I can read the username from AD but I am unsure how to validate the verified email address against the account.
Currently if the username is correct any email address can be used to verify.
Technical profile:
<TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
<DisplayName>Reset password using logon name</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
Validation Technical Profile:
<TechnicalProfile Id="AAD-UserReadUsingLogonName">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalAlreadyExists">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" PartnerClaimType="signInNames.userName" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
User Journey:
<UserJourney Id="PasswordReset">
<OrchestrationSteps>
<!--Get user by username-->
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="PasswordResetUsingEmailAddressExchange" TechnicalProfileReferenceId="SA-LocalAccountDiscoveryUsingLogonName" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Reset password-->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="NewCredentials" TechnicalProfileReferenceId="SA-LocalAccountPasswordReset" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Read remaining attributes of user-->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="ReadUser" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<!--Create token-->
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
If you write the email address to both the "otherMails" and "strongAuthenticationEmailAddress" properties during the sign-up policy, then you can verify that the email address is associated with the user name during the password reset policy using a REST API.
This REST API must be declared as a claims provider:
<ClaimsProvider>
<DisplayName>REST APIs</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="RestApi-CheckUser">
<DisplayName>Check User REST API</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">Insert the REST API endpoint URL</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The REST API can query the user object by the "signInNames" and "otherMails" properties using the Azure AD Graph API (you can't read the "strongAuthenticationEmailAddress" property using this Graph API) and, as described in the REST API walkthrough, then return 200 OK if the email address is associated with the user name or 409 Conflict if not so.
The REST API technical profile can then be invoked as a validation technical profile from the "SA-LocalAccountDiscoveryUsingLogonName" technical profile:
<TechnicalProfile Id="SA-LocalAccountDiscoveryUsingLogonName">
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="RestApi-CheckUser" />
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingLogonName" />
</ValidationTechnicalProfiles>
</TechnicalProfile>
If the "RestApi-CheckUser" technical profile returns 200 OK, then the "AAD-UserReadUsingLogonName" technical profile is invoked and the end user can continue with the password reset. If the "RestApi-CheckUser" technical profile returns 409 Conflict, then the "AAD-UserReadUsingLogonName" technical profile isn't invoked and the end user can't continue.

Resources