Get singin email from Azure AD in a B2C custom policy - azure-ad-b2c

In AAD B2C I have users created with 2 identities. I used graph to create them with this body :
{
"displayName": "John Doe",
"mail":"johndoe19287456#gmail.com",
"identities": [
{
"signInType": "userName",
"issuer": "mytenant.onmicrosoft.com",
"issuerAssignedId": "606198"
},
{
"signInType": "emailAddress",
"issuer": "mytenant.onmicrosoft.com",
"issuerAssignedId": "johndoe19287456#gmail.com"
}
],
"passwordProfile" : {
"password": "Soleil!23",
"forceChangePasswordNextSignIn": false
},
"passwordPolicies": "DisablePasswordExpiration"
}
This allow the user to connect either with an email (johndoe19287456#gmail.com) or an ID (606198).
When a user input his ID and then click on the "Forgot password?" link, I'd like to get the email value from AAD so the user cannot input whatever he wants. But I'd still like it to be "verified" by sending a code to that email address. I have 2 problems :
I can't fnd a way to get the email value from AzureActiveDirectoryProvider
I can't find a way to populate the Verified.Email field (and make it readonly).
Here's a sample of one of the many things I've tried yet.
Building blocks custom claims :
<ClaimType Id="ReadOnlyEmail">
<DisplayName>Verified Email Address</DisplayName>
<DataType>string</DataType>
<UserInputType>Readonly</UserInputType>
</ClaimType>
<ClaimType Id="emailFromAAD">
<DisplayName>Email from AAD</DisplayName>
<DataType>string</DataType>
<UserHelpText />
<UserInputType>Readonly</UserInputType>
</ClaimType>
<ClaimType Id="readOnlySignInName">
<DisplayName>Sign in name</DisplayName>
<DataType>string</DataType>
<UserHelpText />
<UserInputType>Readonly</UserInputType>
</ClaimType>
<ClaimType Id="emailValue">
<DisplayName>Matched mail</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="isEmailBoolean">
<DisplayName>is Email</DisplayName>
<DataType>boolean</DataType>
</ClaimType>
<ClaimType Id="strongAuthenticationEmailAddress">
<DisplayName>string</DisplayName>
<DataType>string</DataType>
<AdminHelpText>Email address that the user can use for strong authentication.</AdminHelpText>
<UserHelpText>Email address to use for strong authentication.</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
Claims transformation :
<ClaimsTransformation Id="CopySignInNameFromReadOnly" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<!-- If signin name match the regex, it is an email identifier. Oterwise, it'll be considered as username -->
<ClaimsTransformation Id="isEmail" TransformationMethod="setClaimsIfRegexMatch">
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" TransformationClaimType="claimToMatch" />
</InputClaims>
<InputParameters>
<InputParameter Id="matchTo" DataType="string" Value="[^#]+#[^\.]+\..+" />
<InputParameter Id="outputClaimIfMatched" DataType="string" Value="isEmail" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailValue" TransformationClaimType="outputClaim" />
<OutputClaim ClaimTypeReferenceId="isEmailBoolean" TransformationClaimType="regexCompareResultClaim" />
</OutputClaims>
</ClaimsTransformation>
Technical profiles:
<!-- Password reset step 1b - Included in step SelfAsserted-LocalAccountLookup-Combined-PwdReset
That's where the input claim is define. signInName = the Username field on the screen -->
<TechnicalProfile Id="SelfAsserted-LocalAccountLookup-Combined-SignUp">
<DisplayName>Local Account Sign Up</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>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" DefaultValue="{OIDC:LoginHint}" AlwaysUseDefaultValue="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlySignInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="isEmailBoolean" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopySignInNameFromReadOnly" />
</OutputClaimsTransformations>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="regexAnalysisUsername" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<!-- Password reset step 1a. Includes SelfAsserted-LocalAccountLookup-Combined-SignUp
Input claim is defined in this. Here, we define output claims -->
<TechnicalProfile Id="SelfAsserted-LocalAccountLookup-Combined-PwdReset">
<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="setting.showCancelButton">false</Item>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlySignInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="isEmailBoolean" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="emailFromAAD" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingIdentifier" />
<ValidationTechnicalProfile ReferenceId="regexAnalysisUsername" />
</ValidationTechnicalProfiles>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-LocalAccountLookup-Combined-SignUp" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<!-- Password reset step 1c. Verify signin name on the "Continue" button clicked in the first screen -->
<TechnicalProfile Id="AAD-UserReadUsingIdentifier">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" PartnerClaimType="signInNames" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="emailFromAAD" PartnerClaimType="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<!-- Passwod reset step 2. Only if sign in data was a username -->
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<DisplayName>Reset password using username</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Other values maybe defined in localized api.selfasserted.fr and api.selfasserted.en -->
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
<Item Key="AllowGenerationOfClaimsWithNullValues">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">An account could not be found for the provided User ID and email combination.</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
<Item Key="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<!-- Reduce the default self-asserted retry limit of 7 for the reset journey -->
<Item Key="setting.retryLimit">5</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlySignInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" DefaultValue="strongAuthenticationEmailAddress" Required="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
<OutputClaim ClaimTypeReferenceId="emailFromAAD" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingUserNameAndValidateStrongAuthenticationEmailAddress" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
For now, I get the first screen with the readonly ID working.
The next screen presents the textboxes for email. The Email from AAD shouldn't visible.
But anyway it is empty, showing that it didn't get anything from AAD or I failed to properly store it in the claim bag or pass it down. Note that I have tried getting the value both from "strongAuthenticationEmailAddress" and "signInNames.emailAddress" based on Microsoft documentation but none of it works. Maybe it's in the way I define my output claim with PartnerClaimType in the AAD-UserReadUsingIdentifier profile?
To make it clear for everyone, here's what I'd like to have. A simple page with 2 reaonly fields with a button to send the code and then another one to continue after the code has been verified.
Can anyone help me with this one?
I started from the B2C custom policies starter pack and added customization from this community repo.
UPDATE
Here is what I get from MS Graph when querying for my user :
{
"#odata.context": "https://graph.microsoft.com/v1.0/$metadata#users(identities,id,displayName,mail,surname,userPrincipalName,extension_06a19ccd80a0430c9730c62e4d96c895_ClientID,extension_06a19ccd80a0430c9730c62e4d96c895_requiresMigration)/$entity",
"id": "051e***************",
"displayName": "test - Tests2",
"mail": "johndoe19287456#gmail.com",
"surname": null,
"userPrincipalName": "051ea****************#mytenant.onmicrosoft.com",
"identities": [
{
"signInType": "emailAddress",
"issuer": "mytenant.onmicrosoft.com",
"issuerAssignedId": "johndoe19287456#gmail.com"
},
{
"signInType": "userName",
"issuer": "mytenant.onmicrosoft.com",
"issuerAssignedId": "606198"
},
{
"signInType": "userPrincipalName",
"issuer": "mytenant.onmicrosoft.com",
"issuerAssignedId": "051ea****************#mytenant.onmicrosoft.com"
}
]
}

You must provide the emailFromAAD as an input claim in LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress for it to be pre populated here.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/self-asserted-technical-profile#input-claims
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" PartnerClaimType="signInNames" Required="true" />
<InputClaim ClaimTypeReferenceId="emailFromAAD" />
</InputClaims>
Now it will be prepopulated on this page.
If you want to force verification of this email, copy it into a read only claim using a claim transform, then do:
<OutputClaim ClaimTypeReferenceId="readOnlyEmailFromAAD" PartnerClaimType="Verified.Email" Required="true" />

I managed to get email address from AAD by following Jas Suri - MSFT advice with a few other things. So just to sum things up here's what I did. I modified the Validation TP AAD-UserReadUsingIdentifier by replacing the output claim emailFromAAD simply by signInNames.emailAddress (no more PartnerClaimType). In the LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress TP, I added the InputClaim emailFromAAD that I populate with a CopyClaim input claims transformation. I also cleaned up the first TP of the journey SelfAsserted-LocalAccountLookup-Combined-PwdReset :
<ClaimsTransformation Id="CopySignInEmailAddressToEmail" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInNames.emailAddress" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailFromAAD" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<TechnicalProfile Id="SelfAsserted-LocalAccountLookup-Combined-PwdReset">
<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="setting.showCancelButton">false</Item>
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlySignInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="isEmailBoolean" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingIdentifier" />
<ValidationTechnicalProfile ReferenceId="regexAnalysisReadOnlyUsername" />
</ValidationTechnicalProfiles>
<IncludeTechnicalProfile ReferenceId="SelfAsserted-LocalAccountLookup-Combined-SignUp" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
<TechnicalProfile Id="AAD-UserReadUsingIdentifier">
<Metadata>
<Item Key="Operation">Read</Item>
<Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">true</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" PartnerClaimType="signInNames" Required="true" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="signInNames.emailAddress" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
</OutputClaims>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
</TechnicalProfile>
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<DisplayName>Reset password using username</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<!-- Other values maybe defined in localized api.selfasserted.fr and api.selfasserted.en -->
<Item Key="IpAddressClaimReferenceId">IpAddress</Item>
<Item Key="ContentDefinitionReferenceId">api.localaccountpasswordreset</Item>
<Item Key="AllowGenerationOfClaimsWithNullValues">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">An account could not be found for the provided User ID and email combination.</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
<Item Key="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<!-- Reduce the default self-asserted retry limit of 7 for the reset journey -->
<Item Key="setting.retryLimit">5</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopySignInEmailAddressToEmail" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlySignInName" />
<InputClaim ClaimTypeReferenceId="emailFromAAD" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readOnlySignInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="emailFromAAD" PartnerClaimType="Verified.Email" Required="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingUserNameAndValidateStrongAuthenticationEmailAddress" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>

Related

Custom Email Verification breaks two-step Azure AD B2C Custom Sign-Up Policy

I have created a 2 step sign-up custom policy. Where the first step verifies the email, and the second step generates a new password, and it was working well.
Then I followed all the steps here:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-email-sendgrid?pivots=b2c-custom-policy
to create a custom email verifier, so I could send the verification code with custom content.
This, also seems to work fine.
However, adding it changed my flow somehow so that the email claim doesn't seem to be outputted by my first step and my claim transformation in my second step which uses the 'email' claim is failing.
In other words this works fine without the display claims portion below, and starts failing after I add it. What am I missing?
<ClaimsTransformation Id="CreateReadonlyEmailClaim" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readonlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<!-- 2 step partner sign-up -->
<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.signUpVerifyEmailPage</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">A user with this email address already exists.</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" />
</InputClaims>
<!-- this breaks the next step when I add the display claims -->
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.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>
<TechnicalProfile Id="PartnerSignUpSetNewPasswordPage">
<DisplayName>Local 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.signUpSetNewPasswordPage</Item>
<Item Key="EnforceEmailVerification">False</Item>
</Metadata>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateReadonlyEmailClaim" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="readOnlyEmail" />
</InputClaims>
<OutputClaims>
<!--OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="readOnlyEmail" /-->
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="newUser" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
</ValidationTechnicalProfiles>
<IncludeTechnicalProfile ReferenceId="AAD-Common" />
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
Removing PartnerClaimType="Verified.Email" from the output claim made it work.
Credit to Jas on this older post: https://github.com/azure-ad-b2c/samples/issues/193

How to update an Azure AD B2C Username sample for sending custom emails with SendGrid?

I'm using this policy for Username-based SUSI extended off of this starter pack for local account SUSI. But I had also changed that starter pack for custom verification emails with SendGrid according to the how-to found here. The requirements for login went from "just use B2C" to "login with a username and any number of users may share a verification email."
I know that TrustFrameworkExtensions_Username is extending TrustFrameworkExtensions from the local account starter pack. I can apply the same overrides from the "Local Account" ClaimsProvider over the LocalAccountSignUpWithLogonEmail and LocalAccountDiscoveryUsingEmailAddress TechnicalProfiles to the "Local Account SignIn" ClaimsProvider's LocalAccountSignUpWithLogonName and LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress TechnicalProfiles directly.
The policy inheritance looks like this:
LocalAccounts starter pack TrustFrameworkBase
|
v
LocalAccounts starter pack TrustFrameworkLocalization
|
v
LocalAccounts starter pack TrustFrameworkExtensions <- modified for custom emails via SendGrid
|
v
username-signup-or-signin sample TrustFrameworkExtenions_Username
| |
v v
username-signup-or-signin sample SignupOrSignin_Username and PasswordReset_Username
The display controls in local extensions from the how-to:
<DisplayControls>
<DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
<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="SendOtp" />
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>
Tech profiles referenced from the display controls:
<ClaimsProvider>
<DisplayName>One time password technical profiles</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="GenerateOtp">
<DisplayName>Generate one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">GenerateCode</Item>
<Item Key="CodeExpirationInSeconds">1200</Item>
<Item Key="CodeLength">6</Item>
<Item Key="CharacterSet">0-9</Item>
<Item Key="ReuseSameCode">true</Item>
<Item Key="NumRetryAttempts">5</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otp" PartnerClaimType="otpGenerated" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="VerifyOtp">
<DisplayName>Verify one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">VerifyCode</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
<InputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpToVerify" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<ClaimsProvider>
<DisplayName>RestfulProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SendOtp">
<DisplayName>Use SendGrid's email API to send the code the the user</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://api.sendgrid.com/v3/mail/send</Item>
<Item Key="AuthenticationType">Bearer</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="ClaimUsedForRequestPayload">emailRequestBody</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BearerAuthenticationToken" StorageReferenceId="B2C_1A_SendGridSecret" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GenerateEmailRequestBody" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailRequestBody" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
The SUSI tech profile overrides in the local extensions:
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
<DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
<DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</DisplayClaims>
</TechnicalProfile>
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
I can get the sign-up part of the journey working in this manner - I could take the overrides in the extension changes found in the how-to here and add the DisplayClaims to the LocalAccountSignUpWithLogonName TechnicalProfile in
TrustFrameworkExtensions_Username.xml:
<TechnicalProfile Id="LocalAccountSignUpWithLogonName">
<DisplayName>User ID 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="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<Item Key="language.button_continue">Create</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<DisplayClaims>
<DisplayClaim ClaimTypeReferenceId="signInName" Required="true" />
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
<DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
<DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" Required="true" />
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="executed-SelfAsserted-Input" DefaultValue="true" />
<OutputClaim ClaimTypeReferenceId="newUser" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
</OutputClaims>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonName" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
From the OutputClaim with ClaimTypeReferenceId=email PartnerClaimType="Verified.Email" was removed. Not necessary for getting the display control actually used but for later use of the email claim in the claim bag in a subsequent orchestration step (was not set otherwise).
But when updating the TechnicalProfile for LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress in a similar way:
<TechnicalProfile Id="LocalAccountDiscoveryUsingUserNameAndValidateStrongAuthenticationEmailAddress">
<DisplayName>Reset password using username</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.localaccountpasswordchange1.1</Item>
<Item Key="AllowGenerationOfClaimsWithNullValues">true</Item>
<Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">An account could not be found for the provided user ID.</Item>
<Item Key="UserMessageIfClaimsTransformationBooleanValueIsNotEqual">Your account has been locked. Contact your support person to unlock it, then try again.</Item>
<Item Key="LocalAccountType">Username</Item>
<Item Key="LocalAccountProfile">true</Item>
<!-- Reduce the default self-asserted retry limit of 7 for the reset journey -->
<Item Key="setting.retryLimit">3</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<!-- using this requires removal of OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true", below
according to https://stackoverflow.com/a/73709880/2347078 -->
<!-- line is required for non-customized email journey presently -->
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<!-- <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" /> -->
<OutputClaim ClaimTypeReferenceId="email" Required="true" />
<OutputClaim ClaimTypeReferenceId="emails" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="userPrincipalName" />
<OutputClaim ClaimTypeReferenceId="sub" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
<OutputClaim ClaimTypeReferenceId="strongAuthenticationEmailAddress" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromObjectID" />
</OutputClaimsTransformations>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingUserNameAndValidateStrongAuthenticationEmailAddress" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
In this case, see the comments as to why the OutputClaim with ClaimTypeReferenceId="email" was modified, which is referencing Jas Suri - MSFT below. With the OutputClaim looking like <OutputClaim ClaimTypeReferenceId="email" Required="true" /> the display claim is ignored entirely (the built-in verification control is presented).
With the OutputClaim entirely removed, instead the password reset policy winds up not having any fields:
All starter packs/samples are from current repos, and are using v2 pages supporting display claims.
Remove
<OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="Verified.Email" Required="true" />

How do I make the email address (signInName) read only for the sign-in form

I am creating an Azure AD B2C custom policy for inviting users to user my applications. As part of the invite I set custom user attributes.
In my user journey, if the user exists I send them to a sign in screen with the email pre-populated.
How do I make the email address (signInName) field read only so that the user can't apply the invite to a different account?
This is the technical profile I have for signing in. The signInName is populated by my invite token, but it is not read only.
<TechnicalProfile Id="LocalAccountSigninWithReadOnlyEmail">
<DisplayName>Local Account Signin</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="setting.showSignupLink">false</Item>
<Item Key="SignUpTarget">SignUpWithLogonEmailExchange</Item>
<Item Key="setting.operatingMode">Email</Item>
<Item Key="ContentDefinitionReferenceId">api.selfasserted</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" Required="true" PartnerClaimType="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CopySignInNameToReadOnly" />
</OutputClaimsTransformations>
<ValidationTechnicalProfiles>
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
Create a readonly field.
<ClaimType Id="readOnlyEmail">
<DisplayName>Email Address</DisplayName>
<DataType>string</DataType>
<UserHelpText/>
<UserInputType>Readonly</UserInputType>
</ClaimType>
Copy email via a claims transformation e.g.
<ClaimsTransformation Id="CreateReadonlyEmailClaim" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="mail" TransformationClaimType="inputClaim"/>
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}"/>
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="readonlyEmail" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
Invoke the transformation e.g. <InputClaimsTransformations>
and then use the readonly field in the screen.
I had to face the same problem, I mean read-only email for sign-in custom flow. First of all, you have to create read-only custom claim:
<ClaimType Id="ReadOnlyEmail">
<DisplayName>Verified Email Address</DisplayName>
<DataType>string</DataType>
<UserInputType>Readonly</UserInputType>
</ClaimType>
Next, you have to create two custom claims transformations:
<ClaimsTransformation Id="CopyEmailToReadOnlyEmail" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="ReadOnlyEmail" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
<ClaimsTransformation Id="CopyEmailToSignInName" TransformationMethod="FormatStringClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="inputClaim" />
</InputClaims>
<InputParameters>
<InputParameter Id="stringFormat" DataType="string" Value="{0}" />
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="signInName" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
And finally it is custom claims provider for sign-in with read-only email:
<ClaimsProvider>
<DisplayName>Local Account Sign In</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SelfAsserted-LocalAccountSignin-Password">
<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="ContentDefinitionReferenceId">api.selfasserted</Item>
<Item Key="setting.showCancelButton">false</Item>
</Metadata>
<IncludeInSso>false</IncludeInSso>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyEmailToReadOnlyEmail" />
<InputClaimsTransformation ReferenceId="CopyEmailToSignInName" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="ReadOnlyEmail" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="ReadOnlyEmail" Required="true" />
<OutputClaim ClaimTypeReferenceId="password" Required="true" />
<OutputClaim ClaimTypeReferenceId="objectId" />
<OutputClaim ClaimTypeReferenceId="authenticationSource" />
</OutputClaims>
<ValidationTechnicalProfiles>
<!-- check users password if user existed -->
<ValidationTechnicalProfile ReferenceId="login-NonInteractive" />
</ValidationTechnicalProfiles>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>

ADB2C Custom policy for sending email through sendgrid

Other than optional steps, I have followed all steps mentioned below:
https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-email-sendgrid
I keep getting the following error when clicking through the "Send Veirification code" button -
Basic credentials specified for 'SendOtp' are invalid. Check that the credentials are correct and that access has been granted by the resource.
The sendgrid account is configured correctly and I am able to send email through Postman. The postman request, using the same sendgrid api key I am setting in Azure policy key
{
"personalizations": [
{
"to": [
{
"email": "abc#abc.com",
"name": "abc abc"
}
],
"dynamic_template_data": {
"otp": "123456",
"subject": "account email verification code",
},
}
],
"from": {
"email": "noreply#johndoe.com",
"name": "John Doe"
},
"reply_to": {
"email": "noreply#johndoe.com",
"name": "John Doe"
},
"template_id": "d-xxxxxxxxxxxxxxxxxxxxxxxx"
}
These are the blocks from the above link that I have added
<!--Step 1 SendGrid Email-->
<ClaimType Id="Otp">
<DisplayName>Secondary One-time password</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="emailRequestBody">
<DisplayName>SendGrid request body</DisplayName>
<DataType>string</DataType>
</ClaimType>
<ClaimType Id="VerificationCode">
<DisplayName>Secondary Verification Code</DisplayName>
<DataType>string</DataType>
<UserHelpText>Enter your email verification code</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
<!--Step 1-->
<!--Step 2 SendGrid Email To be worked on -->
<ClaimsTransformation Id="GenerateEmailRequestBody" TransformationMethod="GenerateJson">
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.to.0.email" />
<InputClaim ClaimTypeReferenceId="otp" TransformationClaimType="personalizations.0.dynamic_template_data.otp" />
<InputClaim ClaimTypeReferenceId="email" TransformationClaimType="personalizations.0.dynamic_template_data.email" />
</InputClaims>
<InputParameters>
<!-- Update the template_id value with the ID of your SendGrid template. -->
<InputParameter Id="template_id" DataType="string" Value="d-xxxxxxxxxxxxxxxxxxxxxxxxxx"/>
<InputParameter Id="from.email" DataType="string" Value="my_email#mydomain.com"/>
<!-- Update with a subject line appropriate for your organization. -->
<InputParameter Id="personalizations.0.dynamic_template_data.subject" DataType="string" Value="account email verification code"/>
</InputParameters>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="emailRequestBody" TransformationClaimType="outputClaim"/>
</OutputClaims>
</ClaimsTransformation>
<!--Step 2-->
<!--Step 3 SendGrid Email-->
<ContentDefinition Id="api.localaccountsignup">
<DataUri>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.0</DataUri>
</ContentDefinition>
<ContentDefinition Id="api.localaccountpasswordreset">
<DataUri>urn:com:microsoft:aad:b2c:elements:contract:selfasserted:2.1.0</DataUri>
</ContentDefinition>
<!--Step 3-->
<!--Step 4 SendGrid Email-->
<DisplayControls>
<DisplayControl Id="emailVerificationControl" UserInterfaceControlType="VerificationControl">
<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="SendOtp" />
</ValidationClaimsExchange>
</Action>
<Action Id="VerifyCode">
<ValidationClaimsExchange>
<ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" />
</ValidationClaimsExchange>
</Action>
</Actions>
</DisplayControl>
</DisplayControls>
<!--Step 4-->
<!--Step 5 SendGrid Email-->
<ClaimsProvider>
<DisplayName>One time password technical profiles</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="GenerateOtp">
<DisplayName>Generate one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">GenerateCode</Item>
<Item Key="CodeExpirationInSeconds">1200</Item>
<Item Key="CodeLength">6</Item>
<Item Key="CharacterSet">0-9</Item>
<Item Key="ReuseSameCode">true</Item>
<Item Key="NumRetryAttempts">5</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="otp" PartnerClaimType="otpGenerated" />
</OutputClaims>
</TechnicalProfile>
<TechnicalProfile Id="VerifyOtp">
<DisplayName>Verify one time password</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.OneTimePasswordProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="Operation">VerifyCode</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="email" PartnerClaimType="identifier" />
<InputClaim ClaimTypeReferenceId="verificationCode" PartnerClaimType="otpToVerify" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<!--Step 5-->
<!--Step 6 SendGrid Email-->
<ClaimsProvider>
<DisplayName>RestfulProvider</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="SendOtp">
<DisplayName>Use SendGrid's email API to send the code the the user</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://api.sendgrid.com/v3/mail/send</Item>
<Item Key="AuthenticationType">Bearer</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="ClaimUsedForRequestPayload">emailRequestBody</Item>
</Metadata>
<CryptographicKeys>
<Key Id="BearerAuthenticationToken" StorageReferenceId="B2C_1A_SendGridSecret" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="GenerateEmailRequestBody" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="emailRequestBody" />
</InputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<!--Step 6-->
<!--Step 7 SendGrid Email-->
<ClaimsProvider>
<DisplayName>Local Account</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="LocalAccountSignUpWithLogonEmail">
<Metadata>
<!--OTP validation error messages-->
<Item Key="UserMessageIfSessionDoesNotExist">You have exceeded the maximum time allowed.</Item>
<Item Key="UserMessageIfMaxRetryAttempted">You have exceeded the number of retries allowed.</Item>
<Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
<Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
</Metadata>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
<DisplayClaim ClaimTypeReferenceId="displayName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="givenName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="surName" Required="true" />
<DisplayClaim ClaimTypeReferenceId="newPassword" Required="true" />
<DisplayClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
</DisplayClaims>
</TechnicalProfile>
<TechnicalProfile Id="LocalAccountDiscoveryUsingEmailAddress">
<Metadata>
<!--OTP validation error messages-->
<Item Key="UserMessageIfSessionDoesNotExist">You have exceeded the maximum time allowed.</Item>
<Item Key="UserMessageIfMaxRetryAttempted">You have exceeded the number of retries allowed.</Item>
<Item Key="UserMessageIfInvalidCode">You have entered the wrong code.</Item>
<Item Key="UserMessageIfSessionConflict">Cannot verify the code, please try again later.</Item>
</Metadata>
<DisplayClaims>
<DisplayClaim DisplayControlReferenceId="emailVerificationControl" />
</DisplayClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
<!--Step 7-->
Can any one help me figure how to debug this problem. Many thanks.
Try to replace "my_email#mydomain.com" in the block with SendGrid account>Single Sender Verification> verified "DOMAIN"
Like here
I encountered the same issue. To fix it you have to do the following steps:
Add a Sender: To do so, navigate Sender Authentication page, and click Create New Sender. Next, you have to fill in all of the fields on the page and then click Save.
Verify the sender: You will receive a verification email that you need to confirm.
Update the Custom policy's from.email parameter
<ClaimsTransformations>
<ClaimsTransformation Id="GenerateEmailRequestBody" TransformationMethod="GenerateJson">
...
<InputParameter Id="from.email" DataType="string" Value="XXX#gmail.com"/>
...
</ClaimsTransformation>
</ClaimsTransformations>
Update the Custom policy's template_id parameter: If you are using a dynamic template you must update the template ID.
<ClaimsTransformations>
<ClaimsTransformation Id="GenerateEmailRequestBody" TransformationMethod="GenerateJson">
...
<InputParameter Id="template_id" DataType="string" Value="d-ac01c68f69364014adc44c7857f95d2e"/>
...
</ClaimsTransformation>
</ClaimsTransformations>

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>

Resources