添加租户声明以基于acr值使用IdentityServer 4访问令牌

问题描述 投票:0回答:2

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

当客户端应用程序尝试登录时,我指定一个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中使用IsActiveAsync时,只有acr值可用的点在context.Caller == AuthorizeEndpoint方法中。

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

但是在IsActiveAsync中,我无法提出索赔。在GetProfileDataAsync调用中,acr值在ProfileDataRequestContext或HttpContext中均不可用。在这里我想访问ACR值时context.Caller = IdentityServerConstants.ProfileDataCallers.ClaimsProviderAccessToken。如果我可以访问,我可以发出房客索赔。

另外,我没有成功地分析CustomTokenRequestValidatorIClaimsServiceITokenService。看来根本问题是令牌端点不接收/处理acr值。 (尽管提到了here acr事件)

我很难理解这一点。任何帮助表示赞赏。我尝试的内容可能完全错误吗?在弄清楚这一点之后,我也将了解这如何影响访问令牌刷新。

identityserver4
2个回答
2
投票

因为您希望用户登录每个租户(绕过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));

当调用ProfileService时,您可以使用声明并将其传递给访问令牌:

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是令牌提供者,因此为什么不创建包含租户信息的自定义令牌。使用extension grants创建访问令牌,该访问令牌将承租人和用户结合在一起,并仅限制对该组合的访问。


0
投票

为已接受答案的其他人提供一些代码,这些人希望将扩展许可验证器用作一个建议的选项。请注意,该代码又快又脏,必须进行适当的检查。Here是带有扩展许可验证器的类似stackoverflow答案。

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,
                    },
                },

客户端中的令牌交换

我制作了一个剃须刀页面,该页面接收到所请求的租户ID作为url参数,因为我的测试应用程序是blazor服务器端应用程序,并且我在使用新令牌登录时遇到问题(通过_userStore.StoreTokenAsync)。请注意,我正在使用IdentityModel.AspNetCore管理令牌刷新。那就是为什么我使用IUserTokenStore。否则,您必须将httpcontext.signinasync设为Here

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());
      }
    }
  }
© www.soinside.com 2019 - 2024. All rights reserved.