针对 Azure AD IdP 和本地登录问题的 Azure B2C 策略

Azure B2C Policy for Azure AD IdP and Local Logon Problem

我遇到了一个看起来很常见的情况,但在策略入门包或任何 public 存储库和自定义策略示例中都没有解决方案。

我有一个供内部员工和外部客户使用的应用程序。为此,我使用 B2C 和我们自己的 Azure AD 作为 'social' IdP,并为外部用户提供本地登录。

由于多种原因,Azure 门户的内置功能不符合要求。外部用户账号在B2C目录手动创建,禁止注册。因此,SignUpSignSignIn 是不可行的。我想要达到的体验是:

If LocalLogon Then
    Authenticate with Azure B2C Directory
    Redirect to Application
Else
    If AADSocialIdP Selected
        Authenticate with Azure AD
        If User Exists in B2C Then
            Redirect to Application
        Else
            Create User in B2C using claims received (do not prompt for email verification)
            Redirect to Application

我已使用自定义策略,使用入门包中的 SocialAndLocalAccounts 作为基准,并显着修改了 UserJourney 以便实现使用 AAD 的单点登录,不会提示用户输入他们的名字、姓氏、电子邮件地址,然后验证他们的电子邮件地址(与内置功能一样)。并且,用户被正确地重定向到应用程序。但是,通过创建此 AAD TechnicalProfile 并将其与 SignUpSignIn 旅程集成 - 尽管我通过策略包中的各种更改禁用了注册。

但是,一旦集成,本地登录就坏了。我已经使用了 vanilla LocalAccounts 策略包并确认它可以正常工作并按预期重定向到具有声明的应用程序,但是一旦我添加了我的 AAD TechnicalProfile 和 ClaimsExchange 然后在使用本地登录时我得到的是 用户名或密码不正确.

我认为这是我编写的 UserJourney 的一个问题,但目前我不知道如何为本地登录调用不同的社交旅程。我认为我的 TechnicalProfile 正在覆盖导致此错误的旅程中的声明。

我的 AAD 技术简介是:

        <TechnicalProfile Id="AAD-GB-OpenIdConnect">
          <DisplayName>XXXXXXXXXXXXX</DisplayName>
          <Description>XXXXXXXXXXXXX</Description>
          <Protocol Name="OpenIdConnect"/>
          <OutputTokenFormat>JWT</OutputTokenFormat>
          <Metadata>
            <Item Key="METADATA">https://login.microsoftonline.com/XXXXXXXXXXXXX/v2.0/.well-known/openid-configuration</Item>
            <Item Key="client_id">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</Item>
            <Item Key="response_types">code</Item>
            <Item Key="scope">openid profile</Item>
            <Item Key="response_mode">form_post</Item>
            <Item Key="HttpBinding">POST</Item>
            <Item Key="UsePolicyInRedirectUri">false</Item>
            <Item Key="Prompt">true</Item>
          </Metadata>
          <CryptographicKeys>
            <Key Id="client_secret" StorageReferenceId="B2C_1A_PortalAADSecret"/>
          </CryptographicKeys>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="issuerUserId" PartnerClaimType="oid"/>
            <OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="given_name" />
            <OutputClaim ClaimTypeReferenceId="surName" PartnerClaimType="family_name" />
            <OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
            <OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="socialIdpAuthentication" AlwaysUseDefaultValue="true" />
            <OutputClaim ClaimTypeReferenceId="identityProvider" PartnerClaimType="iss" />
            <OutputClaim ClaimTypeReferenceId="identityProviderAccessToken" PartnerClaimType="{oauth2:access_token}" />
            <OutputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
          </OutputClaims>
          <OutputClaimsTransformations>
            <OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
            <OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
            <OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
            <OutputClaimsTransformation ReferenceId="CreateSubjectClaimFromAlternativeSecurityId"/>
            <OutputClaimsTransformation ReferenceId="CreateOtherMailsFromEmail"/>
          </OutputClaimsTransformations>
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-SocialLogin"/>
        </TechnicalProfile>

在这里值得一提的是,我创建了一个 CreateOtherMailsFromEmail OutputClaimsTransformation,它基本上创建了一个电子邮件输出声明,因为该应用程序旨在获取电子邮件数组的第一个元素,而不是单个元素电子邮件地址。

我的UserJourney如下:

    <UserJourney Id="CustomSignIn">
      <OrchestrationSteps>
        
          <OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
            <ClaimsProviderSelections>
              <ClaimsProviderSelection TargetClaimsExchangeId="AzureADXXXXXXXXExchange" />
              <ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
            </ClaimsProviderSelections>
            <ClaimsExchanges>
              <ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
            </ClaimsExchanges>
          </OrchestrationStep>

          <!-- Check if the user has selected to sign in using one of the social providers -->
          <OrchestrationStep Order="2" Type="ClaimsExchange">
            <Preconditions>
              <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                <Value>objectId</Value>
                <Action>SkipThisOrchestrationStep</Action>
              </Precondition>
            </Preconditions>
            <ClaimsExchanges>
              <ClaimsExchange Id="AzureADXXXXXXXXExchange" TechnicalProfileReferenceId="AAD-GB-OpenIdConnect" />
              <ClaimsExchange Id="SignUpWithLogonEmailExchange" TechnicalProfileReferenceId="LocalAccountSignUpWithLogonEmail" />
            </ClaimsExchanges>
          </OrchestrationStep>

          <!-- For social IDP authentication, attempt to find the user account in the directory. -->
          <OrchestrationStep Order="3" Type="ClaimsExchange">
            <Preconditions>
              <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
                <Value>authenticationSource</Value>
                <Value>localAccountAuthentication</Value>
                <Action>SkipThisOrchestrationStep</Action>
              </Precondition>
            </Preconditions>
            <ClaimsExchanges>
              <ClaimsExchange Id="AADUserReadUsingAlternativeSecurityId" TechnicalProfileReferenceId="AAD-UserReadUsingAlternativeSecurityId-NoError" />
            </ClaimsExchanges>
          </OrchestrationStep>

          <!-- Show self-asserted page only if the directory does not have the user account already (i.e. we do not have an objectId). 
            This can only happen when authentication happened using a social IDP. If local account was created or authentication done
            using ESTS in step 2, then an user account must exist in the directory by this time. -->
          <OrchestrationStep Order="4" Type="ClaimsExchange">
            <Preconditions>
              <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                <Value>objectId</Value>
                <Action>SkipThisOrchestrationStep</Action>
              </Precondition>
            </Preconditions>
            <ClaimsExchanges>
              <ClaimsExchange Id="SelfAsserted-Social" TechnicalProfileReferenceId="SelfAsserted-Social" />
            </ClaimsExchanges>
          </OrchestrationStep>

          <!-- This step reads any user attributes that we may not have received when authenticating using ESTS so they can be sent 
            in the token. -->
          <OrchestrationStep Order="5" Type="ClaimsExchange">
            <Preconditions>
              <Precondition Type="ClaimEquals" ExecuteActionsIf="true">
                <Value>authenticationSource</Value>
                <Value>socialIdpAuthentication</Value>
                <Action>SkipThisOrchestrationStep</Action>
              </Precondition>
            </Preconditions>
            <ClaimsExchanges>
              <ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
            </ClaimsExchanges>
          </OrchestrationStep>
          <!-- The previous step (SelfAsserted-Social) could have been skipped if there were no attributes to collect 
              from the user. So, in that case, create the user in the directory if one does not already exist 
              (verified using objectId which would be set from the last step if account was created in the directory. -->
          <OrchestrationStep Order="6" Type="ClaimsExchange">
            <Preconditions>
              <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                <Value>objectId</Value>
                <Action>SkipThisOrchestrationStep</Action>
              </Precondition>
            </Preconditions>
            <ClaimsExchanges>
              <ClaimsExchange Id="AADUserWrite" TechnicalProfileReferenceId="AAD-UserWriteUsingAlternativeSecurityId" />
            </ClaimsExchanges>
          </OrchestrationStep>
  
          <OrchestrationStep Order="7" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
  
        </OrchestrationSteps>
      <ClientDefinition ReferenceId="DefaultWeb" />
    </UserJourney>

我已启用 App Insights 调试并正在通过 VSCode 检查日志,但没有发现任何有用的信息。

如何调整此旅程以支持两种登录方式?

此问题是由于在统一的 SignInSignUp 依赖方部分中使用“必需”属性集指定输出声明造成的。所有这些都可以通过社交 IdP 流程获得,但不能通过本地帐户登录选项获得。

在指定“必需”输出声明时,您必须确保每个可能的用户旅程都将遵循检索或添加这些声明的技术配置文件链。在我的具体案例中,应用程序开发人员要求的声明规范包括一些无法通过入门包中的基线技术配置文件提供的内容。

为了解决这个问题,我不得不修改几个技术配置文件,创建一个唯一的声明转换并将其作为输出声明转换应用于 AAD-UserReadUsingObjectId 基线技术配置文件。

aka.ms/iefsetup

这是一个非常有用的工具,它将有助于创建社交 IdP 和本地登录自定义 B2C 配置(信用@Jas Suri -微软公司)。它提供了一种完全自动化的方法来自定义和部署所有必要的配置,以配置 B2C 租户以使用身份体验框架。

就我而言,这并不是我的全部解决方案,但它帮助我重新审视了框架的确切运行方式并最终实现了所需的解决方案。