using System; using System.Collections.Generic; using System.Linq; using LaDOSE.Business.Interface; using LaDOSE.Entity; using LaDOSE.Entity.Context; using Microsoft.EntityFrameworkCore; namespace LaDOSE.Business.Service { public class UserService : IUserService { private LaDOSEDbContext _context; public UserService(LaDOSEDbContext context) { _context = context; } public ApplicationUser Authenticate(string username, string password) { if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password)) return null; var user = _context.ApplicationUser .Include(x => x.UserRoles).ThenInclude(ur => ur.Role) .SingleOrDefault(x => x.Username == username); // check if username exists if (user == null) return null; // check if password is correct if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt)) return null; // authentication successful return user; } public IEnumerable GetAll() { return _context.ApplicationUser .Include(x => x.UserRoles).ThenInclude(ur => ur.Role) .ToList(); } /// /// Roles are included because authorization reads them on every authenticated /// request (see the OnTokenValidated handler in Startup). /// public ApplicationUser GetById(int id) { return _context.ApplicationUser .Include(x => x.UserRoles).ThenInclude(ur => ur.Role) .SingleOrDefault(x => x.Id == id); } public IEnumerable GetAllRoles() { return _context.ApplicationRole.OrderBy(x => x.Name).ToList(); } public ApplicationUser Create(ApplicationUser user, string password, IEnumerable roleNames = null) { // validation if (string.IsNullOrWhiteSpace(user?.Username)) throw new Exception("Username is required"); if (string.IsNullOrWhiteSpace(password)) throw new Exception("Password is required"); if (_context.ApplicationUser.Any(x => x.Username == user.Username)) throw new Exception("Username \"" + user.Username + "\" is already taken"); // EF fills userid/roleid on the join rows from these navigations when it saves. user.UserRoles = ResolveRoles(roleNames) .Select(role => new ApplicationUserRole { Role = role }) .ToList(); byte[] passwordHash, passwordSalt; CreatePasswordHash(password, out passwordHash, out passwordSalt); user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; _context.ApplicationUser.Add(user); _context.SaveChanges(); return user; } /// /// Turns role names into the existing rows of applicationrole. An unknown /// name is an error rather than a new role, so a typo cannot quietly produce an /// account with no privileges — or, worse, a second spelling of "Admin". /// private List ResolveRoles(IEnumerable roleNames) { var wanted = (roleNames ?? Enumerable.Empty()) .Where(name => !string.IsNullOrWhiteSpace(name)) .Select(name => name.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (wanted.Count == 0) return new List(); var known = _context.ApplicationRole.ToList(); var resolved = new List(); foreach (var name in wanted) { var role = known.FirstOrDefault(r => string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase)); if (role == null) throw new Exception($"Unknown role \"{name}\""); resolved.Add(role); } return resolved; } public void Update(ApplicationUser userParam, string password = null) { var user = _context.ApplicationUser.Find(userParam.Id); if (user == null) throw new Exception("User not found"); if (userParam.Username != user.Username) { // username has changed so check if the new username is already taken if (_context.ApplicationUser.Any(x => x.Username == userParam.Username)) throw new Exception("Username " + userParam.Username + " is already taken"); } // update user properties user.FirstName = userParam.FirstName; user.LastName = userParam.LastName; user.Username = userParam.Username; // update password if it was entered if (!string.IsNullOrWhiteSpace(password)) { byte[] passwordHash, passwordSalt; CreatePasswordHash(password, out passwordHash, out passwordSalt); user.PasswordHash = passwordHash; user.PasswordSalt = passwordSalt; } _context.ApplicationUser.Update(user); _context.SaveChanges(); } public void Delete(int id) { // applicationuserrole's foreign keys are ON DELETE RESTRICT, so the join rows // have to go first — clearing the collection makes EF delete them. var user = _context.ApplicationUser .Include(x => x.UserRoles).ThenInclude(ur => ur.Role) .SingleOrDefault(x => x.Id == id); if (user == null) return; user.UserRoles?.Clear(); _context.ApplicationUser.Remove(user); _context.SaveChanges(); } // private helper methods private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt) { if (password == null) throw new ArgumentNullException("password"); if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password"); using (var hmac = new System.Security.Cryptography.HMACSHA512()) { passwordSalt = hmac.Key; passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); } } private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt) { if (password == null) throw new ArgumentNullException("password"); if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password"); if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash"); if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash"); using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt)) { var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password)); for (int i = 0; i < computedHash.Length; i++) { if (computedHash[i] != storedHash[i]) return false; } } return true; } } }