Azure B2C注册流程:点击发送验证码时提示邮箱已占用
Azure B2C自定义策略:点击发送验证码前校验邮箱是否已注册
问题背景
现有仅支持注册的Azure B2C自定义策略,包含自定义邮箱Display Control。当前逻辑是用户完成邮箱验证后,才会收到「该邮箱已被占用」的提示,用户资料与密码信息在后续步骤收集,需优化校验时机。
当前实现代码
<TechnicalProfile Id="PartnerSignUpVerifyEmailPage"> <DisplayName>Local Email Verification</DisplayName> <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.SelfAssertedAttributeProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> <Metadata> <Item Key="IpAddressClaimReferenceId">IpAddress</Item> <Item Key="ContentDefinitionReferenceId">api.partners.signUpVerifyEmailPage</Item> <Item Key="IncludeClaimResolvingInClaimsHandling">true</Item> <Item Key="UserMessageIfClaimsTransformationStringsAreNotEqual">A user with this email address already exists.</Item> </Metadata> <InputClaimsTransformations> <InputClaimsTransformation ReferenceId="GetLocalizedStringsForEmail" /> </InputClaimsTransformations> <InputClaims> <InputClaim ClaimTypeReferenceId="email" DefaultValue="{OIDC:LoginHint}" AlwaysUseDefaultValue="true" /> </InputClaims> <DisplayClaims> <DisplayClaim DisplayControlReferenceId="localizedSignUpEmailVerificationControl" /> </DisplayClaims> <OutputClaims> <OutputClaim ClaimTypeReferenceId="email" Required="true" /> </OutputClaims> <ValidationTechnicalProfiles> <!-- this validation asserts the email provided isn't already in use --> <ValidationTechnicalProfile ReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists" /> </ValidationTechnicalProfiles> <IncludeTechnicalProfile ReferenceId="AAD-Common" /> <UseTechnicalProfileForSessionManagement ReferenceId="SM-AAD" /> </TechnicalProfile> <DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl"> <InputClaims> <InputClaim ClaimTypeReferenceId="email" /> </InputClaims> <DisplayClaims> <DisplayClaim ClaimTypeReferenceId="email" Required="true" /> <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" /> </DisplayClaims> <OutputClaims> <OutputClaim ClaimTypeReferenceId="email" /> </OutputClaims> <Actions> <Action Id="SendCode"> <ValidationClaimsExchange> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp"/> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp"/> </ValidationClaimsExchange> </Action> <Action Id="VerifyCode"> <ValidationClaimsExchange> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" /> </ValidationClaimsExchange> </Action> </Actions> </DisplayControl> </DisplayControls> <TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists"> <Metadata> <Item Key="Operation">Read</Item> <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item> </Metadata> <IncludeInSso>false</IncludeInSso> <InputClaims> <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" /> </InputClaims> <OutputClaims> <!-- Required claims --> <OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" /> <OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" /> </OutputClaims> <OutputClaimsTransformations> <!-- ensure that the object id isn't already used --> <OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" /> <!-- blank the object id, in the case that it was used, so we can let the user change the email and retest --> <OutputClaimsTransformation ReferenceId="SetObjectIdToNull" /> </OutputClaimsTransformations> <IncludeTechnicalProfile ReferenceId="AAD-Common" /> </TechnicalProfile>
需求目标
用户点击「Send Verification Code」按钮时,先验证该邮箱未被占用;若邮箱已被占用,则显示错误提示,不发送邮件且不推进控件流程。
尝试过的无效方法
方法1:验证、报错并终止
- 细节:在SendCode的ValidationClaimsExchanges顶部添加
AAD-UserReadUsingEmailAddress-RaiseIfExists的ValidationClaimsExchangeTechnicalProfile,为其他ValidationClaimsExchangeTechnicalProfile添加ContinueOnError="false" - 结果:邮箱已占用时会显示错误,但控件仍提示发送验证码,且其他验证步骤仍执行并发送邮件。
方法2:评估新Claim并使用前置条件
- 细节:创建布尔类型Claim
emailAlreadyRegistered,在SendCode的ValidationClaimsExchanges顶部添加AAD-UserReadUsingEmailAddress-RaiseIfExists的ValidationClaimsExchangeTechnicalProfile,在该技术配置文件中添加CompareClaims转换以在邮箱匹配时设置emailAlreadyRegistered=True,为其他ValidationClaimsExchangeTechnicalProfile添加基于ClaimEquals的前置条件判断emailAlreadyRegistered是否为True - 结果:邮箱已占用时会显示错误,但控件仍提示发送验证码,且其他验证步骤仍执行并发送邮件。
方法3:嵌套验证步骤
- 细节:创建布尔类型Claim
emailAlreadyRegistered,新建Claims Transformation Technical Profile,该配置文件以邮箱为输入并通过转换设置emailAlreadyRegistered,同时包含AAD-UserReadUsingEmailAddress-RaiseIfExists、GenerateOtp、SendLocalizedOtp三个验证技术配置文件;在SendCode的ValidationClaimsExchanges中替换为该新配置文件 - 结果:邮箱已占用时不显示错误,控件直接进入验证码验证步骤,且未发送邮件。
解决方案(致谢Christian Le Breton)
核心思路
AAD Technical Profile无法设置新Claim,而Claims Technical Profile无法读取AAD数据,因此需要将原有的邮箱校验逻辑拆分为两个独立的技术配置文件,分别完成AAD数据读取和Claim状态设置,再通过前置条件控制后续验证码生成与发送步骤的执行。
实现步骤
- 创建布尔类型Claim
emailAlreadyRegistered - 将
AAD-UserReadUsingEmailAddress-RaiseIfExists拆分为两个独立的Technical Profile:- 第一个保留AAD读取与错误断言逻辑,但移除Claim清理和状态设置的输出转换
- 第二个为
ClaimsTransformationProtocolProvider类型的技术配置文件,包含原有的Claim清理转换,并新增CompareClaims转换以设置emailAlreadyRegistered,同时输出默认值为"false"的emailAlreadyRegisteredClaim
- 在SendCode的ValidationClaimsExchanges顶部添加这两个新的Technical Profile
- 为
GenerateOtp和SendLocalizedOtp添加基于ClaimEquals的前置条件,当emailAlreadyRegistered为true时跳过当前步骤
最终关键代码
<DisplayControl Id="localizedSignUpEmailVerificationControl" UserInterfaceControlType="VerificationControl"> <InputClaims> <InputClaim ClaimTypeReferenceId="email" /> </InputClaims> <DisplayClaims> <DisplayClaim ClaimTypeReferenceId="email" Required="true" /> <DisplayClaim ClaimTypeReferenceId="verificationCode" ControlClaimType="VerificationCode" Required="true" /> </DisplayClaims> <OutputClaims> <OutputClaim ClaimTypeReferenceId="email" /> </OutputClaims> <Actions> <Action Id="SendCode"> <ValidationClaimsExchange> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt1" ContinueOnError="false" /> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2" ContinueOnError="false" /> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="GenerateOtp" ContinueOnError="false" > <Preconditions> <Precondition Type="ClaimEquals" ExecuteActionsIf="true"> <Value>emailAlreadyRegistered</Value> <Value>true</Value> <Action>SkipThisValidationTechnicalProfile</Action> </Precondition> </Preconditions> </ValidationClaimsExchangeTechnicalProfile> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="SendLocalizedOtp" ContinueOnError="false"> <Preconditions> <Precondition Type="ClaimEquals" ExecuteActionsIf="true"> <Value>emailAlreadyRegistered</Value> <Value>true</Value> <Action>SkipThisValidationTechnicalProfile</Action> </Precondition> </Preconditions> </ValidationClaimsExchangeTechnicalProfile> </ValidationClaimsExchange> </Action> <Action Id="VerifyCode"> <ValidationClaimsExchange> <ValidationClaimsExchangeTechnicalProfile TechnicalProfileReferenceId="VerifyOtp" /> </ValidationClaimsExchange> </Action> </Actions> </DisplayControl> <TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt1"> <Metadata> <Item Key="Operation">Read</Item> <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item> </Metadata> <IncludeInSso>false</IncludeInSso> <InputClaims> <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="signInNames.emailAddress" Required="true" /> </InputClaims> <OutputClaims> <!-- Required claims --> <OutputClaim ClaimTypeReferenceId="objectId" DefaultValue="NOTFOUND" /> <OutputClaim ClaimTypeReferenceId="objectIdNotFound" DefaultValue="NOTFOUND" AlwaysUseDefaultValue="true" /> </OutputClaims> <OutputClaimsTransformations> <!-- ensure that the object id isn't already used --> <OutputClaimsTransformation ReferenceId="AssertObjectIdObjectIdNotFoundAreEqual" /> </OutputClaimsTransformations> <IncludeTechnicalProfile ReferenceId="AAD-Common" /> </TechnicalProfile> <TechnicalProfile Id="AAD-UserReadUsingEmailAddress-RaiseIfExists-Pt2"> <DisplayName>Check If Email Is Registered</DisplayName> <Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> <Metadata> <Item Key="RaiseErrorIfClaimsPrincipalDoesNotExist">false</Item> </Metadata> <IncludeInSso>false</IncludeInSso> <InputClaims> <InputClaim ClaimTypeReferenceId="objectId" /> <InputClaim ClaimTypeReferenceId="objectIdNotFound" /> </InputClaims> <OutputClaims> <OutputClaim ClaimTypeReferenceId="emailAlreadyRegistered" /> <OutputClaim ClaimTypeReferenceId="objectId" /> </OutputClaims> <OutputClaimsTransformations> <!-- ensure that the object id isn't already used --> <OutputClaimsTransformation ReferenceId="SetEmailAlreadyRegistered" /> <!-- blank the object id, in the case that it was used, so we can let the user change the email and retest --> <OutputClaimsTransformation ReferenceId="SetObjectIdToNull" /> </OutputClaimsTransformations> </TechnicalProfile>
内容的提问来源于stack exchange,提问作者fei0x
相关产品推荐
相关产品推荐

