我有一个与授权代码流程配合使用的登录策略和用户旅程。但是,当我添加接受查询字符串值并将其发送到 Azure 函数的编排步骤时,/authorize 之后的 /token 调用将返回带有以下正文的 HTTP 400:
{
"error": "invalid_grant",
"error_description": "AADB2C90085: The service has encountered an internal error. Please reauthenticate and try again."
}
我用两种方式测试这个:
预先修改的策略适用于这两者。
只要所选应用程序不使用授权代码流,修改后的策略在使用身份体验框架门户中提供的“立即运行端点”时就可以正常工作。但是,当使用带有授权代码流的 React SPA 时,/token 会损坏。
注意:以下代码示例使用占位符值
原政策:
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="placeholder.onmicrosoft.com"
PolicyId="b2c_1a_signin"
PublicPolicyUri="http://placeholder.onmicrosoft.com/b2c_1a_signin"
TenantObjectId="my tenant id">
<BasePolicy>
<TenantId>placeholder.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<Endpoints>
<!--points to refresh token journey when app makes refresh token request-->
<Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
</Endpoints>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
原始用户旅程:
<UserJourney Id="SignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when in the token. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
修改后的政策:
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="placeholder.onmicrosoft.com"
PolicyId="b2c_1a_signin"
PublicPolicyUri="http://placeholder.onmicrosoft.com/b2c_1a_signin"
TenantObjectId="my tenant id">
<BasePolicy>
<TenantId>placeholder.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<Endpoints>
<!--points to refresh token journey when app makes refresh token request-->
<Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
</Endpoints>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="qsValue" DefaultValue="{OAUTH-KV:qsValue}" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
修改后的用户旅程:
<UserJourney Id="SignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when in the token. -->
<OrchestrationStep Order="2" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CallMyAzureFunction" TechnicalProfileReferenceId="AzureFunction"></ClaimsExchange>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
调用 Azure 函数的新声明提供程序:
<ClaimsProvider>
<DisplayName>Claims</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AzureFunction">
<DisplayName>Calls an Azure Function to check whether the query string value is valid</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">Azure Function Service URL placeholder</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim Required="true" ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="qsValue" DefaultValue="{OAUTH-KV:qsValue}" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="qsValue" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
查询字符串参数的声明类型:
<ClaimType Id="qsValue">
<DisplayName>Query String Value</DisplayName>
<DataType>string</DataType>
<UserHelpText>A value passed in the query string</UserHelpText>
</ClaimType>
以下是 Application Insights 中对 React SPA 进行的失败令牌调用的跟踪。这是相关 ID 的唯一应用程序洞察。
[
{
"Kind": "Headers",
"Content": {
"UserJourneyRecorderEndpoint": "urn:journeyrecorder:applicationinsights",
"CorrelationId": "3cf51ffc-0093-468e-bda1-cc3022e5883b",
"EventInstance": "Event:TOKEN",
"TenantId": "placeholder.onmicrosoft.com",
"PolicyId": "B2C_1A_signin"
}
},
{
"Kind": "Transition",
"Content": {
"EventName": "TOKEN",
"StateName": "Initial"
}
},
{
"Kind": "Predicate",
"Content": "Web.TPEngine.StateMachineHandlers.IsTokenExchangeValidRequest"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true,
"Statebag": {
"MACHSTATE": {
"c": "2024-04-03T16:14:01.8426102Z",
"k": "MACHSTATE",
"v": "Initial",
"p": true
},
"JC": {
"c": "2024-04-03T16:14:01.8416127Z",
"k": "JC",
"v": "en",
"p": true
},
"ComplexItems": "_MachineEventQ, TCTX"
},
"PredicateResult": "True"
}
},
{
"Kind": "Predicate",
"Content": "Web.TPEngine.StateMachineHandlers.IsTokenExchangeOnBehalfOfFlow"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true,
"PredicateResult": "False"
}
},
{
"Kind": "Predicate",
"Content": "Web.TPEngine.StateMachineHandlers.IsTokenExchangeCustomClientCredentialsFlow"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true,
"PredicateResult": "False"
}
},
{
"Kind": "Predicate",
"Content": "Web.TPEngine.StateMachineHandlers.IsTokenExchangeResourceOwnerFlow"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true,
"PredicateResult": "False"
}
},
{
"Kind": "Predicate",
"Content": "Web.TPEngine.StateMachineHandlers.ShouldTokenExchangeRunAsUserJourneyHandler"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true,
"PredicateResult": "False"
}
},
{
"Kind": "Action",
"Content": "Web.TPEngine.StateMachineHandlers.TokenExchangeHandler"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": false,
"Statebag": {
"Complex-CLMS": {
"tenantId": "tenant id"
},
"CI": {
"c": "2024-04-03T16:14:01.8546106Z",
"k": "CI",
"v": "bf57499d-ca05-4a27-a281-dcd9b32c3e15",
"p": true
},
"CT": {
"c": "2024-04-03T16:14:01.9176155Z",
"k": "CT",
"v": "Spa",
"p": true
},
"ComplexItems": "_MachineEventQ, TCTX, REPRM, AUPRM, PRMCH"
}
}
},
{
"Kind": "Action",
"Content": "Web.TPEngine.StateMachineHandlers.TransactionEndHandler"
},
{
"Kind": "HandlerResult",
"Content": {
"Result": true
}
}
]
阅读这篇博文后我意识到我的错误:https://blog.wojtek.pro/aad-b2c-quick-tips-query-string-parameters/
问题不在于对 Azure 函数的调用,而在于我传递查询字符串参数值的方式。
我添加了一个声明提供程序,它可以获取查询字符串参数,而不是到处依赖 {OAUTH-KV:qsValue}。然后,我添加了一个声明交换编排步骤,该步骤调用所述声明提供程序,并将其作为我的 SignIn 用户旅程中的第一个编排步骤。 qsValue 参数现在从查询字符串中提取,并在整个过程中持续存在,就像我想要的那样。
这是我的最终配置:
查询字符串参数的声明类型:
<ClaimType Id="qsValue">
<DisplayName>Query String Value</DisplayName>
<DataType>string</DataType>
<UserHelpText>A value passed in the query string</UserHelpText>
</ClaimType>
获取查询字符串参数的新声明提供者:
<ClaimsProvider>
<DisplayName>Get query string value</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="GetValueFromQueryString">
<DisplayName>Populate query string value claim with parameter value</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="qsValue" DefaultValue="{OAUTH-KV:qsValue}" AlwaysUseDefaultValue="true"/>
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
调用 Azure 函数的声明提供者:
<ClaimsProvider>
<DisplayName>Claims</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AzureFunction">
<DisplayName>Calls an Azure Function to check whether the query string value is valid</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">Azure Function Service URL placeholder</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim Required="true" ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="qsValue" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="qsValue" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
政策:
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="placeholder.onmicrosoft.com"
PolicyId="b2c_1a_signin"
PublicPolicyUri="http://placeholder.onmicrosoft.com/b2c_1a_signin"
TenantObjectId="my tenant id">
<BasePolicy>
<TenantId>placeholder.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<Endpoints>
<!--points to refresh token journey when app makes refresh token request-->
<Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
</Endpoints>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="qsValue" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
用户旅程:
<UserJourney Id="SignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="GetQueryStringValue" TechnicalProfileReferenceId="GetValueFromQueryString" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when in the token. -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CallMyAzureFunction" TechnicalProfileReferenceId="AzureFunction"></ClaimsExchange>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>
阅读这篇博文后我意识到我的错误:https://blog.wojtek.pro/aad-b2c-quick-tips-query-string-parameters/
问题不在于对 Azure 函数的调用,而在于我传递查询字符串参数值的方式。
我添加了一个声明提供程序,它可以获取查询字符串参数,而不是到处依赖 {OAUTH-KV:qsValue}。然后,我添加了一个声明交换编排步骤,该步骤调用所述声明提供程序,并将其作为我的 SignIn 用户旅程中的第一个编排步骤。 qsValue 参数现在从查询字符串中提取,并按照我想要的方式在整个过程中持续存在。
这是我的最终配置:
查询字符串参数的声明类型:
<ClaimType Id="qsValue">
<DisplayName>Query String Value</DisplayName>
<DataType>string</DataType>
<UserHelpText>A value passed in the query string</UserHelpText>
</ClaimType>
获取查询字符串参数的新声明提供者:
<ClaimsProvider>
<DisplayName>Get query string value</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="GetValueFromQueryString">
<DisplayName>Populate query string value claim with parameter value</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.ClaimsTransformationProtocolProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="IncludeClaimResolvingInClaimsHandling">true</Item>
</Metadata>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="qsValue" DefaultValue="{OAUTH-KV:qsValue}" AlwaysUseDefaultValue="true"/>
</OutputClaims>
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
调用 Azure 函数的声明提供者:
<ClaimsProvider>
<DisplayName>Claims</DisplayName>
<TechnicalProfiles>
<TechnicalProfile Id="AzureFunction">
<DisplayName>Calls an Azure Function to check whether the query string value is valid</DisplayName>
<Protocol Name="Proprietary" Handler="Web.TPEngine.Providers.RestfulProvider, Web.TPEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<Metadata>
<Item Key="ServiceUrl">Azure Function Service URL placeholder</Item>
<Item Key="AuthenticationType">None</Item>
<Item Key="SendClaimsIn">Body</Item>
<Item Key="AllowInsecureAuthInProduction">true</Item>
</Metadata>
<InputClaims>
<InputClaim Required="true" ClaimTypeReferenceId="objectId" />
<InputClaim ClaimTypeReferenceId="qsValue" />
</InputClaims>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="qsValue" />
</OutputClaims>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop" />
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
政策:
<TrustFrameworkPolicy xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.microsoft.com/online/cpim/schemas/2013/06"
PolicySchemaVersion="0.3.0.0"
TenantId="placeholder.onmicrosoft.com"
PolicyId="b2c_1a_signin"
PublicPolicyUri="http://placeholder.onmicrosoft.com/b2c_1a_signin"
TenantObjectId="my tenant id">
<BasePolicy>
<TenantId>placeholder.onmicrosoft.com</TenantId>
<PolicyId>B2C_1A_TrustFrameworkExtensions</PolicyId>
</BasePolicy>
<RelyingParty>
<DefaultUserJourney ReferenceId="SignIn" />
<Endpoints>
<!--points to refresh token journey when app makes refresh token request-->
<Endpoint Id="Token" UserJourneyReferenceId="RedeemRefreshToken" />
</Endpoints>
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
<Protocol Name="OpenIdConnect" />
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" />
<OutputClaim ClaimTypeReferenceId="objectId" PartnerClaimType="sub" />
<OutputClaim ClaimTypeReferenceId="tenantId" AlwaysUseDefaultValue="true" DefaultValue="{Policy:TenantObjectId}" />
<OutputClaim ClaimTypeReferenceId="qsValue" />
</OutputClaims>
<SubjectNamingInfo ClaimType="sub" />
</TechnicalProfile>
</RelyingParty>
</TrustFrameworkPolicy>
用户旅程:
<UserJourney Id="SignIn">
<OrchestrationSteps>
<OrchestrationStep Order="1" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="GetQueryStringValue" TechnicalProfileReferenceId="GetValueFromQueryString" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="2" Type="CombinedSignInAndSignUp" ContentDefinitionReferenceId="api.signuporsignin">
<ClaimsProviderSelections>
<ClaimsProviderSelection ValidationClaimsExchangeId="LocalAccountSigninEmailExchange" />
</ClaimsProviderSelections>
<ClaimsExchanges>
<ClaimsExchange Id="LocalAccountSigninEmailExchange" TechnicalProfileReferenceId="SelfAsserted-LocalAccountSignin-Email" />
</ClaimsExchanges>
</OrchestrationStep>
<!-- This step reads any user attributes that we may not have received when in the token. -->
<OrchestrationStep Order="3" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="AADUserReadWithObjectId" TechnicalProfileReferenceId="AAD-UserReadUsingObjectId" />
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="4" Type="ClaimsExchange">
<ClaimsExchanges>
<ClaimsExchange Id="CallMyAzureFunction" TechnicalProfileReferenceId="AzureFunction"></ClaimsExchange>
</ClaimsExchanges>
</OrchestrationStep>
<OrchestrationStep Order="5" Type="SendClaims" CpimIssuerTechnicalProfileReferenceId="JwtIssuer" />
</OrchestrationSteps>
<ClientDefinition ReferenceId="DefaultWeb" />
</UserJourney>