如何在 Azure AD B2C 中存储来自 IdentityServer 3 的声明或仅将其包含在 AAD B2C 颁发的令牌中
How to store claims from IdentityServer 3 in Azure AD B2C or just include it in tokens issued by AAD B2C
我想知道是否可以将 oid 声明或基本上由 Identity Server 3 发布的任何其他声明传播到 AAD B2C 并使其成为 Azure AD B2C 发布的令牌的一部分?我们需要在客户端有一个原始 ID,我们可以从 sub 和 oid 声明中获得的是 AAD B2C 用户帐户的 ID。我也想对角色声明做同样的事情。任何帮助将不胜感激。
[编辑]
这是我的身份服务器配置的技术配置文件在自定义策略 (TrustFrameworkExtensions.xml) 中的样子:
<TechnicalProfile Id="IdentityServerProfile">
<DisplayName>IdentityServer</DisplayName>
<Description>Login with your IdentityServer account</Description>
<Protocol Name="OpenIdConnect"/>
<OutputTokenFormat>JWT</OutputTokenFormat>
<Metadata>
<Item Key="METADATA">https://{identity_server_hostname}/identity/.well-known/openid-configuration</Item>
<Item Key="ProviderName">https://{identity_server_hostname}/identity</Item>
<Item Key="client_id">00000000-0000-0000-0000-000000000000</Item>
<Item Key="IdTokenAudience">00000000-0000-0000-0000-000000000000</Item>
<Item Key="response_types">code</Item>
<Item Key="scope">openid profile customScope</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="AccessTokenResponseFormat">json</Item>
<Item Key="HttpBinding">POST</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_IdentityServerAppSecret"/>
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="IdentityServer" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="tid" />
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="sub" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>
这是 Application Instights 的原始日志,来自具有给定 correlationId 的日志:
{
"TenantId":"{租户}.onmicrosoft.com",
"PolicyId":"B2C_1A_signup_signin_capcom",
"RedirectUri":"http://localhost:3000/",
"AdditionalParameters":{
"client_info":"1",
"x-client-SKU":"MSAL.JS",
"x-client-Ver":"0.1.5",
"client-request-id":"a0ef9d95-ba21-4796-ad65-bc9e2b8c5e89"
},
"Nonce":"bf198ee6-e084-433d-885c-2bd8f0292b3d",
"State":"7f732141-498e-4445-9efe-9b0d54178ef6",
"ClientId":"012c0f85-4568-483f-9bca-054d6201ac00",
"ResponseType":"id_token",
"ResponseMode":"fragment",
"ResponseRedirector":{
"URI":"http://localhost:3000/",
"D":错误,
"WF":是的,
"R":错误,
"S":假
},
"AppModelVersion":1,
"ScopedProviders":[
]
}
我注意到并且发现奇怪的是 ResponseType 值。在我们的 TechnicalProfile 中设置为代码,这里它的值为 id_token.
[已编辑 2]
对 socialIdpUserId 的声明终于通过了。下面还有一个问题是什么的答案。现在,我仍然对 firstName、lastName 和 displayName 有疑问。我遵循了与 socialIdpUserId 相同的模式,但我唯一能看到的是默认值。
这是我对它们的配置:
1) 声明类型定义:
<ClaimType Id="displayName">
<DisplayName>Display Name</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="unique_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="displayName" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your display name.</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
...
<ClaimType Id="givenName">
<DisplayName>Given Name</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="given_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="given_name" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your given name (also known as first name).</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
<ClaimType Id="surname">
<DisplayName>Surname</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="family_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="family_name" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your surname (also known as family name or last name).</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
并在我的 IdP 的技术资料中输出声明:
<TechnicalProfiles>
<TechnicalProfile Id="CapcomProfile">
...
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="firstName" DefaultValue="No" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="lastName" DefaultValue="Name" />
...
最后,RP 输出定义:
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="surname" DefaultValue="Not found in IdP" />
...
有什么建议吗?
为此,您必须从内置策略转移到 a custom policy,它将来自身份提供者的传入声明通过传出声明传递给应用程序。
例如,如果您从 the starter pack 创建自定义策略,那么您可以将用户标识符从 Facebook 传递到应用程序,如下所示:
1) 确保已声明 socialIdpUserId 声明:
<ClaimType Id="socialIdpUserId">
<DisplayName>Username</DisplayName>
<DataType>string</DataType>
</ClaimType>
2) 确保 socialIdpUserId 声明已添加到 Facebook 技术资料的 <OutputClaims />
集合中:
<ClaimsProvider>
<Domain>facebook.com</Domain>
...
<TechnicalProfiles>
<TechnicalProfile Id="Facebook-OAUTH">
<DisplayName>Facebook</DisplayName>
...
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="id" />
...
</OutputClaims>
...
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
3) 确保 socialIdpUserId 声明已添加到依赖方技术资料的 <OutputClaims />
集合中:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="ext_id" />
</OutputClaims>
...
</TechnicalProfile>
</RelyingParty>
对于其他外部声明,您可以遵循相同的模式。
最后,我完成了这项工作,现在我的大部分索赔都通过了。最有可能的是,问题是由于我的密钥策略中缺少来自 Azure AD 应用程序的适当客户端机密,B2C_1A_CapcomIdentityServerAppSecret 更具体。因此,如果您已将适当的密钥从 AAD 应用程序密钥复制到您的策略密钥,请仔细检查它。
不幸的是,这只是解决方案的一部分,因为它仍然不能完全工作,而且我没有在客户端收到返回的令牌。但好消息是,在我配置好这些密钥之后,我开始在 Application Insights 中收到非常有用的异常(您可以在此处查看如何将自定义策略与 Application Insights 连接:https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-troubleshoot-custom)。我强烈建议任何对自定义策略有任何疑问的人进行设置。此外,如果您像我一样使用 IdentityServer3,请尝试在其一侧临时打开 http 日志记录。这是示例:
LoggingOptions = new LoggingOptions
{
EnableHttpLogging = true
}
当我在 Application Insights 中分析跟踪时,大多数异常的发生是因为我错过了一些属性,例如 socialIdpUserId,它后来在 OutputClaimsTransformations 中使用。如果在将自定义策略上传到 Azure 并收到有关错误的一些详细消息时立即检查这一点会更好。
修复此问题后,我终于开始在客户端接收经过 IdP 身份验证的用户的 userId。现在唯一剩下的就是配置文件范围内的数据,例如 displayName、givenName 和 surname,但我希望我也能很快找到解决方案。您可以在 [EDITED 2] 下找到问题描述作为原始问题的一部分。
[UPDATE - SOLUTION]
昨天,在安静地花了很长时间尝试了这么多不同的事情之后,我终于明白了为什么我们没有把所有的索赔都归还给客户。它们实际上并不存在于 identity 令牌中,而仅存在于 access 令牌中。 AAD B2C 使用第一个,身份令牌,同时执行自定义策略中定义的映射,这就是重点。最后我不得不在 IdentityServer3 端做一些小改动(看看下面的代码)。
这就是负责发布声明并生成身份和访问令牌的 class 现在的样子:
public class CustomClaimsProvider : DefaultClaimsProvider
{
private readonly IIndex<string, IClaimsDefinition> claimDefinitions;
public CustomClaimsProvider(
IUserService users,
IIndex<string, IClaimsDefinition> claimDefinitions)
: base(users)
{
this.claimDefinitions = claimDefinitions;
}
public override async Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(
ClaimsPrincipal subject,
Client client,
IEnumerable<Scope> scopes,
bool includeAllIdentityClaims,
ValidatedRequest request)
{
var claims = await base.GetIdentityTokenClaimsAsync(subject, client, scopes, includeAllIdentityClaims, request).ConfigureAwait(false);
return GetAdditionalClaims(scopes, claims);
}
public override async Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(
ClaimsPrincipal subject,
Client client,
IEnumerable<Scope> scopes,
ValidatedRequest request)
{
var claims = await base.GetAccessTokenClaimsAsync(subject, client, scopes, request).ConfigureAwait(false);
return GetAdditionalClaims(scopes, claims);
}
private IEnumerable<Claim> GetAdditionalClaims(IEnumerable<Scope> scopes, IEnumerable<Claim> claims)
{
var scopesList = scopes.ToList();
var claimsList = claims.ToList();
foreach (var scope in scopesList.Select(x => x.Name))
{
if (claimDefinitions.TryGetValue(scope, out IClaimsDefinition claimDef))
{
claimsList.AddRange(claimDef.GetClaims(claims));
}
}
return claimsList;
}
}
所以,要点是,如果您想将一些额外的声明作为身份令牌的一部分,您还应该重写派生自 DefaultClaimsProvider 的 class 中的 GetIdentityTokenClaimsAsync 方法。
我想知道是否可以将 oid 声明或基本上由 Identity Server 3 发布的任何其他声明传播到 AAD B2C 并使其成为 Azure AD B2C 发布的令牌的一部分?我们需要在客户端有一个原始 ID,我们可以从 sub 和 oid 声明中获得的是 AAD B2C 用户帐户的 ID。我也想对角色声明做同样的事情。任何帮助将不胜感激。
[编辑]
这是我的身份服务器配置的技术配置文件在自定义策略 (TrustFrameworkExtensions.xml) 中的样子:
<TechnicalProfile Id="IdentityServerProfile">
<DisplayName>IdentityServer</DisplayName>
<Description>Login with your IdentityServer account</Description>
<Protocol Name="OpenIdConnect"/>
<OutputTokenFormat>JWT</OutputTokenFormat>
<Metadata>
<Item Key="METADATA">https://{identity_server_hostname}/identity/.well-known/openid-configuration</Item>
<Item Key="ProviderName">https://{identity_server_hostname}/identity</Item>
<Item Key="client_id">00000000-0000-0000-0000-000000000000</Item>
<Item Key="IdTokenAudience">00000000-0000-0000-0000-000000000000</Item>
<Item Key="response_types">code</Item>
<Item Key="scope">openid profile customScope</Item>
<Item Key="UsePolicyInRedirectUri">false</Item>
<Item Key="AccessTokenResponseFormat">json</Item>
<Item Key="HttpBinding">POST</Item>
</Metadata>
<CryptographicKeys>
<Key Id="client_secret" StorageReferenceId="B2C_1A_IdentityServerAppSecret"/>
</CryptographicKeys>
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="authenticationSource" DefaultValue="IdentityServer" />
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="name" />
<OutputClaim ClaimTypeReferenceId="identityProvider" DefaultValue="tid" />
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="sub" />
</OutputClaims>
<OutputClaimsTransformations>
<OutputClaimsTransformation ReferenceId="CreateRandomUPNUserName"/>
<OutputClaimsTransformation ReferenceId="CreateUserPrincipalName"/>
<OutputClaimsTransformation ReferenceId="CreateAlternativeSecurityId"/>
</OutputClaimsTransformations>
<UseTechnicalProfileForSessionManagement ReferenceId="SM-Noop"/>
</TechnicalProfile>
这是 Application Instights 的原始日志,来自具有给定 correlationId 的日志:
{
"TenantId":"{租户}.onmicrosoft.com",
"PolicyId":"B2C_1A_signup_signin_capcom",
"RedirectUri":"http://localhost:3000/",
"AdditionalParameters":{
"client_info":"1",
"x-client-SKU":"MSAL.JS",
"x-client-Ver":"0.1.5",
"client-request-id":"a0ef9d95-ba21-4796-ad65-bc9e2b8c5e89"
},
"Nonce":"bf198ee6-e084-433d-885c-2bd8f0292b3d",
"State":"7f732141-498e-4445-9efe-9b0d54178ef6",
"ClientId":"012c0f85-4568-483f-9bca-054d6201ac00",
"ResponseType":"id_token",
"ResponseMode":"fragment",
"ResponseRedirector":{
"URI":"http://localhost:3000/",
"D":错误,
"WF":是的,
"R":错误,
"S":假
},
"AppModelVersion":1,
"ScopedProviders":[
] }
我注意到并且发现奇怪的是 ResponseType 值。在我们的 TechnicalProfile 中设置为代码,这里它的值为 id_token.
[已编辑 2] 对 socialIdpUserId 的声明终于通过了。下面还有一个问题是什么的答案。现在,我仍然对 firstName、lastName 和 displayName 有疑问。我遵循了与 socialIdpUserId 相同的模式,但我唯一能看到的是默认值。 这是我对它们的配置:
1) 声明类型定义:
<ClaimType Id="displayName">
<DisplayName>Display Name</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="unique_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="displayName" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your display name.</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
...
<ClaimType Id="givenName">
<DisplayName>Given Name</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="given_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="given_name" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your given name (also known as first name).</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
<ClaimType Id="surname">
<DisplayName>Surname</DisplayName>
<DataType>string</DataType>
<DefaultPartnerClaimTypes>
<Protocol Name="OAuth2" PartnerClaimType="family_name" />
<Protocol Name="OpenIdConnect" PartnerClaimType="family_name" />
<Protocol Name="SAML2" PartnerClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" />
</DefaultPartnerClaimTypes>
<UserHelpText>Your surname (also known as family name or last name).</UserHelpText>
<UserInputType>TextBox</UserInputType>
</ClaimType>
并在我的 IdP 的技术资料中输出声明:
<TechnicalProfiles>
<TechnicalProfile Id="CapcomProfile">
...
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="firstName" DefaultValue="No" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="lastName" DefaultValue="Name" />
...
最后,RP 输出定义:
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="displayName" PartnerClaimType="displayName" />
<OutputClaim ClaimTypeReferenceId="givenName" PartnerClaimType="givenName" />
<OutputClaim ClaimTypeReferenceId="surname" PartnerClaimType="surname" DefaultValue="Not found in IdP" />
...
有什么建议吗?
为此,您必须从内置策略转移到 a custom policy,它将来自身份提供者的传入声明通过传出声明传递给应用程序。
例如,如果您从 the starter pack 创建自定义策略,那么您可以将用户标识符从 Facebook 传递到应用程序,如下所示:
1) 确保已声明 socialIdpUserId 声明:
<ClaimType Id="socialIdpUserId">
<DisplayName>Username</DisplayName>
<DataType>string</DataType>
</ClaimType>
2) 确保 socialIdpUserId 声明已添加到 Facebook 技术资料的 <OutputClaims />
集合中:
<ClaimsProvider>
<Domain>facebook.com</Domain>
...
<TechnicalProfiles>
<TechnicalProfile Id="Facebook-OAUTH">
<DisplayName>Facebook</DisplayName>
...
<OutputClaims>
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="id" />
...
</OutputClaims>
...
</TechnicalProfile>
</TechnicalProfiles>
</ClaimsProvider>
3) 确保 socialIdpUserId 声明已添加到依赖方技术资料的 <OutputClaims />
集合中:
<RelyingParty>
<DefaultUserJourney ReferenceId="SignUpOrSignIn" />
<TechnicalProfile Id="PolicyProfile">
<DisplayName>PolicyProfile</DisplayName>
...
<OutputClaims>
...
<OutputClaim ClaimTypeReferenceId="socialIdpUserId" PartnerClaimType="ext_id" />
</OutputClaims>
...
</TechnicalProfile>
</RelyingParty>
对于其他外部声明,您可以遵循相同的模式。
最后,我完成了这项工作,现在我的大部分索赔都通过了。最有可能的是,问题是由于我的密钥策略中缺少来自 Azure AD 应用程序的适当客户端机密,B2C_1A_CapcomIdentityServerAppSecret 更具体。因此,如果您已将适当的密钥从 AAD 应用程序密钥复制到您的策略密钥,请仔细检查它。
不幸的是,这只是解决方案的一部分,因为它仍然不能完全工作,而且我没有在客户端收到返回的令牌。但好消息是,在我配置好这些密钥之后,我开始在 Application Insights 中收到非常有用的异常(您可以在此处查看如何将自定义策略与 Application Insights 连接:https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-troubleshoot-custom)。我强烈建议任何对自定义策略有任何疑问的人进行设置。此外,如果您像我一样使用 IdentityServer3,请尝试在其一侧临时打开 http 日志记录。这是示例:
LoggingOptions = new LoggingOptions
{
EnableHttpLogging = true
}
当我在 Application Insights 中分析跟踪时,大多数异常的发生是因为我错过了一些属性,例如 socialIdpUserId,它后来在 OutputClaimsTransformations 中使用。如果在将自定义策略上传到 Azure 并收到有关错误的一些详细消息时立即检查这一点会更好。
修复此问题后,我终于开始在客户端接收经过 IdP 身份验证的用户的 userId。现在唯一剩下的就是配置文件范围内的数据,例如 displayName、givenName 和 surname,但我希望我也能很快找到解决方案。您可以在 [EDITED 2] 下找到问题描述作为原始问题的一部分。
[UPDATE - SOLUTION]
昨天,在安静地花了很长时间尝试了这么多不同的事情之后,我终于明白了为什么我们没有把所有的索赔都归还给客户。它们实际上并不存在于 identity 令牌中,而仅存在于 access 令牌中。 AAD B2C 使用第一个,身份令牌,同时执行自定义策略中定义的映射,这就是重点。最后我不得不在 IdentityServer3 端做一些小改动(看看下面的代码)。
这就是负责发布声明并生成身份和访问令牌的 class 现在的样子:
public class CustomClaimsProvider : DefaultClaimsProvider
{
private readonly IIndex<string, IClaimsDefinition> claimDefinitions;
public CustomClaimsProvider(
IUserService users,
IIndex<string, IClaimsDefinition> claimDefinitions)
: base(users)
{
this.claimDefinitions = claimDefinitions;
}
public override async Task<IEnumerable<Claim>> GetIdentityTokenClaimsAsync(
ClaimsPrincipal subject,
Client client,
IEnumerable<Scope> scopes,
bool includeAllIdentityClaims,
ValidatedRequest request)
{
var claims = await base.GetIdentityTokenClaimsAsync(subject, client, scopes, includeAllIdentityClaims, request).ConfigureAwait(false);
return GetAdditionalClaims(scopes, claims);
}
public override async Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(
ClaimsPrincipal subject,
Client client,
IEnumerable<Scope> scopes,
ValidatedRequest request)
{
var claims = await base.GetAccessTokenClaimsAsync(subject, client, scopes, request).ConfigureAwait(false);
return GetAdditionalClaims(scopes, claims);
}
private IEnumerable<Claim> GetAdditionalClaims(IEnumerable<Scope> scopes, IEnumerable<Claim> claims)
{
var scopesList = scopes.ToList();
var claimsList = claims.ToList();
foreach (var scope in scopesList.Select(x => x.Name))
{
if (claimDefinitions.TryGetValue(scope, out IClaimsDefinition claimDef))
{
claimsList.AddRange(claimDef.GetClaims(claims));
}
}
return claimsList;
}
}
所以,要点是,如果您想将一些额外的声明作为身份令牌的一部分,您还应该重写派生自 DefaultClaimsProvider 的 class 中的 GetIdentityTokenClaimsAsync 方法。