BuildFeed/Authentication/MongoAuth/MongoMembershipProvider.cs

430 lines
15 KiB
C#
Raw Normal View History

2016-08-19 20:45:52 +08:00
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Web.Security;
2016-08-19 20:45:52 +08:00
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
namespace MongoAuth
{
2015-11-09 22:13:36 +08:00
public class MongoMembershipProvider : MembershipProvider
{
2016-08-19 20:45:52 +08:00
private const string MEMBER_COLLECTION_NAME = "members";
2015-11-09 22:13:36 +08:00
private bool _enablePasswordReset = true;
private int _maxInvalidPasswordAttempts = 5;
private IMongoCollection<MongoMember> _memberCollection;
2015-11-09 22:13:36 +08:00
private int _minRequiredNonAlphanumericCharacters = 1;
private int _minRequriedPasswordLength = 12;
private int _passwordAttemptWindow = 60;
private bool _requiresUniqueEmail = true;
2015-11-09 22:13:36 +08:00
public override string ApplicationName { get; set; }
2015-11-09 22:13:36 +08:00
public override bool EnablePasswordReset => _enablePasswordReset;
2015-11-09 22:13:36 +08:00
public override bool EnablePasswordRetrieval => false;
2015-11-09 22:13:36 +08:00
public override int MaxInvalidPasswordAttempts => _maxInvalidPasswordAttempts;
2015-11-09 22:13:36 +08:00
public override int MinRequiredNonAlphanumericCharacters => _minRequiredNonAlphanumericCharacters;
2015-11-09 22:13:36 +08:00
public override int MinRequiredPasswordLength => _minRequriedPasswordLength;
2015-11-09 22:13:36 +08:00
public override int PasswordAttemptWindow => _passwordAttemptWindow;
2015-11-09 22:13:36 +08:00
public override MembershipPasswordFormat PasswordFormat => MembershipPasswordFormat.Hashed;
2015-11-09 22:13:36 +08:00
public override string PasswordStrengthRegularExpression => "";
2015-11-09 22:13:36 +08:00
public override bool RequiresQuestionAndAnswer => false;
2015-11-09 22:13:36 +08:00
public override bool RequiresUniqueEmail => _requiresUniqueEmail;
2015-11-09 22:13:36 +08:00
public override void Initialize(string name, NameValueCollection config)
{
if (config == null)
{
throw new ArgumentNullException(nameof(config));
}
2015-11-09 22:13:36 +08:00
base.Initialize(name, config);
2016-08-19 20:45:52 +08:00
_enablePasswordReset = TryReadBool(config["enablePasswordReset"], _enablePasswordReset);
_maxInvalidPasswordAttempts = TryReadInt(config["maxInvalidPasswordAttempts"], _maxInvalidPasswordAttempts);
_minRequiredNonAlphanumericCharacters = TryReadInt(config["minRequiredNonAlphanumericCharacters"], _minRequiredNonAlphanumericCharacters);
_minRequriedPasswordLength = TryReadInt(config["minRequriedPasswordLength"], _minRequriedPasswordLength);
_passwordAttemptWindow = TryReadInt(config["passwordAttemptWindow"], _passwordAttemptWindow);
_requiresUniqueEmail = TryReadBool(config["requiresUniqueEmail"], _requiresUniqueEmail);
MongoClientSettings settings = new MongoClientSettings
2015-11-09 22:13:36 +08:00
{
Server = new MongoServerAddress(DatabaseConfig.Host, DatabaseConfig.Port)
};
if (!string.IsNullOrEmpty(DatabaseConfig.Username) && !string.IsNullOrEmpty(DatabaseConfig.Password))
{
settings.Credentials = new List<MongoCredential>
{
MongoCredential.CreateCredential(DatabaseConfig.Database, DatabaseConfig.Username, DatabaseConfig.Password)
};
}
MongoClient dbClient = new MongoClient(settings);
_memberCollection = dbClient.GetDatabase(DatabaseConfig.Database).GetCollection<MongoMember>(MEMBER_COLLECTION_NAME);
2015-11-09 22:13:36 +08:00
}
2015-11-09 22:13:36 +08:00
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
bool isAuthenticated = ValidateUser(username, oldPassword);
2015-11-09 22:13:36 +08:00
if (isAuthenticated)
{
2016-08-19 20:45:52 +08:00
Task<MongoMember> task = _memberCollection.Find(m => m.UserName.ToLower() == username.ToLower()).SingleOrDefaultAsync();
task.Wait();
2016-08-19 20:45:52 +08:00
MongoMember mm = task.Result;
2015-11-09 22:13:36 +08:00
if (mm == null)
{
2015-11-09 22:13:36 +08:00
return false;
}
var salt = new byte[24];
2016-08-19 20:45:52 +08:00
byte[] hash = CalculateHash(newPassword, ref salt);
2015-11-09 22:13:36 +08:00
mm.PassSalt = salt;
mm.PassHash = hash;
2016-08-19 20:45:52 +08:00
Task<ReplaceOneResult> replaceTask = _memberCollection.ReplaceOneAsync(m => m.Id == mm.Id, mm);
2015-11-09 22:13:36 +08:00
replaceTask.Wait();
return replaceTask.IsCompleted;
}
return false;
}
public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
if (password.Length < MinRequiredPasswordLength)
{
status = MembershipCreateStatus.InvalidPassword;
return null;
}
MembershipUser mu = null;
2016-08-19 20:45:52 +08:00
Task<long> dupeUsers = _memberCollection.Find(m => m.UserName.ToLower() == username.ToLower()).CountAsync();
Task<long> dupeEmails = _memberCollection.Find(m => m.EmailAddress.ToLower() == email.ToLower()).CountAsync();
2015-11-09 22:13:36 +08:00
dupeUsers.Wait();
dupeEmails.Wait();
if (dupeUsers.Result > 0)
{
status = MembershipCreateStatus.DuplicateUserName;
}
else if (dupeEmails.Result > 0)
{
status = MembershipCreateStatus.DuplicateEmail;
}
else
{
var salt = new byte[24];
2016-08-19 20:45:52 +08:00
byte[] hash = CalculateHash(password, ref salt);
2016-08-19 20:45:52 +08:00
MongoMember mm = new MongoMember
2015-11-09 22:13:36 +08:00
{
Id = Guid.NewGuid(),
UserName = username,
PassHash = hash,
PassSalt = salt,
EmailAddress = email,
IsApproved = false,
IsLockedOut = false,
CreationDate = DateTime.Now,
LastLoginDate = DateTime.MinValue,
LastActivityDate = DateTime.MinValue,
LastLockoutDate = DateTime.MinValue
};
2016-08-19 20:45:52 +08:00
Task insertTask = _memberCollection.InsertOneAsync(mm);
2015-11-09 22:13:36 +08:00
insertTask.Wait();
if (insertTask.Status == TaskStatus.RanToCompletion)
{
status = MembershipCreateStatus.Success;
2016-08-19 20:45:52 +08:00
mu = new MembershipUser(Name, mm.UserName, mm.Id, mm.EmailAddress, "", "", mm.IsApproved, mm.IsLockedOut, mm.CreationDate, mm.LastLoginDate, mm.LastActivityDate, DateTime.MinValue, mm.LastLockoutDate);
2015-11-09 22:13:36 +08:00
}
else
{
2015-11-09 22:13:36 +08:00
status = MembershipCreateStatus.ProviderError;
}
2015-11-09 22:13:36 +08:00
}
return mu;
}
public override bool DeleteUser(string username, bool deleteAllRelatedData)
{
2016-08-19 20:45:52 +08:00
Task<DeleteResult> task = _memberCollection.DeleteOneAsync(m => m.UserName.ToLower() == username.ToLower());
2015-11-09 22:13:36 +08:00
task.Wait();
return task.Result.IsAcknowledged && (task.Result.DeletedCount == 1);
2015-11-09 22:13:36 +08:00
}
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
MembershipUserCollection muc = new MembershipUserCollection();
2016-08-19 20:45:52 +08:00
IFindFluent<MongoMember, MongoMember> users = _memberCollection.Find(new BsonDocument());
2015-11-09 22:13:36 +08:00
2016-08-19 20:45:52 +08:00
Task<long> totalRecordsTask = users.CountAsync();
2015-11-09 22:13:36 +08:00
totalRecordsTask.Wait();
totalRecords = Convert.ToInt32(totalRecordsTask.Result);
2016-08-19 20:45:52 +08:00
users = users.Skip(pageIndex * pageSize).Limit(pageSize);
Task<List<MongoMember>> pageItemsTask = users.ToListAsync();
2015-11-09 22:13:36 +08:00
pageItemsTask.Wait();
2016-08-19 20:45:52 +08:00
foreach (MongoMember mm in pageItemsTask.Result)
2015-11-09 22:13:36 +08:00
{
2016-08-19 20:45:52 +08:00
muc.Add(new MembershipUser(Name, mm.UserName, mm.Id, mm.EmailAddress, "", "", mm.IsApproved, mm.IsLockedOut, mm.CreationDate, mm.LastLoginDate, mm.LastActivityDate, DateTime.MinValue, mm.LastLockoutDate));
2015-11-09 22:13:36 +08:00
}
return muc;
}
public override int GetNumberOfUsersOnline()
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override string GetPassword(string username, string answer)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override MembershipUser GetUser(string username, bool userIsOnline)
{
2016-08-19 20:45:52 +08:00
Task<MongoMember> task = _memberCollection.Find(f => f.UserName.ToLower() == username.ToLower()).FirstOrDefaultAsync();
2015-11-09 22:13:36 +08:00
task.Wait();
2016-08-19 20:45:52 +08:00
MongoMember mm = task.Result;
2015-11-09 22:13:36 +08:00
2016-08-19 20:45:52 +08:00
return mm == null
? null
: new MembershipUser(Name, mm.UserName, mm.Id, mm.EmailAddress, "", "", mm.IsApproved, mm.IsLockedOut, mm.CreationDate, mm.LastLoginDate, mm.LastActivityDate, DateTime.MinValue, mm.LastLockoutDate);
2015-11-09 22:13:36 +08:00
}
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
2016-08-19 20:45:52 +08:00
Task<MongoMember> task = _memberCollection.Find(f => f.Id == (Guid)providerUserKey).FirstOrDefaultAsync();
2015-11-09 22:13:36 +08:00
task.Wait();
2016-08-19 20:45:52 +08:00
MongoMember mm = task.Result;
2015-11-09 22:13:36 +08:00
2016-08-19 20:45:52 +08:00
return mm == null
? null
: new MembershipUser(Name, mm.UserName, mm.Id, mm.EmailAddress, "", "", mm.IsApproved, mm.IsLockedOut, mm.CreationDate, mm.LastLoginDate, mm.LastActivityDate, DateTime.MinValue, mm.LastLockoutDate);
2015-11-09 22:13:36 +08:00
}
public override string GetUserNameByEmail(string email)
{
2016-08-19 20:45:52 +08:00
Task<MongoMember> task = _memberCollection.Find(f => f.EmailAddress.ToLower() == email.ToLower()).FirstOrDefaultAsync();
2015-11-09 22:13:36 +08:00
task.Wait();
return task.Result.UserName;
}
public override string ResetPassword(string username, string answer)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
2016-08-19 20:45:52 +08:00
public void ChangeApproval(Guid id, bool newStatus)
2015-11-09 22:13:36 +08:00
{
2016-08-19 20:45:52 +08:00
Task<UpdateResult> task = _memberCollection.UpdateOneAsync(Builders<MongoMember>.Filter.Eq(u => u.Id, id), Builders<MongoMember>.Update.Set(u => u.IsApproved, newStatus));
2015-11-09 22:13:36 +08:00
task.Wait();
}
2016-08-19 20:45:52 +08:00
public void ChangeLockStatus(Guid id, bool newStatus)
2015-11-09 22:13:36 +08:00
{
var updateDefinition = new List<UpdateDefinition<MongoMember>>
{
Builders<MongoMember>.Update.Set(u => u.IsLockedOut, newStatus)
};
2015-11-09 22:13:36 +08:00
if (newStatus)
{
updateDefinition.Add(Builders<MongoMember>.Update.Set(u => u.LastLockoutDate, DateTime.Now));
}
else
{
updateDefinition.Add(Builders<MongoMember>.Update.Set(u => u.LockoutWindowAttempts, 0));
updateDefinition.Add(Builders<MongoMember>.Update.Set(u => u.LastLockoutDate, DateTime.MinValue));
}
2016-08-19 20:45:52 +08:00
Task<UpdateResult> task = _memberCollection.UpdateOneAsync(Builders<MongoMember>.Filter.Eq(u => u.Id, id), Builders<MongoMember>.Update.Combine(updateDefinition));
2015-11-09 22:13:36 +08:00
task.Wait();
}
public override bool UnlockUser(string userName)
{
2016-08-19 20:45:52 +08:00
Task<UpdateResult> task = _memberCollection.UpdateOneAsync(Builders<MongoMember>.Filter.Eq(m => m.UserName.ToLower(), userName.ToLower()), Builders<MongoMember>.Update.Set(m => m.IsLockedOut, false));
2015-11-09 22:13:36 +08:00
task.Wait();
return task.Result.IsAcknowledged && (task.Result.ModifiedCount == 1);
2015-11-09 22:13:36 +08:00
}
public override void UpdateUser(MembershipUser user)
{
throw new NotImplementedException();
}
2015-11-09 22:13:36 +08:00
public override bool ValidateUser(string username, string password)
{
2016-08-19 20:45:52 +08:00
Task<MongoMember> task = _memberCollection.Find(f => f.UserName.ToLower() == username.ToLower()).FirstOrDefaultAsync();
2015-11-09 22:13:36 +08:00
task.Wait();
2016-08-19 20:45:52 +08:00
MongoMember mm = task.Result;
2015-11-09 22:13:36 +08:00
if ((mm == null)
2016-08-19 20:45:52 +08:00
|| !(mm.IsApproved && !mm.IsLockedOut))
2015-11-09 22:13:36 +08:00
{
return false;
}
2015-11-09 22:13:36 +08:00
byte[] salt = mm.PassSalt;
2016-08-19 20:45:52 +08:00
byte[] hash = CalculateHash(password, ref salt);
2015-11-09 22:13:36 +08:00
bool isFail = false;
2015-11-09 22:13:36 +08:00
for (int i = 0; i > hash.Length; i++)
{
2016-08-19 20:45:52 +08:00
isFail |= hash[i] != mm.PassHash[i];
2015-11-09 22:13:36 +08:00
}
2015-11-09 22:13:36 +08:00
if (isFail)
{
if (mm.LockoutWindowStart == DateTime.MinValue)
{
2015-11-09 22:13:36 +08:00
mm.LockoutWindowStart = DateTime.Now;
mm.LockoutWindowAttempts = 1;
}
else
{
2015-11-09 22:13:36 +08:00
if (mm.LockoutWindowStart.AddMinutes(PasswordAttemptWindow) > DateTime.Now)
{
// still within window
mm.LockoutWindowAttempts++;
if (mm.LockoutWindowAttempts >= MaxInvalidPasswordAttempts)
{
mm.IsLockedOut = true;
}
}
else
{
// outside of window, reset
mm.LockoutWindowStart = DateTime.Now;
mm.LockoutWindowAttempts = 1;
}
}
2015-11-09 22:13:36 +08:00
}
else
{
mm.LastLoginDate = DateTime.Now;
mm.LockoutWindowStart = DateTime.MinValue;
mm.LockoutWindowAttempts = 0;
}
2016-08-19 20:45:52 +08:00
Task<ReplaceOneResult> updTask = _memberCollection.ReplaceOneAsync(Builders<MongoMember>.Filter.Eq(u => u.Id, mm.Id), mm);
2015-11-09 22:13:36 +08:00
updTask.Wait();
return !isFail;
}
2016-08-19 20:45:52 +08:00
private static byte[] CalculateHash(string password, ref byte[] salt)
2015-11-09 22:13:36 +08:00
{
if (!salt.Any(v => v != 0))
{
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
rng.GetBytes(salt);
}
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
var hashPlaintext = new byte[salt.Length + passwordBytes.Length];
2015-11-09 22:13:36 +08:00
passwordBytes.CopyTo(hashPlaintext, 0);
salt.CopyTo(hashPlaintext, passwordBytes.Length);
SHA512CryptoServiceProvider sha = new SHA512CryptoServiceProvider();
byte[] hash = sha.ComputeHash(hashPlaintext);
return hash;
}
2016-08-19 20:45:52 +08:00
private static bool TryReadBool(string config, bool defaultValue)
2015-11-09 22:13:36 +08:00
{
2016-08-19 20:45:52 +08:00
bool temp;
2015-11-09 22:13:36 +08:00
bool success = bool.TryParse(config, out temp);
2016-08-19 20:45:52 +08:00
return success
? temp
: defaultValue;
2015-11-09 22:13:36 +08:00
}
2016-08-19 20:45:52 +08:00
private static int TryReadInt(string config, int defaultValue)
2015-11-09 22:13:36 +08:00
{
2016-08-19 20:45:52 +08:00
int temp;
2015-11-09 22:13:36 +08:00
bool success = int.TryParse(config, out temp);
2016-08-19 20:45:52 +08:00
return success
? temp
: defaultValue;
2015-11-09 22:13:36 +08:00
}
}
public class MongoMember
{
[BsonId]
public Guid Id { get; set; }
public string UserName { get; set; }
public byte[] PassHash { get; set; }
public byte[] PassSalt { get; set; }
public string EmailAddress { get; set; }
public bool IsApproved { get; set; }
public bool IsLockedOut { get; set; }
public DateTime CreationDate { get; set; }
public DateTime LastActivityDate { get; set; }
public DateTime LastLockoutDate { get; set; }
public DateTime LastLoginDate { get; set; }
public DateTime LockoutWindowStart { get; set; }
public int LockoutWindowAttempts { get; set; }
}
2016-08-19 20:45:52 +08:00
}