电子邮件邀请注册后 AD B2C 登录失败

AD B2C Sign in fails after email invitation sign up

我遵循了 SignUp with email invitation 教程,除了一个例外,它工作正常。 用户完成注册过程并被重定向后,应用程序无法验证用户并显示错误。

查看来自 OpenIdConnectEventsOnMessageReceived 事件表明 MessageReceivedContext.ProtocolMessage.IdToken 为空。

但是,用户被重定向到 /MicrosoftIdentity/Account/Errorid_token 似乎有效。

OnRemoteFailure 事件中,我可以捕捉到以下错误:

{
    "ClassName": "System.Exception",
    "Message": "OpenIdConnectAuthenticationHandler: message.State is null or empty.",
    "Data": null,
    "InnerException": null,
    "HelpURL": null,
    "StackTraceString": null,
    "RemoteStackTraceString": null,
    "RemoteStackIndex": 0,
    "ExceptionMethod": null,
    "HResult": -2146233088,
    "Source": null,
    "WatsonBuckets": null
}

顺便说一句,新用户已正确添加到AD B2C,之后可以登录。只是电子邮件 link 发起的注册过程未能通过身份验证。

这是完整的 SignUpInvitation.xml 自定义策略:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<TrustFrameworkPolicy
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
  PolicySchemaVersion="0.3.0.0"
  TenantId="mytenant.onmicrosoft.com"
  PolicyId="B2C_1A_signup_invitation"
  PublicPolicyUri="http://mytenant.onmicrosoft.com/B2C_1A_signup_invitation"
  DeploymentMode="Development"
  UserJourneyRecorderEndpoint="urn:journeyrecorder:applicationinsights">

  <BasePolicy>
    <TenantId>mytenant.onmicrosoft.com</TenantId>
    <PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
  </BasePolicy>

  <BuildingBlocks>
    <ClaimsSchema>
      <!-- Sample: Read only email address to present to the user-->
      <ClaimType Id="ReadOnlyEmail">
        <DisplayName>Verified Email Address</DisplayName>
        <DataType>string</DataType>
        <UserInputType>Readonly</UserInputType>
      </ClaimType>
    
      <!--Sample: Stores the error message for unsolicited request (a request without id_token_hint) and user not found-->
      <ClaimType Id="errorMessage">
          <DisplayName>Error</DisplayName>
          <DataType>string</DataType>
        <UserHelpText>Add help text here</UserHelpText>
          <UserInputType>Paragraph</UserInputType>
      </ClaimType> 
    </ClaimsSchema>

    <ClaimsTransformations>
      <!--Sample: Initiates the errorMessage claims type with the error message-->
      <ClaimsTransformation Id="CreateUnsolicitedErrorMessage" TransformationMethod="CreateStringClaim">
        <InputParameters>
          <InputParameter Id="value" DataType="string" Value="You cannot sign-up without invitation" />
        </InputParameters>
        <OutputClaims>
          <OutputClaim ClaimTypeReferenceId="errorMessage" TransformationClaimType="createdClaim" />
        </OutputClaims>
      </ClaimsTransformation>

      <!--Sample: Copy the email to ReadOnlyEmail claim type-->
      <ClaimsTransformation Id="CopyEmailAddress" 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>
    </ClaimsTransformations>
  </BuildingBlocks>  

  <ClaimsProviders>
    <ClaimsProvider>
      <DisplayName>Local Account</DisplayName>
      <TechnicalProfiles>
        <!--Sample: Sign-up self-asserted technical profile-->
        <TechnicalProfile Id="LocalAccountSignUpWithReadOnlyEmail">
          <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>
            <!-- Sample: Remove sign-up email verification -->
            <Item Key="EnforceEmailVerification">False</Item>
          </Metadata>
          <InputClaimsTransformations>
            <!--Sample: Copy the email to ReadOnlyEamil claim type-->
            <InputClaimsTransformation ReferenceId="CopyEmailAddress" />
          </InputClaimsTransformations>
          <InputClaims>
            <!--Sample: Set input the ReadOnlyEmail claim type to prefilled the email address-->
            <InputClaim ClaimTypeReferenceId="ReadOnlyEmail" />
            <InputClaim ClaimTypeReferenceId="displayName" />
            <InputClaim ClaimTypeReferenceId="givenName" />
            <InputClaim ClaimTypeReferenceId="surName" />
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="objectId" />
            <!-- Sample: Display the ReadOnlyEmail claim type (instead of email claim type)-->
            <OutputClaim ClaimTypeReferenceId="ReadOnlyEmail" 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" />
          </OutputClaims>
          <ValidationTechnicalProfiles>
            <ValidationTechnicalProfile ReferenceId="AAD-UserWriteUsingLogonEmail" />
          </ValidationTechnicalProfiles>
          <!-- Sample: Disable session management for sign-up page -->
          <UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
        </TechnicalProfile>      
      </TechnicalProfiles>
    </ClaimsProvider>

    <ClaimsProvider>
      <DisplayName>Self Asserted</DisplayName>
      <TechnicalProfiles>
        <!-- Demo: Show error message-->
        <TechnicalProfile Id="SelfAsserted-Unsolicited">
          <DisplayName>Unsolicited error message</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>
            <!-- Sample: Remove the continue button-->
            <Item Key="setting.showContinueButton">false</Item>
         </Metadata>
          <InputClaimsTransformations>
            <InputClaimsTransformation ReferenceId="CreateUnsolicitedErrorMessage" />
          </InputClaimsTransformations>         
          <InputClaims>
            <InputClaim ClaimTypeReferenceId="errorMessage"/>
          </InputClaims>
          <OutputClaims>
            <OutputClaim ClaimTypeReferenceId="errorMessage"/>
          </OutputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>

    <!--Sample: This technical profile specifies how B2C should validate your token, and what claims you want B2C to extract from the token. 
      The METADATA value in the TechnicalProfile meta-data is required. 
      The “IdTokenAudience” and “issuer” arguments are optional (see later section)-->
    <ClaimsProvider>
      <DisplayName>My ID Token Hint ClaimsProvider</DisplayName>
      <TechnicalProfiles>
        <TechnicalProfile Id="IdTokenHint_ExtractClaims">
          <DisplayName>My ID Token Hint TechnicalProfile</DisplayName>
          <Protocol Name="None" />
          <Metadata>
          
            <!--Sample action required: replace with your endpoint location -->
            <Item Key="METADATA">https://mytenant.b2clogin.com/mytenant.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1A_SIGNUP_INVITATION</Item>

            <!-- <Item Key="IdTokenAudience">your_optional_audience_override</Item> -->
            <Item Key="issuer">https://localhost:44316/</Item>
          </Metadata>
          <OutputClaims>
            <!--Sample: Read the email claim from the id_token_hint-->
            <OutputClaim ClaimTypeReferenceId="email" />  
          </OutputClaims>
        </TechnicalProfile>
      </TechnicalProfiles>
    </ClaimsProvider>
  </ClaimsProviders>

  <UserJourneys>
    <UserJourney Id="SignUpInvitation">
        <OrchestrationSteps>

        <!--Sample: Read the input claims from the id_token_hint-->
        <OrchestrationStep Order="1" Type="GetClaims" CpimIssuerTechnicalProfileReferenceId="IdTokenHint_ExtractClaims" />

        <!-- Sample: Check if user tries to run the policy without invitation -->
        <OrchestrationStep Order="2" Type="ClaimsExchange">
            <Preconditions>
                <Precondition Type="ClaimsExist" ExecuteActionsIf="true">
                    <Value>email</Value>
                    <Action>SkipThisOrchestrationStep</Action>
                </Precondition>
            </Preconditions>        
            <ClaimsExchanges>
                <ClaimsExchange Id="SelfAsserted-Unsolicited" TechnicalProfileReferenceId="SelfAsserted-Unsolicited"/>
            </ClaimsExchanges>
        </OrchestrationStep>

        <!-- Sample: Self-asserted sign-up page -->
        <OrchestrationStep Order="3" Type="ClaimsExchange">
            <ClaimsExchanges>
                <ClaimsExchange Id="LocalAccountSignUpWithReadOnlyEmail" TechnicalProfileReferenceId="LocalAccountSignUpWithReadOnlyEmail"/>
            </ClaimsExchanges>
        </OrchestrationStep>

        <!--Sample: Issue an access token-->
        <OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer"/>

        </OrchestrationSteps>
        <ClientDefinition ReferenceId="DefaultWeb"/>
    </UserJourney>
  </UserJourneys>

  <RelyingParty>
    <DefaultUserJourney ReferenceId="SignUpInvitation" />
    <UserJourneyBehaviors>
        <JourneyInsights TelemetryEngine="ApplicationInsights" InstrumentationKey="64729310-c74e-45ab-a59e-f35f7ada76ee" DeveloperMode="true" ClientEnabled="false" ServerEnabled="true" TelemetryVersion="1.0.0" />
    </UserJourneyBehaviors>
    <TechnicalProfile Id="PolicyProfile">
      <DisplayName>PolicyProfile</DisplayName>
      <Protocol Name="OpenIdConnect" />

        <!--Sample: Set the input claims to be read from the id_token_hint-->
      <InputClaims>
        <InputClaim ClaimTypeReferenceId="email" PartnerClaimType="email" />
        <InputClaim ClaimTypeReferenceId="surname" PartnerClaimType="surname" />
        <InputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="givenName" />
        <InputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName"/>
      </InputClaims>
      
      <OutputClaims>
        <OutputClaim ClaimTypeReferenceId="displayName" />
        <OutputClaim ClaimTypeReferenceId="givenName" />
        <OutputClaim ClaimTypeReferenceId="surname" />
        <OutputClaim ClaimTypeReferenceId="email" />
        <OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub"/>
        <OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
      </OutputClaims>
      <SubjectNamingInfo ClaimType="sub" />
    </TechnicalProfile>
  </RelyingParty>

</TrustFrameworkPolicy>

编辑

如前所述,邀请 link 应重定向到我的网络应用程序,因此我添加了以下路由来处理 id_token_hint.

[HttpGet("/redeem/{scheme?}")]
public IActionResult Redeem([FromRoute] string scheme, [Bind(Prefix = "id_token_hint")] string idTokenHint, string policy)
{
    scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
    string redirectUrl = Url.Content("~/");

    AuthenticationProperties properties = new AuthenticationProperties
    {
        RedirectUri = redirectUrl,
        Items =
        {
            ["id_token_hint"] = idTokenHint,
            ["policy"] = policy
        }
    };

    return Challenge(properties, scheme);
}

StartUp.cs 中,我必须添加以下块才能使其正常工作。

services.AddMicrosoftIdentityWebAppAuthentication(Configuration, Constants.AzureAdB2C);

services.Configure<MicrosoftIdentityOptions>(options =>
{
    options.Events ??= new OpenIdConnectEvents();

    options.Events.OnRedirectToIdentityProvider += async ctx =>
    {
        // Append token hint when present (ie. email invitation)
        if (ctx.Properties.Items.ContainsKey("id_token_hint"))
            ctx.ProtocolMessage.IdTokenHint = ctx.Properties.Items["id_token_hint"];

        await Task.CompletedTask.ConfigureAwait(false);
    };
});

在您的生产应用程序中使用它

当身份验证流程从您的应用程序开始时,身份验证库会创建一个 state。此示例为 Azure AD B2C 策略创建原始 link,也称为“运行 Now”link。这种类型的 link 不适合您的生产应用程序实例,只能用于测试示例。

对于生产场景,包含 id_token_hint 的 link 应该指向您的应用程序 https://myapp.com/redeem?hint=<id_token_hint value>。应用程序应该有一个有效的路由来处理包含 id_token_hint 的查询参数。然后,应用程序应使用身份验证库启动对 AAD B2C 策略 ID 的身份验证,该 id_token_hint 将在该 ID 上使用。该库将包含一种将查询参数添加到身份验证请求的方法。请参阅用于实现此功能的库的文档。

身份验证库随后将构建最终身份验证 link,并将 id_token_hint 作为查询参数的一部分附加。这现在将是一个有效的身份验证请求,您的用户将从您的应用程序重定向到 Azure AD B2C 策略。您的应用程序将能够正确处理来自 Azure AD B2C 的响应。

对于单页应用程序,请参阅文档 here。 对于 .Net 应用程序,请参阅文档 here