在我的情况下,用户可以链接到不同的租户。用户应在租户的上下文中登录。这意味着我希望访问令牌包含租户声明类型,以限制对该租户数据的访问。
当客户端应用程序尝试登录时,我指定一个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
。如果我可以访问,我可以发出房客索赔。
另外,我没有成功地分析CustomTokenRequestValidator
,IClaimsService
和ITokenService
。看来根本问题是令牌端点不接收/处理acr值。 (尽管提到了here 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));
当调用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创建访问令牌,该访问令牌将承租人和用户结合在一起,并仅限制对该组合的访问。
为已接受答案的其他人提供一些代码,这些人希望将扩展许可验证器用作一个建议的选项。请注意,该代码又快又脏,必须进行适当的检查。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());
}
}
}