ABP.IO - 多租户 - 从外部 IDP 设置租户
ABP.IO - MultiTenancy - Setting Tenant from External IDP
我正在尝试在我的 ABP.IO 应用程序(具有集成身份服务器的 MVC)中将 Auth0 配置为外部登录提供程序。我已经让它工作了,所以我可以正常登录,但我不知道如何在 ABP 端设置租户。
我想出的是 Auth0 端的规则,用于填充 TenantId 作为 id 令牌中的声明,因此我可以在 GetExternalLoginInfoAsync
方法中的自定义 SingInManager 中解析它,如下所示:
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");
我只是很难从那里弄清楚如何处理它。假设用户将被配置为通过 Auth0 进行身份验证,并且用户将在首次登录时在本地创建(这同样适用于租户部分)。
好的,这是我的解决方法,它应该可以转移到您所依赖的任何外部登录系统。我不确定这是否是正确的做法,所以如果有人想加入一个更高效的系统,我会洗耳恭听。
无论如何,我的工作流程假定您已经像我一样创建了一种机制,以便从外部 IDP 发送 TenantId。为此,我使用了 Auth0 中的 Organizations 功能并将 TenantId 添加为元数据,然后我在 Auth0 中创建了一个 Action 以将该元数据附加为要在 ABP 端使用的声明。
在ABP中,我按照这篇文章重写了SignInManager:https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753
如文章中所述,我覆盖了登录管理器的 GetExternalLoginInfoAsync
方法并添加了以下行以将 TenantId 从 Auth0 声明中提取出来并使用预定义的 AbpClaimTypes.TenantId
值。
编辑:我还必须覆盖 ExternalLoginSignInAsync
方法来解决多租户问题(否则它会一直尝试重新创建用户并抛出重复的电子邮件错误)。我将在下面 post 完整的 class 以及我在评论中添加的内容:
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>
{
private const string LoginProviderKey = "LoginProvider";
private const string XsrfKey = "XsrfId";
private readonly IDataFilter _dataFilter;
public CustomSignInManager(
IDataFilter dataFilter,
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_dataFilter = dataFilter;
}
/// <summary>
/// Gets the external login information for the current login, as an asynchronous operation.
/// </summary>
/// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
/// for the sign-in attempt.</returns>
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(XsrfKey))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
?? provider;
/* Begin tenantId claim search */
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId"); //pull the tenantId claim if it exists
if(!string.IsNullOrEmpty(tenantId))
{
auth.Principal.Identities.FirstOrDefault().AddClaim(new Claim(AbpClaimTypes.TenantId, tenantId)); //if there is a tenantId, add the AbpClaimTypes.TenantId claim back into the principal
}
/* End tenantId claim search */
var eli = new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
return eli;
}
/// <summary>
/// Signs in a user via a previously registered third party login, as an asynchronous operation.
/// </summary>
/// <param name="loginProvider">The login provider to use.</param>
/// <param name="providerKey">The unique provider identifier for the user.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="bypassTwoFactor">Flag indicating whether to bypass two factor authentication.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
/// for the sign-in attempt.</returns>
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
Volo.Abp.Identity.IdentityUser user = null; //stage the user variable as null
using (_dataFilter.Disable<IMultiTenant>()) //disable the tenantid filters so we can search all logins for the expected key
{
user = await UserManager.FindByLoginAsync(loginProvider, providerKey); //search logins for the expected key
}
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
}
完成后,我追踪了 GetExternalLoginInfoAsync
的使用位置,并发现我必须覆盖登录页面 LoginModel
内的 CreateExternalUserAsync
方法。为此,我按照本文中的说明创建 CustomLoginModel.cs
和 Login.cshtml
:https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page-4foaup7p
所以,我的 Auth0LoginModel class 看起来像这样:
public class Auth0LoginModel : LoginModel
{
public Auth0LoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions) : base(schemeProvider, accountOptions, identityOptions)
{
}
protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
/* Begin TenantId claim check */
var tenantId = info.Principal.FindFirstValue(AbpClaimTypes.TenantId);
if (!string.IsNullOrEmpty(tenantId))
{
try
{
CurrentTenant.Change(Guid.Parse(tenantId));
}
catch
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Unable to parse TenantId: " + tenantId
}) ;
}
}
/* End TenantId claim check */
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
return user;
}
}
添加的代码在注释之间,方法的其余部分是从源代码中提取的。因此,我寻找存在的 AbpClaimTypes.TenantId
声明,如果存在,我会尝试使用 CurrentTenant.Change
方法在调用之前更改租户以创建新的 IdentityUser。
一旦完成,用户就会在正确的租户中创建,一切都会像预期的那样进行。
我正在尝试在我的 ABP.IO 应用程序(具有集成身份服务器的 MVC)中将 Auth0 配置为外部登录提供程序。我已经让它工作了,所以我可以正常登录,但我不知道如何在 ABP 端设置租户。
我想出的是 Auth0 端的规则,用于填充 TenantId 作为 id 令牌中的声明,因此我可以在 GetExternalLoginInfoAsync
方法中的自定义 SingInManager 中解析它,如下所示:
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");
我只是很难从那里弄清楚如何处理它。假设用户将被配置为通过 Auth0 进行身份验证,并且用户将在首次登录时在本地创建(这同样适用于租户部分)。
好的,这是我的解决方法,它应该可以转移到您所依赖的任何外部登录系统。我不确定这是否是正确的做法,所以如果有人想加入一个更高效的系统,我会洗耳恭听。
无论如何,我的工作流程假定您已经像我一样创建了一种机制,以便从外部 IDP 发送 TenantId。为此,我使用了 Auth0 中的 Organizations 功能并将 TenantId 添加为元数据,然后我在 Auth0 中创建了一个 Action 以将该元数据附加为要在 ABP 端使用的声明。
在ABP中,我按照这篇文章重写了SignInManager:https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753
如文章中所述,我覆盖了登录管理器的 GetExternalLoginInfoAsync
方法并添加了以下行以将 TenantId 从 Auth0 声明中提取出来并使用预定义的 AbpClaimTypes.TenantId
值。
编辑:我还必须覆盖 ExternalLoginSignInAsync
方法来解决多租户问题(否则它会一直尝试重新创建用户并抛出重复的电子邮件错误)。我将在下面 post 完整的 class 以及我在评论中添加的内容:
public class CustomSignInManager : Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>
{
private const string LoginProviderKey = "LoginProvider";
private const string XsrfKey = "XsrfId";
private readonly IDataFilter _dataFilter;
public CustomSignInManager(
IDataFilter dataFilter,
Microsoft.AspNetCore.Identity.UserManager<Volo.Abp.Identity.IdentityUser> userManager,
Microsoft.AspNetCore.Http.IHttpContextAccessor contextAccessor,
Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory,
Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.IdentityOptions> optionsAccessor,
Microsoft.Extensions.Logging.ILogger<Microsoft.AspNetCore.Identity.SignInManager<Volo.Abp.Identity.IdentityUser>> logger,
Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider schemes,
Microsoft.AspNetCore.Identity.IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
_dataFilter = dataFilter;
}
/// <summary>
/// Gets the external login information for the current login, as an asynchronous operation.
/// </summary>
/// <param name="expectedXsrf">Flag indication whether a Cross Site Request Forgery token was expected in the current request.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="ExternalLoginInfo"/>
/// for the sign-in attempt.</returns>
public override async Task<Microsoft.AspNetCore.Identity.ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
var auth = await Context.AuthenticateAsync(IdentityConstants.ExternalScheme);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(LoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(XsrfKey))
{
return null;
}
var userId = items[XsrfKey] as string;
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var provider = items[LoginProviderKey] as string;
if (providerKey == null || provider == null)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName
?? provider;
/* Begin tenantId claim search */
string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId"); //pull the tenantId claim if it exists
if(!string.IsNullOrEmpty(tenantId))
{
auth.Principal.Identities.FirstOrDefault().AddClaim(new Claim(AbpClaimTypes.TenantId, tenantId)); //if there is a tenantId, add the AbpClaimTypes.TenantId claim back into the principal
}
/* End tenantId claim search */
var eli = new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
return eli;
}
/// <summary>
/// Signs in a user via a previously registered third party login, as an asynchronous operation.
/// </summary>
/// <param name="loginProvider">The login provider to use.</param>
/// <param name="providerKey">The unique provider identifier for the user.</param>
/// <param name="isPersistent">Flag indicating whether the sign-in cookie should persist after the browser is closed.</param>
/// <param name="bypassTwoFactor">Flag indicating whether to bypass two factor authentication.</param>
/// <returns>The task object representing the asynchronous operation containing the <see name="SignInResult"/>
/// for the sign-in attempt.</returns>
public override async Task<SignInResult> ExternalLoginSignInAsync(string loginProvider, string providerKey, bool isPersistent, bool bypassTwoFactor)
{
Volo.Abp.Identity.IdentityUser user = null; //stage the user variable as null
using (_dataFilter.Disable<IMultiTenant>()) //disable the tenantid filters so we can search all logins for the expected key
{
user = await UserManager.FindByLoginAsync(loginProvider, providerKey); //search logins for the expected key
}
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
return await SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
}
}
完成后,我追踪了 GetExternalLoginInfoAsync
的使用位置,并发现我必须覆盖登录页面 LoginModel
内的 CreateExternalUserAsync
方法。为此,我按照本文中的说明创建 CustomLoginModel.cs
和 Login.cshtml
:https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page-4foaup7p
所以,我的 Auth0LoginModel class 看起来像这样:
public class Auth0LoginModel : LoginModel
{
public Auth0LoginModel(IAuthenticationSchemeProvider schemeProvider, IOptions<AbpAccountOptions> accountOptions, IOptions<IdentityOptions> identityOptions) : base(schemeProvider, accountOptions, identityOptions)
{
}
protected override async Task<IdentityUser> CreateExternalUserAsync(ExternalLoginInfo info)
{
await IdentityOptions.SetAsync();
var emailAddress = info.Principal.FindFirstValue(AbpClaimTypes.Email);
/* Begin TenantId claim check */
var tenantId = info.Principal.FindFirstValue(AbpClaimTypes.TenantId);
if (!string.IsNullOrEmpty(tenantId))
{
try
{
CurrentTenant.Change(Guid.Parse(tenantId));
}
catch
{
await IdentitySecurityLogManager.SaveAsync(new IdentitySecurityLogContext()
{
Identity = IdentitySecurityLogIdentityConsts.IdentityExternal,
Action = "Unable to parse TenantId: " + tenantId
}) ;
}
}
/* End TenantId claim check */
var user = new IdentityUser(GuidGenerator.Create(), emailAddress, emailAddress, CurrentTenant.Id);
CheckIdentityErrors(await UserManager.CreateAsync(user));
CheckIdentityErrors(await UserManager.SetEmailAsync(user, emailAddress));
CheckIdentityErrors(await UserManager.AddLoginAsync(user, info));
CheckIdentityErrors(await UserManager.AddDefaultRolesAsync(user));
return user;
}
}
添加的代码在注释之间,方法的其余部分是从源代码中提取的。因此,我寻找存在的 AbpClaimTypes.TenantId
声明,如果存在,我会尝试使用 CurrentTenant.Change
方法在调用之前更改租户以创建新的 IdentityUser。
一旦完成,用户就会在正确的租户中创建,一切都会像预期的那样进行。