ABP.IO - 多租户 - 从外部 IDP 设置租户

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

我正在尝试将 Auth0 配置为我的 ABP.IO 应用程序(带有集成身份服务器的 MVC)中的外部登录提供程序。我已经可以正常登录了,但我不知道如何在 ABP 端设置租户。

我想出的是 Auth0 端的一条规则,用于将 TenantId 填充为 id 令牌中的声明,这样我就可以在

GetExternalLoginInfoAsync
方法中的自定义 SingInManager 中解析它,如下所示:

string tenantId = auth.Principal.FindFirstValue("https://example.com/tenantId");

我只是很难弄清楚如何处理它。假设用户将被配置为通过 Auth0 进行身份验证,并且用户将在首次登录时在本地创建(这同样适用于租户部分)。

multi-tenant auth0 abp
2个回答
1
投票

好吧,这是我已经采取的解决方法,它应该可以转移到您所依赖的任何外部登录系统。我不确定这是否是正确的方法,所以如果其他人想参与更高效的系统,我会洗耳恭听。

无论如何,我的工作流程假设您已经像我一样创建了一种从外部 IDP 发送 TenantId 的机制。为此,我使用了 Auth0 中的组织功能,并将 TenantId 添加为元数据,然后在 Auth0 中创建了一个操作,将该元数据附加为要在 ABP 端使用的声明。

在ABP中,我按照这篇文章重写了SignInManager:

https://community.abp.io/articles/how-to-customize-the-signin-manager-3e858753

如本文中所述,我重写了登录管理器的

GetExternalLoginInfoAsync

 方法,并添加了以下行以将 TenantId 从 Auth0 声明中提取出来,并使用预定义的 
AbpClaimTypes.TenantId
 值将其添加回来。

编辑:我还必须重写

ExternalLoginSignInAsync

 方法来解决多租户问题(否则它会一直尝试重新创建用户并抛出重复的电子邮件错误)。我将在下面发布完整的课程,并在评论中添加我添加的内容:

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

 的使用位置,并发现我必须重写登录页面 
CreateExternalUserAsync
 内的 
LoginModel
 方法。为此,我按照本文中的说明创建了 
CustomLoginModel.cs
Login.cshtml
https://community.abp.io/articles/hide-the-tenant-switch-of-the-login-page -4foaup7p

所以,我的 Auth0LoginModel 类看起来像这样:

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 之前更改租户。

完成后,用户将在正确的租户中创建,一切都会按预期进行。


0
投票
我们最近使用了@dlboutwe的方法,但基于abp current(目前为8.1.1)进行了修改:

为了在没有静态值的情况下处理发送租户,我们实现 OnRedirectToIdentityProvider 事件,如下所示:

authBuilder.AddAuth0WebAppAuthentication(options => { options.Domain = configuration.GetValue("Auth0:Domain", string.Empty)!; options.ClientId = configuration.GetValue("Auth0:ClientId", string.Empty)!; options.CallbackPath = "/signin-auth0"; options.OpenIdConnectEvents = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents() { OnRedirectToIdentityProvider = async (ctx) => { var tenantRepo = ctx.HttpContext.RequestServices.GetRequiredService<ITenantRepository>(); // Determine the tenant ID dynamically based on the current context var tenants = await tenantRepo.GetListAsync(); var tenant = tenants[0]; // Better way to know? Kind of only makes sense if there is one and only one tenancy // Append the tenant_id to the authentication request's parameters ctx.ProtocolMessage.SetParameter("tenant_id", tenant.Id.ToString()); } }; });
然后,要反映从 Auth0 返回的tenantId - 修改自定义操作:

/** * Handler that will be called during the execution of a PostLogin flow. * * @param {Event} event - Details about the user and the context in which they are logging in. * @param {PostLoginAPI} api - Interface whose methods can be used to change the behavior of the login. */ exports.onExecutePostLogin = async (event, api) => { // Retrieve the tenant ID from the incoming request's parameters const tenantId = event.request.query.tenant_id || event.transaction?.request.query.tenant_id || "DefaultTenantId"; api.idToken.setCustomClaim("https://example.com/tenantId", tenantId); api.accessToken.setCustomClaim("https://example.com/tenantId", tenantId); };
为了完整起见,这是我的全部

CustomSignInManager

/// <summary> /// Signin manager that allows and handles Auth0 /// </summary> public class CustomSignInManager : AbpSignInManager { private readonly IDataFilter _dataFilter; private const string LoginProviderKey = "LoginProvider"; private const string XsrfKey = "XsrfId"; public CustomSignInManager( IDataFilter dataFilter, IdentityUserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<Volo.Abp.Identity.IdentityUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<CustomSignInManager> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation<Volo.Abp.Identity.IdentityUser> confirmation, IOptions<AbpIdentityOptions> options, ISettingProvider settingProvider) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, options, settingProvider) { _dataFilter = dataFilter; } public override async Task<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, it should 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; } 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); } }
    
© www.soinside.com 2019 - 2024. All rights reserved.