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

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

For the third one, do an AAD Read operation instead, and it has a built in error message for when the use exists.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-technical-profile#ui-elements
You can find this used in the AAD B2C starter pack for the default sign up flow.
For password matching, use password and reenter password, use the built in way
https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack/blob/master/LocalAccounts/TrustFrameworkBase.xml#L648
Now you should only need the 1 claims transform for email matching.

Related

azure ad b2c Prevent Password reuse

I am trying to send the password as part of output claims but it never comes up. I tried to create a new extension variable and do a tranformation copy claim but that didnt work either.
I would like to hash the password and store it in external system to work around the issue of not repeating password as thats a security requirement.
If I try to add
<OutputClaim ClaimTypeReferenceId="password" />
as part of
<TechnicalProfile Id="login-NonInteractive">
and the whole login screen stops working.
Please let me know if I am missing something and also on how to hash the password to send it to an external RestAPI Technical profile
REST API that I used
<ClaimsProvider>
<DisplayName>REST API</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="REST-API-ValidatePassword">
<DisplayName>Encrypt the password and validate it against previous passwords</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">https://test********</Item>
<!--<Item Key="ServiceUrl">https://your-azfunc-api.azurewebsites.net/api/EncryptClaims</Item>-->
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
<InputClaim ClaimTypeReferenceId="password" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="passEncrypted" PartnerClaimType="passEncrypted"/>
<OutputClaim ClaimTypeReferenceId="isPasswordUsed" PartnerClaimType="isPasswordUsed"/>
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
As per this:
"AAD and AAD B2C do not maintain a password history. Currently therefore you can only prevent the last password from being used.
We don’t recommend using external systems to maintain a password history."
Have a look at this sample.
You could perhaps copy the password to an extension attribute and output that but then you are exposing the password.
A better way is to use a REST API that saves the password so everything is done server-side.
You can try doing this by copying password claim when it is available. You just need to remember that copying password to another claim makes it vulnerable to logging etc. You need to do it with proper care.
To have a copied password value you need another claim:
<ClaimType Id="pwd">
<DisplayName>Password</DisplayName>
<DataType>string</DataType>
<AdminHelpText>password</AdminHelpText>
<UserHelpText>password</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
then, a claim transformation:
<ClaimsTransformation Id="CopyPass" TransformationMethod="CopyClaim">
<InputClaims>
<InputClaim ClaimTypeReferenceId="password" TransformationClaimType="inputClaim" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="pwd" TransformationClaimType="outputClaim" />
</OutputClaims>
</ClaimsTransformation>
and that claim transformation added to a proper technical profile:
<TechnicalProfile Id="eSTS-NonInteractive">
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CopyPass" />
</InputClaimsTransformations>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="pwd" />
</OutputClaims>
</TechnicalProfile>
You should be able to use pwd claim value (containing actual password) for whatever you need then.

Azure B2C Custom Reset Password Policy

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

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

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

In the PhoneFactor-InputOrVerify Technical Profile (B2C custom user flow), is it possible to allow the user to change their default mobile number?

In Azure B2C custom policies, our business process requirements mean we need control over whether:
The user may not replace the mobile number we have on record for them (i.e. first-time password set experience), or
We present the number we hold on record for them by default, but allow the user to overtype and save this number with a new number (e.g. logged-in edit profile experience).
The question: Is there a way I can allow the user to replace the number we hold on record for them with a new number, perhaps by the addition of a new button [Provide New Number] in this series:
[Send Code] [Call Me] [New Button To Provide New Number] [Cancel]
PhoneFactor-InputOrVerify sample screen (image)
The code below is a slight modification of the similarly-named policy from the B2C Custom Policy Starterpack.
Note how in this example we're providing the mobile phone number value stored in the mobile property of the user object as strongAuthenticationNumber. This is because there is currently no way of setting the MFA number programmatically. (Thanks for your help, Chris Padgett.)
Other than this, the policy is as per starterpack.
<ClaimsProvider>
<DisplayName>PhoneFactor</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="PhoneFactor-InputOrVerify">
<DisplayName>PhoneFactor</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.PhoneFactorProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ContentDefinitionReferenceId">api.phonefactor</Item>
<Item Key="ManualPhoneNumberEntryAllowed">true</Item>
</Metadata>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaimsTransformations>
<InputClaimsTransformation ReferenceId="CreateUserIdForMFA" />
</InputClaimsTransformations>
<InputClaims>
<InputClaim ClaimTypeReferenceId="userIdForMFA" PartnerClaimType="UserId" />
<!--InputClaim ClaimTypeReferenceId="strongAuthenticationPhoneNumber" /-->
<InputClaim ClaimTypeReferenceId="mobile" PartnerClaimType="strongAuthenticationPhoneNumber" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="Verified.strongAuthenticationPhoneNumber" PartnerClaimType="Verified.OfficePhone" />
<OutputClaim ClaimTypeReferenceId="newPhoneNumberEntered" PartnerClaimType="newPhoneNumberEntered" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-MFA" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
I suspect there may be some metadata setting to enable a button or text input box as described, but to my knowledge there is no documentation for this yet.
You must create a custom flow for this that:
Prompts the end user to sign in with a local or external account.
Reads the current phone number for the end user from Azure AD.
Prompts the end user to verify their current phone number.
Prompts the end user to enter and verify their new phone number.
Writes the new phone number for the end user to Azure AD.
See this sample policy.

Azure B2C AD: Is the email address mandatory for Local Accounts identity providers based on Username?

Am I missing something? It does not seem to be possible to set it up so that the email address is not required (or not even prompted for at all) during sign-up? You can only disable the email verification.
We may not allow the users to enter an email address at all. They must register with a username only. Surely this must be supported?
I have redone the entire process over from scratch twice now following all the documentation I could find on the topic. But the outcome remains the same. I start by creating a new B2C tenant and I ensure that under Identity Providers, only "Username" is selected as the "Local Accounts". Then I go to "Sign-up or sign-in policies" and create a custom template, then click on edit. Then I ensure that the Identity Providers is set to only the "User ID signup" (and Local Account), and that in the Sign-up Attributes as well as in the Application Claims I do not have the "Email Address" selected. Then I go to "Page UI Customization" and click on "Local account sign-up page". I enter my custom URL. Under the "Sign-up attributes" it lists "Email Address". Email address should not be there AT ALL. When I click on Email Address there is only an option to set "Require verification" to No. The Optional toggle switch is DISABLED. So I can't even make it optional.
The main point here is that when I use "Username" instead of "Email" as the Identity Provider, it should most definitely not force an email address on me.
If you try the above steps in the Azure Portal as it stands today 8 Aug 2018 I am sure you will find the same restriction. This seems like a bug to me, maybe it slipped in somewhere along the line?
Yes it is possible to register the user in B2C without email using custom policies.
You can follow the steps mention here to create custom policies and download the starter kit which has examples of how the policies can be modified to suit your requirement.
Below is the TechnicalProfile which i have used to SignUp user without email being mandatory
<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>
<CryptographicKeys>
<Key Id="issuer_secret" StorageReferenceId="B2C_1A_TokenSigningKeyContainer" />
</CryptographicKeys>
<InputClaims>
<InputClaim ClaimTypeReferenceId="signInName" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="objectId" Required="true" />
<OutputClaim ClaimTypeReferenceId="signInName" Required="true" />
<OutputClaim ClaimTypeReferenceId="email" />
<OutputClaim ClaimTypeReferenceId="newPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="reenterPassword" Required="true" />
<OutputClaim ClaimTypeReferenceId="jobTitle" />
<OutputClaim ClaimTypeReferenceId="postalCode" />
<OutputClaim ClaimTypeReferenceId="city" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surName" />
<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>
The toggle switch that allows you to select between username-based or email-based account is to indicate which of these will be used for sign-in. However, this does not mean that in a username-based account, the email will not be collected - it's just that email cannot be used to sign-in.
Regardless, the email address in basic policies is required to support password reset user journeys even for username-based accounts. If B2C does not collect the email address, then a user cannot reset their password even if they forgot it.
If you really want to not collect email, then your only option today is to use custom policies. In that case, you will have to determine how you want to support password reset, if at all.

Resources