using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using RSecurityBackend.Models.Auth.Db; using RSecurityBackend.Models.Auth.ViewModels; using RSecurityBackend.Models.Generic; using RSecurityBackend.DbContext; using RSecurityBackend.Models.Image; using RSecurityBackend.Models.Auth.Memory; using RSecurityBackend.Models.Audit.Db; using Microsoft.Extensions.Configuration; namespace RSecurityBackend.Services.Implementation { /// /// Authentication Service /// public class AppUserService : IAppUserService { /// /// Login user, if failed return LoggedOnUserModel is null /// /// /// /// public virtual async Task> Login(LoginViewModel loginViewModel, string clientIPAddress) { //we ignore loginViewModel in automatic auditing to prevent logging password data, so we would add a manual auditing to have enough data on login intrusion and ... REvent log = new REvent() { EventType = "AppUser/Login (POST)(Manual)", StartDate = DateTime.UtcNow, UserName = loginViewModel.Username, IpAddress = clientIPAddress }; _context.AuditLogs.Add(log); await _context.SaveChangesAsync(); RServiceResult checkUserExists = await EnsureDefaultUserExists(); if (!checkUserExists.Result) { return new RServiceResult(null, checkUserExists.ExceptionString); } RAppUser appUser = await _userManager.FindByNameAsync(loginViewModel.Username); if (appUser == null) { appUser = await _userManager.FindByEmailAsync(loginViewModel.Username); if (appUser == null) { return new RServiceResult(null, "نام کاربری و/یا رمز نادرست است."); } } var result = await _signInManager.CheckPasswordSignInAsync(appUser, loginViewModel.Password, true); if (result.IsLockedOut) { return new RServiceResult(null, "نام کاربری شما به دلیل ورود متوالی ۵ بارهٔ رمزهای اشتباه قفل شده است. لطفاً ۵ دقیقهٔ‌دیگر مجدداً تلاش کنید."); } if (result.IsNotAllowed || result.RequiresTwoFactor) { return new RServiceResult(null, result.ToString()); } if (!result.Succeeded) { return new RServiceResult(null, "نام کاربری و/یا رمز نادرست است."); } if (appUser.Status == RAppUserStatus.Inactive) { return new RServiceResult(null, "نام کاربری شما غیرفعال شده است."); } RServiceResult securableItems = await GetUserSecurableItemsStatus(appUser.Id); if (!string.IsNullOrEmpty(securableItems.ExceptionString)) return new RServiceResult(null, securableItems.ExceptionString); RTemporaryUserSession userSession = new RTemporaryUserSession() { RAppUserId = appUser.Id, ClientIPAddress = clientIPAddress, ClientAppName = loginViewModel.ClientAppName, Language = loginViewModel.Language, LoginTime = DateTime.Now, LastRenewal = DateTime.Now, ValidUntil = DateTime.Now + TimeSpan.FromSeconds(DefaultTokenExpirationInSeconds), Token = "" }; await _context.Sessions.AddAsync(userSession); await _context.SaveChangesAsync(); RServiceResult userToken = await GenerateToken(loginViewModel.Username, appUser.Id, userSession.Id); if (userToken.Result == null) { return new RServiceResult(null, userToken.ExceptionString); } userSession.Token = userToken.Result; _context.Sessions.Update(userSession); _context.SaveChanges(); return new RServiceResult( new LoggedOnUserModel() { SessionId = userSession.Id, User = new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }, Token = userToken.Result, SecurableItem = securableItems.Result } ); } /// /// replace a (probably expired session) with a new one /// /// /// /// public virtual async Task> ReLogin(Guid sessionId, string clientIPAddress) { RTemporaryUserSession oldSession = await _context.Sessions.Include(s => s.RAppUser).Where(s => s.Id == sessionId).SingleOrDefaultAsync(); if (oldSession == null) { return new RServiceResult(null, "Invalid session"); } RAppUser appUser = oldSession.RAppUser; if (appUser.Status == RAppUserStatus.Inactive) { return new RServiceResult(null, "User is disabled by an admin."); } RServiceResult securableItems = await GetUserSecurableItemsStatus(appUser.Id); if (!string.IsNullOrEmpty(securableItems.ExceptionString)) return new RServiceResult(null, securableItems.ExceptionString); RTemporaryUserSession newSession = new RTemporaryUserSession() { RAppUserId = appUser.Id, ClientIPAddress = clientIPAddress, ClientAppName = oldSession.ClientAppName, Language = oldSession.Language, LoginTime = DateTime.Now, LastRenewal = DateTime.Now, ValidUntil = DateTime.Now + TimeSpan.FromSeconds(DefaultTokenExpirationInSeconds), Token = "" }; await _context.Sessions.AddAsync(newSession); _context.Sessions.Remove(oldSession); await _context.SaveChangesAsync(); RServiceResult userToken = await GenerateToken(appUser.UserName, appUser.Id, newSession.Id); if (userToken.Result == null) { return new RServiceResult(null, userToken.ExceptionString); } newSession.Token = userToken.Result; _context.Sessions.Update(newSession); _context.SaveChanges(); return new RServiceResult( new LoggedOnUserModel() { SessionId = newSession.Id, User = new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }, Token = userToken.Result, SecurableItem = securableItems.Result } ); } /// /// add user to role /// /// /// /// public virtual async Task> AddUserToRole(Guid userId, string roleName) { RAppUser dbUserInfo = await _userManager.Users.Where(u => u.Id == userId).SingleOrDefaultAsync(); if (dbUserInfo == null) { return new RServiceResult(false, $"کاربر مورد نظر با ایمیل {userId} پیدا نشد "); } if (!await _roleManager.RoleExistsAsync(roleName)) { var identityResult = await _roleManager.CreateAsync(new RAppRole(roleName)); if (!identityResult.Succeeded) { return new RServiceResult(false, $"Error creating {roleName} role : " + ErrorsToString(identityResult.Errors)); } } var addToRoleResult = await _userManager.AddToRoleAsync(dbUserInfo, roleName); if (!addToRoleResult.Succeeded) { return new RServiceResult(false, $"Error adding admin to {roleName} role : " + ErrorsToString(addToRoleResult.Errors)); } return new RServiceResult(true); } /// /// Logout /// /// /// /// public virtual async Task> Logout(Guid userId, Guid sessionId) { RTemporaryUserSession session = await _context.Sessions .Where(s => s.Id == sessionId && s.RAppUserId == userId) .FirstOrDefaultAsync(); if (session == null) return new RServiceResult(false, "session is invalid"); _context.Sessions.Remove(session); await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// Does Session exist? /// /// /// /// public virtual async Task> SessionExists(Guid userId, Guid sessionId) { return new RServiceResult( await _context.Sessions .Where(s => s.RAppUserId == userId && s.Id == sessionId) .FirstOrDefaultAsync() != null ); } /// /// returns user information /// /// /// PasswordHash becomes empty /// /// /// public virtual async Task> GetUserInformation(Guid userId) { RAppUser appUser = await _userManager.Users.Where(u => u.Id == userId).SingleOrDefaultAsync(); if (appUser == null) return new RServiceResult(null); return new RServiceResult( new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }); } /// /// all users informations /// /// /// /// public virtual async Task> GetAllUsersInformation(PagingParameterModel paging, string filterByEmail) { var source = _userManager.Users .Where(appUser => string.IsNullOrEmpty(filterByEmail) || (!string.IsNullOrEmpty(filterByEmail) && appUser.Email.Contains(filterByEmail))) .Select(appUser => new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }); return new RServiceResult<(PaginationMetadata PagingMeta, PublicRAppUser[] Items)>( await QueryablePaginator.Paginate(source, paging)); } /// /// Get User Sessions /// /// if null is passed returns all sessions /// public virtual async Task> GetUserSessions(Guid? userId) { List publicRUserSessions = new List(); RTemporaryUserSession[] sessions = userId == null ? await _context.Sessions.ToArrayAsync() : await _context.Sessions.Where(s => s.RAppUserId == userId).ToArrayAsync() ; foreach (RTemporaryUserSession rUserSession in sessions) publicRUserSessions.Add( new PublicRUserSession() { Id = rUserSession.Id, RAppUser = new PublicRAppUser() { Id = rUserSession.RAppUser.Id, Username = rUserSession.RAppUser.UserName, Email = rUserSession.RAppUser.Email, FirstName = rUserSession.RAppUser.FirstName, SureName = rUserSession.RAppUser.SureName, PhoneNumber = rUserSession.RAppUser.PhoneNumber, RImageId = rUserSession.RAppUser.RImageId, Status = rUserSession.RAppUser.Status, NickName = rUserSession.RAppUser.NickName, Website = rUserSession.RAppUser.Website, Bio = rUserSession.RAppUser.Bio, EmailConfirmed = rUserSession.RAppUser.EmailConfirmed }, ClientAppName = rUserSession.ClientAppName, ClientIPAddress = rUserSession.ClientIPAddress, Language = rUserSession.Language, LastRenewal = rUserSession.LastRenewal, LoginTime = rUserSession.LoginTime } ); return new RServiceResult(publicRUserSessions.ToArray()); } /// /// is user admin? /// /// /// public virtual async Task> IsAdmin(Guid userId) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); if (dbUserInfo == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } bool res = await _userManager.IsInRoleAsync(dbUserInfo, _userRoleService.AdministratorRoleName); return new RServiceResult(res); } /// /// is user in either of passed roles? /// /// /// /// public virtual async Task> IsInRoles(Guid userId, string[] roleNames) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); if (dbUserInfo == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } foreach (string roleName in roleNames) { if (await _userManager.IsInRoleAsync(dbUserInfo, roleName)) { return new RServiceResult(true); } } return new RServiceResult(false); } /// /// Get User Roles /// /// /// public virtual async Task>> GetUserRoles(Guid userId) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); if (dbUserInfo == null) { return new RServiceResult>(null, "کاربر مورد نظر یافت نشد"); } return new RServiceResult>(await _userManager.GetRolesAsync(dbUserInfo)); } /// /// remove user from role /// /// user id /// /// public virtual async Task> RemoveFromRole(Guid id, string role) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(id.ToString()); if (dbUserInfo == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } IdentityResult result = await _userManager.RemoveFromRoleAsync(dbUserInfo, role); if (!result.Succeeded) { return new RServiceResult(false, ErrorsToString(result.Errors)); } return new RServiceResult(true); } /// /// add user to role /// /// user id /// /// public virtual async Task> AddToRole(Guid id, string role) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(id.ToString()); if (dbUserInfo == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } IdentityResult result = await _userManager.AddToRoleAsync(dbUserInfo, role); if (!result.Succeeded) { return new RServiceResult(false, ErrorsToString(result.Errors)); } return new RServiceResult(true); } /// /// Lists user permissions /// /// /// public virtual async Task> GetUserSecurableItemsStatus(Guid userId) { SecurableItem[] securableItems = _userRoleService.GetSecurableItems(); RServiceResult> roles = await GetUserRoles(userId); if (!string.IsNullOrEmpty(roles.ExceptionString)) return new RServiceResult(null, roles.ExceptionString); bool isAdmin = (await IsAdmin(userId)).Result; foreach (SecurableItem securableItem in securableItems) { foreach (SecurableItemOperation operation in securableItem.Operations) { foreach (string role in roles.Result) { RServiceResult hasPermission = await _userRoleService.HasPermission(role, securableItem.ShortName, operation.ShortName); if (!string.IsNullOrEmpty(hasPermission.ExceptionString)) return new RServiceResult(null, hasPermission.ExceptionString); if (isAdmin || hasPermission.Result) { operation.Status = true; } } } } return new RServiceResult(securableItems); } /// /// Has user specified permission /// /// /// /// /// public virtual async Task> HasPermission(Guid userId, string securableItemShortName, string operationShortName) { RServiceResult> roles = await GetUserRoles(userId); if (!string.IsNullOrEmpty(roles.ExceptionString)) return new RServiceResult(false, roles.ExceptionString); foreach (string role in roles.Result) { RServiceResult hasPermission = await _userRoleService.HasPermission(role, securableItemShortName, operationShortName); if (!string.IsNullOrEmpty(hasPermission.ExceptionString)) return new RServiceResult(false, hasPermission.ExceptionString); if (hasPermission.Result) { return new RServiceResult(true); } } return new RServiceResult ( false ); } /// /// all users having a certain permission /// /// /// /// public virtual async Task> GetUsersHavingPermission(string securableItemShortName, string operationShortName) { var rolesResult = await _userRoleService.GetRolesHavingPermission(securableItemShortName, operationShortName); if (!string.IsNullOrEmpty(rolesResult.ExceptionString)) { return new RServiceResult(null, rolesResult.ExceptionString); } List lstPublicUsersInfo = new List(); RAppRole[] roles = rolesResult.Result; foreach (RAppRole role in roles) { var usersInRole = _userManager.GetUsersInRoleAsync(role.Name); foreach (var appUser in usersInRole.Result) { if (lstPublicUsersInfo.Where(u => u.Id == appUser.Id).FirstOrDefault() == null) { lstPublicUsersInfo.Add( new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }); } } } return new RServiceResult(lstPublicUsersInfo.ToArray()); } /// /// add a new user /// /// public virtual async Task> AddUser(RegisterRAppUser newUserInfo) { if (!newUserInfo.IsAdmin) { RServiceResult checkUserExists = await EnsureDefaultUserExists(); if (!checkUserExists.Result) { return new RServiceResult(null, checkUserExists.ExceptionString); } } RAppUser existingInfo = await _userManager.FindByNameAsync(newUserInfo.Username); if (existingInfo != null) { return new RServiceResult(null, "username is already taken"); } if (string.IsNullOrEmpty(newUserInfo.Password)) { newUserInfo.Password = Guid.NewGuid().ToString(); } RAppUser newDbUser = new RAppUser() { UserName = newUserInfo.Username, FirstName = newUserInfo.FirstName, SureName = newUserInfo.SureName, Email = newUserInfo.Email, PhoneNumber = newUserInfo.PhoneNumber, CreateDate = DateTime.Now, Status = newUserInfo.Status }; var result = await _userManager.CreateAsync(newDbUser, newUserInfo.Password); if (!result.Succeeded) { return new RServiceResult(null, ErrorsToString(result.Errors)); } newUserInfo.Id = newDbUser.Id; if (newUserInfo.IsAdmin) { if (!await _roleManager.RoleExistsAsync(_userRoleService.AdministratorRoleName)) { var roleCheckResult = await _roleManager.CreateAsync(new RAppRole(_userRoleService.AdministratorRoleName)); if (!roleCheckResult.Succeeded) { return new RServiceResult(null, "Error creating Administrator role : " + ErrorsToString(roleCheckResult.Errors)); } } var addToAdminRoleResult = await _userManager.AddToRoleAsync(newDbUser, _userRoleService.AdministratorRoleName); if (!addToAdminRoleResult.Succeeded) { return new RServiceResult(null, $"Error adding {newDbUser.UserName} to Administrator role : " + ErrorsToString(addToAdminRoleResult.Errors)); } } return new RServiceResult(newDbUser); } /// /// modify existing user /// /// /// /// public virtual async Task> ModifyUser(Guid userId, RegisterRAppUser updateUserInfo) { RAppUser existingInfo = await _userManager.FindByIdAsync(userId.ToString()); if (existingInfo == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } RServiceResult isAdmin = await IsAdmin(userId); if (!string.IsNullOrEmpty(isAdmin.ExceptionString)) { return new RServiceResult(false, isAdmin.ExceptionString); } if (isAdmin.Result && !updateUserInfo.IsAdmin) { List adminUsers = new List(await _userManager.GetUsersInRoleAsync(_userRoleService.AdministratorRoleName)); int nActiveAdminUsers = 0; foreach (RAppUser adminUser in adminUsers) { if (adminUser.Status == RAppUserStatus.Active) nActiveAdminUsers++; } if (nActiveAdminUsers <= 1) { return new RServiceResult(false, "You cannot reduce number of active admin users to 0."); } } if (isAdmin.Result != updateUserInfo.IsAdmin) { return new RServiceResult(false, "امکان تغییر وضعیت مدیر کاربر از طریق این تابع وجود ندارد."); } if (existingInfo.UserName != updateUserInfo.Username) { RAppUser anotheruserWithUserName = await _userManager.FindByNameAsync(updateUserInfo.Username); if (anotheruserWithUserName != null) { return new RServiceResult(false, "کلمهٔ عبور تکراری می باشد"); } existingInfo.UserName = updateUserInfo.Username; } if (updateUserInfo.RImageId != null && updateUserInfo.RImageId != Guid.Empty && updateUserInfo.RImageId != existingInfo.RImageId) { return new RServiceResult(false, "برای تغییر تصویر کاربر از تابع اختصاصی این کار استفاده کنید"); } existingInfo.FirstName = updateUserInfo.FirstName; existingInfo.SureName = updateUserInfo.SureName; existingInfo.Email = updateUserInfo.Email; existingInfo.PhoneNumber = updateUserInfo.PhoneNumber; existingInfo.Status = updateUserInfo.Status; existingInfo.Bio = updateUserInfo.Bio; existingInfo.NickName = updateUserInfo.NickName; existingInfo.Website = updateUserInfo.Website; if (!string.IsNullOrEmpty(updateUserInfo.Password)) { foreach (var passwordValidator in _userManager.PasswordValidators) { var resPass = await passwordValidator.ValidateAsync(_userManager, existingInfo, updateUserInfo.Password); if (!resPass.Succeeded) { return new RServiceResult(false, ErrorsToString(resPass.Errors)); } } existingInfo.PasswordHash = _userManager.PasswordHasher.HashPassword(existingInfo, updateUserInfo.Password); } var result = await _userManager.UpdateAsync(existingInfo); if (!result.Succeeded) { return new RServiceResult(false, ErrorsToString(result.Errors)); } //updating admin status is not supported return new RServiceResult(true); } /// /// change user password checking old password /// /// /// /// /// public virtual async Task> ChangePassword(Guid userId, string oldPassword, string newPassword) { RAppUser appUser = await _userManager.FindByIdAsync(userId.ToString()); if (appUser == null) { return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } var result = await _userManager.ChangePasswordAsync(appUser, oldPassword, newPassword); if (!result.Succeeded) { return new RServiceResult(false, "Identity error details says: " + result.ToString()); } return new RServiceResult(true); } /// /// delete user /// /// /// true if succeeds public virtual async Task> DeleteUser(Guid userId) { RAppUser dbUserInfo = await _userManager.FindByIdAsync(userId.ToString()); if (dbUserInfo != null) { var result = await _userManager.DeleteAsync(dbUserInfo); if (!result.Succeeded) { return new RServiceResult(false, ErrorsToString(result.Errors)); } return new RServiceResult(true); } return new RServiceResult(false, "کاربر مورد نظر یافت نشد"); } /// /// Set User Image /// /// /// /// public virtual async Task> SetUserImage(Guid userId, IFormFileCollection files) { RAppUser user = await _userManager.FindByIdAsync(userId.ToString()); if (files.Count == 0) { user.RImageId = null; } else { if (files.Count != 1) { return new RServiceResult(null, "files.Count != 1"); } IFormFile file = files[0]; int nImageWidth = 192; using Stream stream = file.OpenReadStream(); using Image img = Image.FromStream(stream); if (img.Width > nImageWidth) { using Bitmap bmpPhase1 = new Bitmap(nImageWidth, nImageWidth); using (Graphics g = Graphics.FromImage(bmpPhase1)) { g.DrawImage(img, new Rectangle(0, 0, nImageWidth, img.Height * nImageWidth / img.Height)); } using Brush brush = new TextureBrush(bmpPhase1); using Bitmap bmpPhase2 = new Bitmap(nImageWidth, nImageWidth); using (Graphics g = Graphics.FromImage(bmpPhase2)) { g.FillEllipse(brush, new Rectangle(0, 0, nImageWidth, nImageWidth)); } bmpPhase2.MakeTransparent(); using MemoryStream ms = new MemoryStream(); bmpPhase2.Save(ms, ImageFormat.Png); ms.Position = 0; RServiceResult image = await _imageFileService.Add(null, ms, file.FileName, "UserProfiles"); if (!string.IsNullOrEmpty(image.ExceptionString)) { return new RServiceResult(null, image.ExceptionString); } user.RImage = image.Result; } else { RServiceResult image = await _imageFileService.Add(null, stream, file.FileName, "UserProfiles"); if (!string.IsNullOrEmpty(image.ExceptionString)) { return new RServiceResult(null, image.ExceptionString); } user.RImage = image.Result; } } var result = await _userManager.UpdateAsync(user); if (!result.Succeeded) { return new RServiceResult(null, ErrorsToString(result.Errors)); } return new RServiceResult((Guid?)user.RImageId); } /// /// Get User Image /// /// /// public virtual async Task> GetUserImage(Guid userId) { RAppUser user = await _userManager.FindByIdAsync(userId.ToString()); if (user.RImageId != null) { return await _imageFileService.GetImage((Guid)user.RImageId); } return new RServiceResult(null); } /// /// Start signup process using email /// /// /// /// /// /// public virtual async Task> SignUp(string email, string clientIPAddress, string clientAppName, string langauge) { if (string.IsNullOrEmpty(clientIPAddress)) { return new RServiceResult(null, "client ip address is empty"); } if (string.IsNullOrEmpty(clientAppName)) { return new RServiceResult(null, "client app name is empty"); } RAppUser existingUser = await _userManager.FindByEmailAsync(email); if (existingUser != null) { return new RServiceResult(null, "شما قبلا ثبت نام کرده‌اید. توجه بفرمایید که کاربران گنجینهٔ گنجور و پیشخان خوانشگران یکسانند و می‌توانید با همان نام کاربری اینجا وارد شوید."); } existingUser = await _userManager.FindByNameAsync(email); if (existingUser != null) { return new RServiceResult(null, "این نام کاربری قبلا استفاده شده است"); } var oldSecrets = await _context.VerifyQueueItems.Where(i => i.DateTime < DateTime.Now.AddDays(-1)).ToListAsync(); if (oldSecrets.Count > 0) { _context.VerifyQueueItems.RemoveRange(oldSecrets); await _context.SaveChangesAsync(); } //checking this queue for previous signup attempts is unnecessary and is not done intentionally RVerifyQueueItem item = new RVerifyQueueItem() { QueueType = RVerifyQueueType.SignUp, Email = email, DateTime = DateTime.Now, ClientIPAddress = clientIPAddress, ClientAppName = clientAppName, Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'), Language = langauge }; var existingSecrets = await _context.VerifyQueueItems.Where(i => i.Secret == item.Secret).ToListAsync(); if (existingSecrets.Count > 0) { _context.VerifyQueueItems.RemoveRange(existingSecrets); await _context.SaveChangesAsync(); } await _context.VerifyQueueItems.AddAsync ( item ); await _context.SaveChangesAsync(); return new RServiceResult(item); } /// /// verify signup / forgot password /// /// /// /// associated email public virtual async Task> RetrieveEmailFromQueueSecret(RVerifyQueueType verifyQueueType, string secret) { secret = secret.Trim(); RVerifyQueueItem item = await _context.VerifyQueueItems.Where(i => i.QueueType == verifyQueueType && i.Secret == secret).SingleOrDefaultAsync(); if (item == null) { return new RServiceResult(""); } return new RServiceResult(item.Email); } /// /// verify email on signup /// public bool VerifyEmailOnSignUp { get { return bool.Parse(Configuration.GetSection("SignUp")["VerifyEmail"]); } } /// /// finalize signup process using email /// /// /// /// /// /// /// public virtual async Task> FinalizeSignUp(string email, string secret, string password, string firstName, string sureName) { RAppUser existingUser = await _userManager.FindByEmailAsync(email); if (existingUser != null) { return new RServiceResult(false, "این آدرس ایمیل قبلا استفاده شده است"); } existingUser = await _userManager.FindByNameAsync(email); if (existingUser != null) { return new RServiceResult(false, "این نام کاربری قبلا استفاده شده است"); } bool verify = VerifyEmailOnSignUp; if (verify) { if ( email != (await RetrieveEmailFromQueueSecret(RVerifyQueueType.SignUp, secret)).Result ) { return new RServiceResult(false, "کد ارسالی به ایمیل اشتباه وارد شده است"); } } secret = secret.Trim(); firstName = firstName.Trim(); sureName = sureName.Trim(); if (string.IsNullOrEmpty(firstName) && string.IsNullOrEmpty(sureName)) { return new RServiceResult(false, "لطفاً حداقل یکی از اطلاعات نام یا نام خانوادگی را وارد کنید."); } RegisterRAppUser newUserInfo = new RegisterRAppUser() { Username = email, Email = email, Password = password, Status = RAppUserStatus.Active, IsAdmin = false, FirstName = firstName, SureName = sureName, NickName = $"{firstName} {sureName}".Trim() }; RServiceResult userAddResult = await AddUser(newUserInfo); if (userAddResult.Result == null) { return new RServiceResult(false, userAddResult.ExceptionString); } if (verify) { userAddResult.Result.EmailConfirmed = true; await _userManager.UpdateAsync(userAddResult.Result); } RVerifyQueueItem[] failedQueue = await _context.VerifyQueueItems.Where(i => i.Email == email && i.Secret != secret && i.QueueType == RVerifyQueueType.SignUp).ToArrayAsync(); if (failedQueue.Length != 0) { _context.VerifyQueueItems.RemoveRange(failedQueue); } await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// Start forgot password process using email /// /// /// /// /// /// public virtual async Task> ForgotPassword(string email, string clientIPAddress, string clientAppName, string langauge) { if (string.IsNullOrEmpty(clientIPAddress)) { return new RServiceResult(null, "client ip address is empty"); } if (string.IsNullOrEmpty(clientAppName)) { return new RServiceResult(null, "client app name is empty"); } RAppUser rAppUser = await _userManager.FindByEmailAsync(email); if (rAppUser == null) { return new RServiceResult(null, "کاربر مورد نظر یافت نشد"); } var oldSecrets = await _context.VerifyQueueItems.Where(i => i.DateTime < DateTime.Now.AddDays(-1)).ToListAsync(); if (oldSecrets.Count > 0) { _context.VerifyQueueItems.RemoveRange(oldSecrets); await _context.SaveChangesAsync(); } //checking this queue for previous signup attempts is unnecessary and is not done intentionally RVerifyQueueItem item = new RVerifyQueueItem() { QueueType = RVerifyQueueType.ForgotPassword, Email = email, DateTime = DateTime.Now, ClientIPAddress = clientIPAddress, ClientAppName = clientAppName, Secret = $"{(new Random(DateTime.Now.Millisecond)).Next(0, 99999)}".PadLeft(6, '0'), Language = langauge }; var existingSecrets = await _context.VerifyQueueItems.Where(i => i.Secret == item.Secret).ToListAsync(); if (existingSecrets.Count > 0) { _context.VerifyQueueItems.RemoveRange(existingSecrets); await _context.SaveChangesAsync(); } await _context.VerifyQueueItems.AddAsync ( item ); await _context.SaveChangesAsync(); return new RServiceResult(item); } /// /// reset password using email /// /// /// /// /// /// public virtual async Task> ResetPassword(string email, string secret, string password, string clientIPAddress) { //we ignore input model in automatic auditing to prevent loginng password data, so we would add a manual auditing to have enough data on login intrusion and ... REvent log = new REvent() { EventType = "AppUser/ResetPassword (POST)(Manual)", StartDate = DateTime.UtcNow, UserName = email, IpAddress = clientIPAddress }; _context.AuditLogs.Add(log); await _context.SaveChangesAsync(); RAppUser existingUser = await _userManager.FindByEmailAsync(email); if (existingUser == null) { return new RServiceResult(false, "کاربر مورد نظر با این آدرس ایمیل یافت نشد"); } if ( email != (await RetrieveEmailFromQueueSecret(RVerifyQueueType.ForgotPassword, secret)).Result ) { return new RServiceResult(false, "کلمه عبور اشتباه وارد شده است"); } foreach (var passwordValidator in _userManager.PasswordValidators) { var resPass = await passwordValidator.ValidateAsync(_userManager, existingUser, password); if (!resPass.Succeeded) { return new RServiceResult(false, ErrorsToString(resPass.Errors)); } } existingUser.PasswordHash = _userManager.PasswordHasher.HashPassword(existingUser, password); await _userManager.UpdateAsync(existingUser); RVerifyQueueItem[] failedQueue = await _context.VerifyQueueItems.Where(i => i.Email == email && i.QueueType == RVerifyQueueType.ForgotPassword).ToArrayAsync(); if (failedQueue.Length != 0) { _context.VerifyQueueItems.RemoveRange(failedQueue); } await _context.SaveChangesAsync(); return new RServiceResult(true); } /// /// Find User By Email /// /// /// public virtual async Task> FindUserByEmail(string email) { RAppUser appUser = await _userManager.FindByEmailAsync(email); if (appUser == null) return new RServiceResult(null); return new RServiceResult( new PublicRAppUser() { Id = appUser.Id, Username = appUser.UserName, Email = appUser.Email, FirstName = appUser.FirstName, SureName = appUser.SureName, PhoneNumber = appUser.PhoneNumber, RImageId = appUser.RImageId, Status = appUser.Status, NickName = appUser.NickName, Website = appUser.Website, Bio = appUser.Bio, EmailConfirmed = appUser.EmailConfirmed }); } /// /// delete tenant /// /// public RServiceResult DeleteTenant() { _context.DeleteDb(); return new RServiceResult(true); } /// /// EnsureDefaultUserExists /// /// private async Task> EnsureDefaultUserExists() { //If no user exists create default one if (_userManager.Users.Count() == 0) { RAppUser admin = new RAppUser() { UserName = "admin@ganjoor.net", FirstName = "راهبر", SureName = "سیستم", Email = "admin@ganjoor.net", PhoneNumber = "00989123456789", CreateDate = DateTime.Now, Status = RAppUserStatus.Active }; var identityResult = await _userManager.CreateAsync( admin, "Test!123" ); if (!identityResult.Succeeded) { return new RServiceResult(false, "Error creating default user : " + ErrorsToString(identityResult.Errors)); } if (!await _roleManager.RoleExistsAsync(_userRoleService.AdministratorRoleName)) { identityResult = await _roleManager.CreateAsync(new RAppRole(_userRoleService.AdministratorRoleName)); if (!identityResult.Succeeded) { return new RServiceResult(false, "Error creating Administrator role : " + ErrorsToString(identityResult.Errors)); } } identityResult = await _userManager.AddToRoleAsync(admin, _userRoleService.AdministratorRoleName); if (!identityResult.Succeeded) { return new RServiceResult(false, "Error adding admin to Administrator role : " + ErrorsToString(identityResult.Errors)); } } return new RServiceResult(true); } /// /// secret used for generating Jwt token /// public string TokenSecret { get { return $"{Configuration.GetSection("Security")["Secret"]}"; } } /// /// JWT Tokens Expiration Time Out /// public int DefaultTokenExpirationInSeconds { get { return int.Parse($"{Configuration.GetSection("Security")["DefaultTokenExpirationInSeconds"]}"); } } #region Internals #region Token Generation /// /// Token Generation /// /// /// /// /// private async Task> GenerateToken(string username, Guid userId, Guid sessionId) { var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return new RServiceResult(null, "کاربر مورد نظر یافت نشد"); var claims = new List { new Claim(ClaimTypes.Name, username), new Claim("UserId", userId.ToString()), new Claim("SessionId", sessionId.ToString()), }; var userClaims = await _userManager.GetClaimsAsync(user); claims.AddRange(userClaims); var userRoles = await _userManager.GetRolesAsync(user); foreach (var userRole in userRoles) { claims.Add(new Claim(ClaimTypes.Role, userRole)); var role = await _roleManager.FindByNameAsync(userRole); if (role != null) { var roleClaims = await _roleManager.GetClaimsAsync(role); foreach (Claim roleClaim in roleClaims) { claims.Add(roleClaim); } } } var token = new JwtSecurityToken( issuer: "Ganjoor", audience: "Everyone", claims: claims, notBefore: DateTime.UtcNow, expires: DateTime.UtcNow.AddSeconds(DefaultTokenExpirationInSeconds), signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenSecret)), SecurityAlgorithms.HmacSha256) ); return new RServiceResult(new JwtSecurityTokenHandler().WriteToken(token)); } /// /// Extract Information From Token /// /// /// /// /// public void ExtractTokenInfo(string token, out string username, out Guid userId, out Guid sessionId) { var principal = GetPrincipalFromExpiredToken(token); username = principal.Identity.Name; userId = new Guid(principal.Claims.FirstOrDefault(c => c.Type == "UserId").Value); sessionId = new Guid(principal.Claims.FirstOrDefault(c => c.Type == "SessionId").Value); } /// /// /// /// /// private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidAudience = "Everyone", ValidateIssuer = true, ValidIssuer = "Ganjoor", ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenSecret)), ValidateLifetime = false, //important ClockSkew = TimeSpan.Zero }; var tokenHandler = new JwtSecurityTokenHandler(); var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken); if (!(securityToken is JwtSecurityToken jwtSecurityToken) || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) throw new SecurityTokenException("Invalid token"); return principal; } #endregion #region Identity Errors conversion /// /// convert identity errors to string /// /// /// protected static string ErrorsToString(IEnumerable errors) { StringBuilder sb = new StringBuilder(); foreach (IdentityError error in errors) { sb.AppendLine(error.Description); } return sb.ToString(); } #endregion #endregion #region Signup/Forget password email related overridables /// /// Sign Up Email Subject /// /// /// subject /// /// public virtual string GetSignUpEmailSubject(string secretCode) { return $"Ganjoor SignUp Code:{secretCode}"; } /// /// Sign Up Email Html Content /// /// /// /// html content public virtual string GetSignUpEmailHtmlContent(string secretCode, string signupCallbackUrl) { if (string.IsNullOrEmpty(signupCallbackUrl)) return $"{signupCallbackUrl}?secret={secretCode}"; return $"لطفا {secretCode} را در صفحهٔ ثبت نام وارد کنید."; } /// /// Forgot Password Email Subject /// /// /// subject /// /// public virtual string GetForgotPasswordEmailSubject(string secretCode) { return $"Ganjoor Forgot Password Code:{secretCode}"; } /// /// Forgot Password Email Html Content /// /// /// /// html content public virtual string GetForgotPasswordEmailHtmlContent(string secretCode, string forgotPasswordCallbackUrl) { return $"{forgotPasswordCallbackUrl}?secret={secretCode}"; } #endregion /// /// Main Database context /// protected readonly RSecurityDbContext _context; /// /// Image File Service /// protected readonly IImageFileService _imageFileService; /// /// User Role Service /// protected readonly IUserRoleService _userRoleService; /// /// Identity User Manageer /// protected UserManager _userManager = null; /// /// Identity SignIn Manager /// protected SignInManager _signInManager = null; /// /// Identity Role Manager /// protected RoleManager _roleManager = null; /// /// secret generator /// protected readonly ISecretGenerator _secretGenerator; /// /// Configuration /// protected IConfiguration Configuration { get; } /// /// constructor /// /// /// /// /// /// /// /// /// public AppUserService( RSecurityDbContext context, UserManager userManager, SignInManager signInManager, RoleManager roleManager, ISecretGenerator secretGenerator, IImageFileService imageFileService, IUserRoleService userRoleService, IConfiguration configuration) { _context = context; _userManager = userManager; _signInManager = signInManager; _roleManager = roleManager; _secretGenerator = secretGenerator; _imageFileService = imageFileService; _userRoleService = userRoleService; Configuration = configuration; } } }