根据 acr 值使用标识服务器 4 将租户声明添加到访问令牌



在我的方案中,用户可以链接到不同的租户。用户应在租户的上下文中登录。这意味着我希望访问令牌包含租户声明类型,以限制对该租户数据的访问。

当客户端应用程序尝试登录时,我指定一个 acr 值来指示要登录的租户。

OnRedirectToIdentityProvider = redirectContext => {
if (redirectContext.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication) {
redirectContext.ProtocolMessage.AcrValues = "tenant:" + tenantId; // the acr value tenant:{value} is treated special by id4 and is made available in IIdentityServerInteractionService
}
return Task.CompletedTask;
}

该值由我的标识提供者解决方案接收,并且在IIdentityServerInteractionService中也可用。

现在的问题是,在哪里可以为请求的租户添加对访问令牌的声明?

IProfileService

在 IProfileService 实现中,当通过 IHttpContextAccessor 在 HttpContext 中context.Caller == AuthorizeEndpoint时,acr 值唯一可用的点是在IsActiveAsync方法中。

String acr_values = _context.HttpContext.Request.Query["acr_values"].ToString();

但在IsActiveAsync我不能提出索赔。 在GetProfileDataAsync调用中,acr 值在 ProfileDataRequestContext 和 HttpContext 中都不可用。在这里,我想在以下情况下访问 acr 值context.Caller = IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken.如果我有访问权限,我可以发出租户声明。

进一步我分析了CustomTokenRequestValidatorIClaimsServiceITokenService,但没有成功。似乎根本问题是令牌终结点不接收/处理 acr 值。(虽然这里提到了ACR(

我很难弄清楚这一点。任何帮助表示赞赏。我正在尝试的可能是完全错误的吗?弄清楚这一点后,我还必须了解这如何影响访问令牌刷新。

由于您希望用户为每个租户登录(绕过 SSO(,因此此解决方案成为可能。

登录时,您可以向存储租户名称的本地用户 (IdentityServer( 添加声明:

public async Task<IActionResult> Login(LoginViewModel model, string button)
{
// take returnUrl from the query
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context?.ClientId != null)
{
// acr value Tenant
if (context.Tenant == null)
await HttpContext.SignInAsync(user.Id, user.UserName);
else
await HttpContext.SignInAsync(user.Id, user.UserName, new Claim("tenant", context.Tenant));

调用配置文件服务时,可以使用声明并将其传递给访问令牌:

public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Only add the claim to the access token
if (context.Caller == "ClaimsProviderAccessToken")
{
var tenant = context.Subject.FindFirstValue("tenant");
if (tenant != null)
claims.Add(new Claim("tenant", tenant));
}

该声明现在在客户端中可用。


问题是,使用单一登录时,本地用户将分配给上次使用的租户。因此,您需要确保用户必须再次登录,忽略并覆盖IdentityServer上的cookie。

这是客户端的责任,因此您可以设置prompt=login强制登录。但是,由于源自客户端,您可能希望将此作为服务器的责任。在这种情况下,您可能需要覆盖交互响应生成器。


但是,当您想要添加特定于租户的声明时,执行此类操作是有意义的。但您似乎只对区分租户感兴趣。

在这种情况下,我不会使用上面的实现,而是从角度出发。我认为有一个更简单的解决方案,您可以保留 SSO 的能力。

如果租户在资源上标识自己,该怎么办?IdentityServer 是一个令牌提供程序,因此为什么不创建一个包含租户信息的自定义令牌。使用扩展授权创建组合租户和用户的访问令牌,并仅限制对该组合的访问。

为其他想要使用扩展授权验证器作为接受答案的建议选项的人提供一些代码。 小心,代码又快又脏,必须正确审查。 这是一个类似的堆栈溢出答案,带有扩展授权验证器。

IExtensionGrantValidator

using IdentityServer4.Models;
using IdentityServer4.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace IdentityService.Logic {
public class TenantExtensionGrantValidator : IExtensionGrantValidator {
public string GrantType => "Tenant";
private readonly ITokenValidator _validator;
private readonly MyUserManager _userManager;
public TenantExtensionGrantValidator(ITokenValidator validator, MyUserManager userManager) {
_validator = validator;
_userManager = userManager;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
String userToken = context.Request.Raw.Get("AccessToken");
String tenantIdRequested = context.Request.Raw.Get("TenantIdRequested");
if (String.IsNullOrEmpty(userToken)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var result = await _validator.ValidateAccessTokenAsync(userToken).ConfigureAwait(false);
if (result.IsError) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
if (Guid.TryParse(tenantIdRequested, out Guid tenantId)) {
var sub = result.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
var claims = result.Claims.ToList();
claims.RemoveAll(x => x.Type == "tenantid");
IEnumerable<Guid> tenantIdsAvailable = await _userManager.GetTenantIds(Guid.Parse(sub)).ConfigureAwait(false);
if (tenantIdsAvailable.Contains(tenantId)) {
claims.Add(new Claim("tenantid", tenantId.ToString()));
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.Result = new GrantValidationResult(principal);
return;
}
}
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
}
}
}

客户端配置

new Client {
ClientId = "tenant.client",
ClientSecrets = { new Secret("xxx".Sha256()) },
AllowedGrantTypes = new [] { "Tenant" },
RequireConsent = false,
RequirePkce = true,
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true,
AllowedScopes = new List<String> {
IdentityServerConstants.StandardScopes.OpenId,
},
},

客户端中的令牌交换

我制作了一个 razor 页面,该页面接收请求的租户 ID 作为 url 参数,因为我的测试应用是 blazor 服务器端应用,并且我在使用新令牌登录(通过_userStore.StoreTokenAsync(时遇到问题。请注意,我正在使用IdentityModel.AspNetCore来管理令牌刷新。这就是我使用IUserTokenStore的原因。否则,您将不得不像此处一样执行httpcontext.signinasync。

public class TenantSpecificAccessTokenModel : PageModel {
private readonly IUserTokenStore _userTokenStore;
public TenantSpecificAccessTokenModel(IUserTokenStore userTokenStore) {
_userTokenStore = userTokenStore;
}
public async Task OnGetAsync() {
Guid tenantId = Guid.Parse(HttpContext.Request.Query["tenantid"]);
await DoSignInForTenant(tenantId);
}
public async Task DoSignInForTenant(Guid tenantId) {
HttpClient client = new HttpClient();
Dictionary<String, String> parameters = new Dictionary<string, string>();
parameters.Add("AccessToken", await HttpContext.GetUserAccessTokenAsync());
parameters.Add("TenantIdRequested", tenantId.ToString());
TokenRequest tokenRequest = new TokenRequest() {
Address = IdentityProviderConfiguration.Authority + "connect/token",
ClientId = "tenant.client",
ClientSecret = "xxx",
GrantType = "Tenant",
Parameters = parameters
};
TokenResponse tokenResponse = await client.RequestTokenAsync(tokenRequest).ConfigureAwait(false);
if (!tokenResponse.IsError) {
await _userTokenStore.StoreTokenAsync(HttpContext.User, tokenResponse.AccessToken, tokenResponse.ExpiresIn, tokenResponse.RefreshToken);
Response.Redirect(Url.Content("~/").ToString());
}
}
}

最新更新