diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginInfoDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginInfoDto.cs new file mode 100644 index 000000000..c43952808 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginInfoDto.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.Account; +public class ExternalLoginInfoDto +{ + public string Name { get; set; } + + public string DisplayName { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginResultDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginResultDto.cs new file mode 100644 index 000000000..930c6456f --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginResultDto.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace LINGYUN.Abp.Account; +public class ExternalLoginResultDto +{ + public List UserLogins { get; set; } = new List(); + public List ExternalLogins { get; set; } = new List(); +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RemoveExternalLoginInput.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RemoveExternalLoginInput.cs new file mode 100644 index 000000000..2b537020c --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RemoveExternalLoginInput.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace LINGYUN.Abp.Account; +public class RemoveExternalLoginInput +{ + [Required] + public string LoginProvider { get; set; } + + [Required] + public string ProviderKey { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UserLoginInfoDto.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UserLoginInfoDto.cs new file mode 100644 index 000000000..f9d674356 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UserLoginInfoDto.cs @@ -0,0 +1,7 @@ +namespace LINGYUN.Abp.Account; +public class UserLoginInfoDto +{ + public string LoginProvider { get; set; } + public string ProviderKey { get; set; } + public string ProviderDisplayName { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN.Abp.Account.WeChat.Work.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN.Abp.Account.WeChat.Work.csproj new file mode 100644 index 000000000..1712ee376 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN.Abp.Account.WeChat.Work.csproj @@ -0,0 +1,25 @@ + + + + + + + net9.0 + LINGYUN.Abp.Account.WeChat.Work + LINGYUN.Abp.Account.WeChat.Work + false + false + false + + + + + + + + + + + + + diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/AbpAccountWeChatWorkModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/AbpAccountWeChatWorkModule.cs new file mode 100644 index 000000000..a2bf20dcf --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/AbpAccountWeChatWorkModule.cs @@ -0,0 +1,27 @@ +using LINGYUN.Abp.WeChat.Work; +using LINGYUN.Abp.WeChat.Work.Localization; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.AspNetCore.Mvc.Localization; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Account.WeChat.Work; + +[DependsOn( + typeof(AbpWeChatWorkModule), + typeof(AbpAspNetCoreMvcModule))] +public class AbpAccountWeChatWorkModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(options => + { + options.AddAssemblyResource(typeof(WeChatWorkResource), typeof(AbpAccountWeChatWorkModule).Assembly); + }); + + PreConfigure(mvcBuilder => + { + mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountWeChatWorkModule).Assembly); + }); + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/Controllers/WeChatWorkAccountController.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/Controllers/WeChatWorkAccountController.cs new file mode 100644 index 000000000..978061f6c --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/Controllers/WeChatWorkAccountController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Authorization; +using System.Threading.Tasks; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Identity; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Account.WeChat.Work.Controllers; + +[Authorize] +public class WeChatWorkAccountController : AbpControllerBase +{ + private readonly IdentityUserManager identityUserManager; + /// + /// 绑定用户 + /// + /// + /// + public async virtual Task BindAsync(string code) + { + var user = await identityUserManager.GetByIdAsync(CurrentUser.GetId()); + + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs index 4d85a1cf0..d6aedbcfc 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs @@ -3,4 +3,5 @@ public static class AbpAccountAuthenticationTypes { public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; + public const string ConfirmUserScheme = "Abp.Account.ConfirmUser"; } diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs new file mode 100644 index 000000000..316d6c489 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs @@ -0,0 +1,111 @@ +using LINGYUN.Abp.Account.Web.ExternalProviders; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Account; +using Volo.Abp.Account.Localization; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Identity; +using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers; + +[Controller] +[Area(AccountRemoteServiceConsts.ModuleName)] +[Route($"api/{AccountRemoteServiceConsts.ModuleName}")] +[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)] +public class AccountController : AbpController +{ + protected AbpSignInManager SignInManager => LazyServiceProvider.LazyGetRequiredService(); + protected IdentityUserManager UserManager => LazyServiceProvider.LazyGetRequiredService(); + protected IExternalProviderService ExternalProviderService => LazyServiceProvider.LazyGetRequiredService(); + + public AccountController() + { + LocalizationResource = typeof(AccountResource); + } + + [HttpGet] + [Authorize] + [Route("external-logins")] + public async virtual Task GetExternalLoginsAsync() + { + var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId()); + var userLogins = await UserManager.GetLoginsAsync(currentUser); + var externalProviders = await ExternalProviderService.GetAllAsync(); + + return new ExternalLoginResultDto + { + UserLogins = userLogins.Select(x => new UserLoginInfoDto + { + ProviderDisplayName = x.ProviderDisplayName, + ProviderKey = x.ProviderKey, + LoginProvider = x.LoginProvider, + }).ToList(), + ExternalLogins = externalProviders.Select(x => + { + return new ExternalLoginInfoDto + { + Name = x.Name, + DisplayName = x.DisplayName, + }; + }).ToList(), + }; + } + + [HttpDelete] + [Authorize] + [Route("external-logins/remove")] + public async virtual Task RemoveExternalLoginAsync(RemoveExternalLoginInput input) + { + var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId()); + var identityResult = await UserManager.RemoveLoginAsync( + currentUser, + input.LoginProvider, + input.ProviderKey); + + if (!identityResult.Succeeded) + { + throw new UserFriendlyException("Operation failed: " + identityResult.Errors.Select(e => $"[{e.Code}] {e.Description}").JoinAsString(", ")); + } + // 解绑的是当前身份认证方案则退出登录 + var amr = CurrentUser.FindClaimValue(ClaimTypes.AuthenticationMethod); + if (!amr.IsNullOrWhiteSpace() && string.Equals(amr, input.LoginProvider, StringComparison.InvariantCultureIgnoreCase)) + { + await SignInManager.SignOutAsync(); + } + } + + [HttpGet] + [Authorize] + [Route("external-logins/bind")] + public virtual async Task ExternalLoginBindAsync(string provider, string returnUrl) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(returnUrl)) + { + Logger.LogWarning("The parameter is incorrect"); + return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary() + { + ["error"] = "The parameter is incorrect" + })); + } + + var tenantId = CurrentTenant.Id; + var userId = CurrentUser.GetId(); + + var redirectUrl = Url.Page("/Account/ExternalLoginBind", pageHandler: "BindCallback", values: new { returnUrl, userId, tenantId }); + + var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, userId.ToString()); + properties.Items["scheme"] = provider; + + return await Task.FromResult(Challenge(properties, provider)); + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml new file mode 100644 index 000000000..eba8e5d10 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml @@ -0,0 +1,4 @@ +@page +@model LINGYUN.Abp.Account.Web.Pages.Account.ExternalLoginBindModel +@{ +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml.cs new file mode 100644 index 000000000..d574c7ec8 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.AspNetCore.Mvc.UI.RazorPages; +using Volo.Abp.Identity; +using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.Uow; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Account.Web.Pages.Account; + +[IgnoreAntiforgeryToken] +public class ExternalLoginBindModel : AbpPageModel +{ + protected AbpSignInManager SignInManager => LazyServiceProvider.LazyGetRequiredService(); + + protected IdentityUserManager UserManager => LazyServiceProvider.LazyGetRequiredService(); + + protected IOptions IdentityOptions => LazyServiceProvider.LazyGetRequiredService>(); + + public virtual async Task OnGetAsync() + { + return await Task.FromResult(NotFound()); + } + + /// + /// SPA绑定第三方绑定 + /// + /// 第三方提供者 + /// 绑定成功回调地址 + /// IActionResult + public virtual async Task OnPostAsync(string provider, string returnUrl) + { + if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(returnUrl)) + { + Logger.LogWarning("The parameter is incorrect"); + return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary() + { + ["error"] = "The parameter is incorrect" + })); + } + + var tenantId = CurrentTenant.Id; + Logger.LogInformation("CurrentTenant:{TenantId}", tenantId); + var userId = CurrentUser.GetId(); + Logger.LogInformation("CurrentUser:{UserId}", userId); + + var redirectUrl = Url.Page("ExternalLoginBind", pageHandler: "BindCallback", values: new { returnUrl, userId, tenantId }); + + var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, userId.ToString()); + properties.Items["scheme"] = provider; + + return await Task.FromResult(Challenge(properties, provider)); + } + + /// + /// SPA绑定第三方登录回调 + /// + /// 绑定成功回调地址 + /// 用户Id + /// 租户Id + /// A representing the asynchronous operation. + [UnitOfWork] + public virtual async Task OnGetBindCallbackAsync(string returnUrl, string userId, Guid? tenantId) + { + if (string.IsNullOrWhiteSpace(returnUrl)) + { + Logger.LogWarning("The returnUrl cannot be empty"); + return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary() + { + ["error"] = "The returnUrl cannot be empty" + })); + } + + using (CurrentTenant.Change(tenantId)) + { + Logger.LogInformation("CurrentTenant:{TenantId}", tenantId); + + await IdentityOptions.SetAsync(); + + var loginInfo = await SignInManager.GetExternalLoginInfoAsync(userId); + if (loginInfo == null) + { + Logger.LogWarning("External login info is not available"); + return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary() + { + ["error"] = "External login info is not available." + })); + } + + await SignInManager.SignOutAsync(); + + if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null) + { + var externalUser = await UserManager.FindByIdAsync(userId); + CheckIdentityErrors(await UserManager.AddLoginAsync(externalUser, loginInfo)); + } + + return Redirect(returnUrl); + } + } + + protected virtual void CheckIdentityErrors(IdentityResult identityResult) + { + if (!identityResult.Succeeded) + { + throw new UserFriendlyException("Operation failed: " + identityResult.Errors.Select(e => $"[{e.Code}] {e.Description}").JoinAsString(", ")); + } + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs index 00998e44b..4c14d1399 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs @@ -361,13 +361,15 @@ public class LoginModel : AccountPageModel if (result.IsLockedOut) { Logger.LogWarning($"External login callback error: user is locked out!"); - throw new UserFriendlyException("Cannot proceed because user is locked out!"); + + return await HandleUserLockedOut(); } if (result.IsNotAllowed) { Logger.LogWarning($"External login callback error: user is not allowed!"); - throw new UserFriendlyException("Cannot proceed because user is not allowed!"); + + return await HandleExternalLoginNotAllowed(loginInfo); } IdentityUser user; @@ -525,14 +527,7 @@ public class LoginModel : AccountPageModel // û޸ if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) { - var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword); - changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, notAllowedUser.Id.ToString())); - if (notAllowedUser.TenantId.HasValue) - { - changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, notAllowedUser.TenantId.ToString())); - } - - await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity)); + await StoreChangePasswordUserAsync(notAllowedUser); return RedirectToPage("ChangePassword", new { @@ -546,6 +541,56 @@ public class LoginModel : AccountPageModel return Page(); } + protected async virtual Task HandleExternalLoginNotAllowed(ExternalLoginInfo loginInfo) + { + Logger.LogWarning("External login callback error: User is Not Allowed!"); + + var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (user == null) + { + Logger.LogWarning($"External login callback error: User is Not Found!"); + return RedirectToPage("./Login"); + } + + if (user.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(user)) + { + await StoreChangePasswordUserAsync(user); + return RedirectToPage("./ChangePassword", new + { + ReturnUrl, + ReturnUrlHash + }); + } + + Alerts.Warning(L["LoginIsNotAllowed"]); + return Page(); + } + + protected virtual async Task StoreChangePasswordUserAsync(IdentityUser user) + { + var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword); + changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, user.Id.ToString())); + if (user.TenantId.HasValue) + { + changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString())); + } + + await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity)); + } + + protected virtual async Task StoreConfirmUserAsync(IdentityUser user) + { + var identity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ConfirmUserScheme); + identity.AddClaim(new Claim(AbpClaimTypes.UserId, user.Id.ToString())); + + if (user.TenantId.HasValue) + { + identity.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString())); + } + + await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ConfirmUserScheme, new ClaimsPrincipal(identity)); + } + protected virtual Task HandleUserNameOrPasswordInvalid() { Alerts.Danger(L["InvalidUserNameOrPassword"]); diff --git a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs index 20fd42d54..5047fd01c 100644 --- a/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs +++ b/aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs @@ -24,7 +24,7 @@ public class WeChatWorkNotificationPublishProvider : NotificationPublishProvider protected ISettingProvider SettingProvider => ServiceProvider.LazyGetRequiredService(); protected IStringLocalizerFactory LocalizerFactory => ServiceProvider.LazyGetRequiredService(); protected IWeChatWorkMessageSender WeChatWorkMessageSender => ServiceProvider.LazyGetRequiredService(); - protected IWeChatWorkInternalUserFinder WeChatWorkInternalUserFinder => ServiceProvider.LazyGetRequiredService(); + protected IWeChatWorkUserClaimProvider WeChatWorkInternalUserFinder => ServiceProvider.LazyGetRequiredService(); protected INotificationDefinitionManager NotificationDefinitionManager => ServiceProvider.LazyGetRequiredService(); protected async override Task CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default)