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

标签 azure oauth-2.0 azure-active-directory azure-ad-b2c azure-ad-b2c-custom-policy

我遇到了看似非常常见的场景,但在策略入门包或任何公共(public)存储库和自定义策略示例中都没有解决方案。

我有一个供内部员工和外部客户使用的应用程序。我为此使用 B2C,并使用我们自己的 Azure AD 作为“社交”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 旅程集成 - 尽管我通过策略包中的各种更改禁用了注册。

但是,一旦集成,本地登录就会被破坏。我已经使用了普通 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 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 依赖方部分中设置了“required”属性来指定输出声明的结果。所有这些都可以通过社交 IdP 流程在包中获得,但不能通过本地帐户登录选项获得。

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

为了解决这个问题,我必须修改多个技术配置文件,创建一个独特的声明转换,并将其作为输出声明转换应用到 AAD-UserReadUsingObjectId 基线技术配置文件。

aka.ms/iefsetup

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

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

关于针对 Azure AD IdP 和本地登录问题的 Azure B2C 策略,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69160232/

相关文章:

azure - Nginx 路由忽略路径规则之后的任何内容

windows - 无法通过 SSH 连接到 Azure Windows Server VM

azure - Powershell 函数 -WhatIf 用于没有 -WhatIf 支持的 cmdlet?

java - Coinbase Pro 和沙盒登录端点

r - 如何让 Shiny-server 使用 Azure Active Directory

c# - Azure 计费使用 API 返回 401 未经授权

rest - 如何为NFL Shield API创建访问 token ?

python - 使用 Django OAuth2 工具包生成单一访问 token

使用 Postman 的 Azure Rest API

azure-active-directory - Azure CLI 中的新 AzureADServiceAppRoleAssignment?