【问题标题】:Add tenant claim to access token using IdentityServer 4 based on acr value根据 acr 值使用 IdentityServer 4 添加租户声明以访问令牌
【发布时间】:2020-06-08 18:21:31
【问题描述】:

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

当客户端应用程序尝试登录时,我指定一个 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 实现中,acr 值可用的唯一点是在 IsActiveAsync 方法中,而 context.Caller == AuthorizeEndpoint 在 HttpContext 中通过 IHttpContextAccessor。

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 的事件)

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

【问题讨论】:

  • 来自文档:acr_values 允许为 password 授权类型传递额外的身份验证相关信息。对于其他流,命中 ProfileService 的请求不包括 acr 值。所以是的,端点没有收到 acr 值。

标签: identityserver4


【解决方案1】:

由于您希望用户为每个租户登录(绕过 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 创建一个访问令牌,将租户和用户组合在一起,并限制仅对该组合的访问。

【讨论】:

  • 这是正确答案。我已经使用类似的方法来实现基于子域/域的多租户和基于租户选择页面的多租户(使用交互响应)。
  • 非常感谢 Ruard。我将尝试根据您的信息解决问题并稍后接受。 @LaliltaCode,老实说,我看到了你的 youtube 视频和你的博客文章。我非常感谢这些信息。尽管如此,我还是无法使用该信息完成我的任务,因为部分原因是我感到有些困惑。也许以后我会的。
  • 我必须同时采用这两种方法。 :) 使用 acr/tenant/claim 登录,并且我还编写了一个 IExtensionGrantValidator ,我可以用它交换没有租户信息的访问令牌与有租户信息的访问令牌。但最重要的是你的回答让我想到了我真正想要的。我看到三个选项:(1)访问令牌中没有租户信息。调用 api 时,租户可以是普通参数。因此,api 必须检查此租户的访问权限,就像我对细粒度访问规则所做的那样。 (2) 登录用户/租户对 (3) 使用用户登录。稍后为我想访问的任何租户交换令牌。
  • 我认为从用户的角度来看,应该有意识地选择我想与哪个租户合作。一般来说,我认为延期补助是要走的路。如有必要,如果用户在没有租户的情况下首次登录,也许我将能够创建一个带有租户选择的登录流程。但是客户端可能会指定一个 acr“DoTenantSelection”,然后我的身份服务器实现会提供一个页面来执行此操作。基本上我想从租户选择/令牌交换中提取逻辑到我的身份服务器实现。因此客户端可以调用登录页面或只是切换租户页面。身份证
  • 我感觉您的样本中缺少某些内容。当您执行claims.Add(new Claim("tenant", tenant)); 时,似乎claims 变量不会在context 中的任何位置出现,我也看不到它的声明位置。我错过了什么?
【解决方案2】:

为希望使用扩展授权验证器的其他人提供一些代码,作为接受答案的建议选项。 小心,代码又快又脏,必须经过适当的审查。 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,
                    },
                },

客户端中的令牌交换

我创建了一个 razor 页面,它接收请求的租户 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());
      }
    }
  }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-05-14
    • 2020-05-19
    • 1970-01-01
    • 2016-07-31
    • 1970-01-01
    • 1970-01-01
    • 2020-01-16
    • 1970-01-01
    相关资源
    最近更新 更多