using System; using System.Collections.Generic; using System.Data.Entity; using System.Diagnostics; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using System.Web.Mvc; using Abp.Auditing; using Abp.Configuration.Startup; using Abp.Domain.Uow; using Abp.Extensions; using Abp.Localization; using Abp.Runtime.Session; using Abp.Threading; using Abp.Timing; using Abp.UI; using Abp.Web.Models; using Abp.Web.Security.AntiForgery; using WePlatform.Authorization; using WePlatform.Authorization.Roles; using WePlatform.Authorization.Users; using WePlatform.BaseSystem.Sessions.Dto; using WePlatform.Configuration; using WePlatform.Controllers.Results; using WePlatform.MultiTenancy; using WePlatform.Models; using WePlatform.Models.Account; using IwbZero.Auditing; using IwbZero.Authorization.Base; using IwbZero.Authorization.Base.Users; using IwbZero.Authorization.Users; using IwbZero.MultiTenancy; using IwbZero.ToolCommon.LogHelpers; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin.Security; namespace WePlatform.Controllers { [AuditLog("用户账号"), AllowAnonymous] public class AccountController : IwbControllerBase { private readonly TenantManager _tenantManager; private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly IUnitOfWorkManager _unitOfWorkManager; private readonly IMultiTenancyConfig _multiTenancyConfig; private readonly LogInManager _logInManager; private readonly ILanguageManager _languageManager; private readonly ITenantCache _tenantCache; private readonly IAuthenticationManager _authenticationManager; private readonly IAuditingHelper _auditingHelper; private readonly IAuditInfoProvider _auditInfoProvider; public AccountController( TenantManager tenantManager, UserManager userManager, RoleManager roleManager, IUnitOfWorkManager unitOfWorkManager, IMultiTenancyConfig multiTenancyConfig, LogInManager logInManager, ILanguageManager languageManager, ITenantCache tenantCache, IAuthenticationManager authenticationManager, IAuditingHelper auditingHelper, IAuditInfoProvider auditInfoProvider) { _tenantManager = tenantManager; _userManager = userManager; _roleManager = roleManager; _unitOfWorkManager = unitOfWorkManager; _multiTenancyConfig = multiTenancyConfig; _logInManager = logInManager; _languageManager = languageManager; _tenantCache = tenantCache; _authenticationManager = authenticationManager; _auditingHelper = auditingHelper; _auditInfoProvider = auditInfoProvider; } #region Login / Logout [DisableAuditing] [AuditLog("登陆")] public async Task Index(string returnUrl = "") { if (string.IsNullOrWhiteSpace(returnUrl)) { returnUrl = Request.ApplicationPath; } ViewBag.IsMultiTenancyEnabled = _multiTenancyConfig.IsEnabled; TenantLoginInfoDto tenantDto = null; List tenantList = new List(); if (_multiTenancyConfig.IsEnabled) { if (AbpSession.TenantId.HasValue) { var tenant = await _tenantManager.GetByIdAsync(AbpSession.GetTenantId()); tenant = await GetActiveTenantAsync(tenant.TenancyName); tenantDto = ObjectMapper.Map(tenant); } var tenants = await _tenantManager.GeTenants(); tenantList.Add(new SelectListItem() { Value = "", Text = L("NotSelected") }); foreach (var t in tenants) { tenantList.Add(new SelectListItem() { Value = t.TenancyName, Text = t.Name }); } } ViewBag.TenantList = tenantList; return View("Login", new LoginFormViewModel { Tenant = tenantDto, ReturnUrl = returnUrl, IsMultiTenancyEnabled = _multiTenancyConfig.IsEnabled, IsSelfRegistrationAllowed = false, MultiTenancySide = AbpSession.MultiTenancySide }); } [HttpPost] [DisableAuditing] [AuditLog("登陆")] public async Task Login(LoginViewModel loginModel, string returnUrl = "", string returnUrlHash = "") { var actionStopwatch = Stopwatch.StartNew(); CheckModelState(); var loginResult = await GetLoginResultAsync( loginModel.UsernameOrEmailAddress, loginModel.Password, GetTenancyNameOrNull() ); await SignInAsync(loginResult.User, loginResult.Identity, loginModel.RememberMe); if (string.IsNullOrWhiteSpace(returnUrl)) { returnUrl = Request.ApplicationPath; } if (!string.IsNullOrWhiteSpace(returnUrlHash)) { returnUrl = returnUrl + returnUrlHash; } actionStopwatch.Stop(); await WriteLoginLog(loginResult, actionStopwatch); return Json(new AjaxResponse { TargetUrl = returnUrl }); } private async Task> GetLoginResultAsync(string usernameOrEmailAddress, string password, string tenancyName) { var loginResult = await _logInManager.LoginAsync(usernameOrEmailAddress, password, tenancyName); switch (loginResult.Result) { case IwbLoginResultType.Success: return loginResult; default: throw CreateExceptionForFailedLoginAttempt(loginResult.Result, usernameOrEmailAddress, tenancyName); } } private async Task SignInAsync(User user, ClaimsIdentity identity = null, bool rememberMe = false) { if (identity == null) { identity = await _userManager.CreateIdentityAsync(user, IwbAuthenticationTypes.ApplicationCookie); } _authenticationManager.SignOut(IwbAuthenticationTypes.ApplicationCookie); // Many browsers do not clean up session cookies when you close them. So the rule of thumb must be: // For having a consistent behaviour across all browsers, don't rely solely on browser behaviour for proper clean-up // of session cookies. It is safer to use non-session cookies (IsPersistent == true) in bundle with an expiration date. // See http://blog.petersondave.com/cookies/Session-Cookies-in-Chrome-Firefox-and-Sitecore/ if (rememberMe) { _authenticationManager.SignIn(new AuthenticationProperties { IsPersistent = true }, identity); } else { _authenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(int.Parse(System.Configuration.ConfigurationManager.AppSettings["AuthSession.ExpireTimeInMinutes.WhenNotPersistent"] ?? "30")) }, identity); } } private Exception CreateExceptionForFailedLoginAttempt(IwbLoginResultType result, string usernameOrEmailAddress, string tenancyName) { switch (result) { case IwbLoginResultType.Success: return new ApplicationException("Don't call this method with a success result!"); case IwbLoginResultType.InvalidUserNameOrEmailAddress: case IwbLoginResultType.InvalidPassword: return new UserFriendlyException(L("LoginFailed"), L("InvalidUserNameOrPassword")); case IwbLoginResultType.InvalidTenancyName: return new UserFriendlyException(L("LoginFailed"), L("ThereIsNoTenantDefinedWithName{0}", tenancyName)); case IwbLoginResultType.TenantIsNotActive: return new UserFriendlyException(L("LoginFailed"), L("TenantIsNotActive", tenancyName)); case IwbLoginResultType.UserIsNotActive: return new UserFriendlyException(L("LoginFailed"), L("UserIsNotActiveAndCanNotLogin", usernameOrEmailAddress)); case IwbLoginResultType.UserEmailIsNotConfirmed: return new UserFriendlyException(L("LoginFailed"), "UserEmailIsNotConfirmedAndCanNotLogin"); case IwbLoginResultType.LockedOut: return new UserFriendlyException(L("LoginFailed"), L("UserLockedOutMessage")); default: //Can not fall to default actually. But other result types can be added in the future and we may forget to handle it Logger.Warn("Unhandled login fail reason: " + result); return new UserFriendlyException(L("LoginFailed")); } } public ActionResult Logout() { _authenticationManager.SignOut(); return RedirectToAction("Index"); } private async Task WriteLoginLog(IwbLoginResult loginResult, Stopwatch actionStopwatch) { try { var auditInfo = new AuditInfo { TenantId = loginResult.User.TenantId, UserId = loginResult.User.Id, ImpersonatorUserId = AbpSession.ImpersonatorUserId, ImpersonatorTenantId = AbpSession.ImpersonatorTenantId, ServiceName = "AccountController", MethodName = "Login", Parameters = "", ExecutionTime = Clock.Now, ExecutionDuration = Convert.ToInt32(actionStopwatch.Elapsed.TotalMilliseconds) }; _auditInfoProvider.Fill(auditInfo); await _auditingHelper.SaveAsync(auditInfo); } catch (Exception e) { this.LogError(e); } } #endregion #region Register public ActionResult Register() { return RegisterView(new RegisterViewModel()); } private ActionResult RegisterView(RegisterViewModel model) { ViewBag.IsMultiTenancyEnabled = _multiTenancyConfig.IsEnabled; return View("Register", model); } //private bool IsSelfRegistrationEnabled() //{ // if (!AbpSession.TenantId.HasValue) // { // return false; //No registration enabled for host users! // } // return true; //} [HttpPost] public virtual async Task Register(RegisterViewModel model) { try { CheckModelState(); //Create user var user = new User { Name = model.Name, Surname = model.Surname, EmailAddress = model.EmailAddress, IsActive = true }; //Get external login info if possible ExternalLoginInfo externalLoginInfo = null; if (model.IsExternalLogin) { externalLoginInfo = await _authenticationManager.GetExternalLoginInfoAsync(); if (externalLoginInfo == null) { throw new ApplicationException("Can not external login!"); } user.Logins = new List { new UserLogin { LoginProvider = externalLoginInfo.Login.LoginProvider, ProviderKey = externalLoginInfo.Login.ProviderKey } }; if (model.UserName.IsNullOrEmpty()) { model.UserName = model.EmailAddress; } model.Password = Authorization.Users.User.CreateRandomPassword(); if (string.Equals(externalLoginInfo.Email, model.EmailAddress, StringComparison.InvariantCultureIgnoreCase)) { user.IsEmailConfirmed = true; } } else { //Username and Password are required if not external login if (model.UserName.IsNullOrEmpty() || model.Password.IsNullOrEmpty()) { throw new UserFriendlyException(L("FormIsNotValidMessage")); } } user.UserName = model.UserName; user.Password = new PasswordHasher().HashPassword(model.Password); //Switch to the tenant _unitOfWorkManager.Current.EnableFilter(AbpDataFilters.MayHaveTenant); //TODO: Needed? _unitOfWorkManager.Current.SetTenantId(AbpSession.GetTenantId()); //Add default roles user.Roles = new List(); foreach (var defaultRole in await _roleManager.Roles.Where(r => r.IsDefault).ToListAsync()) { user.Roles.Add(new UserRole { RoleId = defaultRole.Id }); } //Save user CheckErrors(await _userManager.CreateAsync(user)); await _unitOfWorkManager.Current.SaveChangesAsync(); //Directly login if possible if (user.IsActive) { IwbLoginResult loginResult; if (externalLoginInfo != null) { loginResult = await _logInManager.LoginAsync(externalLoginInfo.Login, GetTenancyNameOrNull()); } else { loginResult = await GetLoginResultAsync(user.UserName, model.Password, GetTenancyNameOrNull()); } if (loginResult.Result == IwbLoginResultType.Success) { await SignInAsync(loginResult.User, loginResult.Identity); return Redirect(Url.Action("Index", "Home")); } Logger.Warn("New registered user could not be login. This should not be normally. login result: " + loginResult.Result); } //If can not login, show a register result page return View("RegisterResult", new RegisterResultViewModel { TenancyName = GetTenancyNameOrNull(), NameAndSurname = user.Name + " " + user.Surname, UserName = user.UserName, EmailAddress = user.EmailAddress, IsActive = user.IsActive }); } catch (UserFriendlyException ex) { ViewBag.IsMultiTenancyEnabled = _multiTenancyConfig.IsEnabled; ViewBag.ErrorMessage = ex.Message; return View("Register", model); } } #endregion #region External Login [HttpPost] [ValidateAntiForgeryToken] public ActionResult ExternalLogin(string provider, string returnUrl) { return new ChallengeResult( provider, Url.Action( "ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl, tenancyName = GetTenancyNameOrNull() }) ); } [UnitOfWork] [DisableAbpAntiForgeryTokenValidation] public virtual async Task ExternalLoginCallback(string returnUrl, string tenancyName = "") { var loginInfo = await _authenticationManager.GetExternalLoginInfoAsync(); if (loginInfo == null) { return RedirectToAction("Login"); } //Try to find tenancy name if (tenancyName.IsNullOrEmpty()) { var tenants = await FindPossibleTenantsOfUserAsync(loginInfo.Login); switch (tenants.Count) { case 0: return await RegisterView(loginInfo); case 1: tenancyName = tenants[0].TenancyName; break; default: return View("TenantSelection", model: new TenantSelectionViewModel { Action = Url.Action("ExternalLoginCallback", "Account", new { returnUrl }), Tenants = ObjectMapper.Map>(tenants) }); } } var loginResult = await _logInManager.LoginAsync(loginInfo.Login, tenancyName); switch (loginResult.Result) { case IwbLoginResultType.Success: await SignInAsync(loginResult.User, loginResult.Identity); if (string.IsNullOrWhiteSpace(returnUrl)) { returnUrl = Url.Action("Index", "Home"); } return Redirect(returnUrl); case IwbLoginResultType.UnknownExternalLogin: return await RegisterView(loginInfo, tenancyName); default: throw CreateExceptionForFailedLoginAttempt(loginResult.Result, loginInfo.Email ?? loginInfo.DefaultUserName, tenancyName); } } private async Task RegisterView(ExternalLoginInfo loginInfo, string tenancyName = null) { var name = loginInfo.DefaultUserName; var surname = loginInfo.DefaultUserName; var extractedNameAndSurname = TryExtractNameAndSurnameFromClaims(loginInfo.ExternalIdentity.Claims.ToList(), ref name, ref surname); var viewModel = new RegisterViewModel { EmailAddress = loginInfo.Email, Name = name, Surname = surname, IsExternalLogin = true }; if (!tenancyName.IsNullOrEmpty() && extractedNameAndSurname) { return await Register(viewModel); } return RegisterView(viewModel); } protected virtual async Task> FindPossibleTenantsOfUserAsync(UserLoginInfo login) { List allUsers; using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.MayHaveTenant)) { allUsers = await _userManager.FindAllAsync(login); } return allUsers .Where(u => u.TenantId != null) .Select(u => AsyncHelper.RunSync(() => _tenantManager.FindByIdAsync(u.TenantId.Value))) .ToList(); } private static bool TryExtractNameAndSurnameFromClaims(List claims, ref string name, ref string surname) { string foundName = null; string foundSurname = null; var givennameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.GivenName); if (givennameClaim != null && !givennameClaim.Value.IsNullOrEmpty()) { foundName = givennameClaim.Value; } var surnameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Surname); if (surnameClaim != null && !surnameClaim.Value.IsNullOrEmpty()) { foundSurname = surnameClaim.Value; } if (foundName == null || foundSurname == null) { var nameClaim = claims.FirstOrDefault(c => c.Type == ClaimTypes.Name); if (nameClaim != null) { var nameSurName = nameClaim.Value; if (!nameSurName.IsNullOrEmpty()) { var lastSpaceIndex = nameSurName.LastIndexOf(' '); if (lastSpaceIndex < 1 || lastSpaceIndex > (nameSurName.Length - 2)) { foundName = foundSurname = nameSurName; } else { foundName = nameSurName.Substring(0, lastSpaceIndex); foundSurname = nameSurName.Substring(lastSpaceIndex); } } } } if (!foundName.IsNullOrEmpty()) { name = foundName; } if (!foundSurname.IsNullOrEmpty()) { surname = foundSurname; } return foundName != null && foundSurname != null; } #endregion #region Common private methods private async Task GetActiveTenantAsync(string tenancyName) { var tenant = await _tenantManager.FindByTenancyNameAsync(tenancyName); if (tenant == null) { throw new UserFriendlyException(L("ThereIsNoTenantDefinedWithName{0}", tenancyName)); } if (!tenant.IsActive) { throw new UserFriendlyException(L("TenantIsNotActive", tenancyName)); } return tenant; } private string GetTenancyNameOrNull() { if (!AbpSession.TenantId.HasValue) { return null; } return _tenantCache.GetOrNull(AbpSession.TenantId.Value)?.TenancyName; } #endregion #region Common Partial Views [ChildActionOnly] public PartialViewResult _AccountLanguages() { var model = new LanguageSelectionViewModel { CurrentLanguage = _languageManager.CurrentLanguage, Languages = _languageManager.GetLanguages().Where(l => !l.IsDisabled).ToList() .Where(l => !l.IsDisabled) .ToList(), CurrentUrl = Request.Path }; return PartialView("_AccountLanguages", model); } #endregion } }