mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-11-28 09:22:55 +08:00
Adding password storage and verification. Clear and hashed supported. Hashed is default, uses SHA1 with unique salt per user.
--HG-- extra : convert_revision : svn%3A5ff7c347-ad56-4c35-b696-ccb81de16e03/trunk%4040935
This commit is contained in:
@@ -16,6 +16,7 @@ using Orchard.Security;
|
||||
using Orchard.UI.Notify;
|
||||
using Orchard.Users.Controllers;
|
||||
using Orchard.Users.Models;
|
||||
using Orchard.Users.Services;
|
||||
using Orchard.Users.ViewModels;
|
||||
|
||||
namespace Orchard.Tests.Packages.Users.Controllers {
|
||||
@@ -26,6 +27,7 @@ namespace Orchard.Tests.Packages.Users.Controllers {
|
||||
public override void Register(ContainerBuilder builder) {
|
||||
builder.Register<AdminController>();
|
||||
builder.Register<DefaultModelManager>().As<IModelManager>();
|
||||
builder.Register<MembershipService>().As<IMembershipService>();
|
||||
builder.Register<UserDriver>().As<IModelDriver>();
|
||||
builder.Register(new Mock<INotifier>().Object);
|
||||
}
|
||||
@@ -76,7 +78,7 @@ namespace Orchard.Tests.Packages.Users.Controllers {
|
||||
[Test]
|
||||
public void CreateShouldAddUserAndRedirect() {
|
||||
var controller = _container.Resolve<AdminController>();
|
||||
var result = controller.Create(new UserCreateViewModel { UserName = "four" });
|
||||
var result = controller.Create(new UserCreateViewModel { UserName = "four",Password="five",ConfirmPassword="five" });
|
||||
Assert.That(result, Is.TypeOf<RedirectToRouteResult>());
|
||||
|
||||
var redirect = (RedirectToRouteResult)result;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web.Security;
|
||||
using Autofac;
|
||||
using Autofac.Builder;
|
||||
using Autofac.Modules;
|
||||
using NHibernate;
|
||||
@@ -20,6 +22,7 @@ namespace Orchard.Tests.Packages.Users.Services {
|
||||
private IMembershipService _membershipService;
|
||||
private ISessionFactory _sessionFactory;
|
||||
private ISession _session;
|
||||
private IContainer _container;
|
||||
|
||||
|
||||
public class TestSessionLocator : ISessionLocator {
|
||||
@@ -59,8 +62,8 @@ namespace Orchard.Tests.Packages.Users.Services {
|
||||
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
|
||||
_session = _sessionFactory.OpenSession();
|
||||
builder.Register(new TestSessionLocator(_session)).As<ISessionLocator>();
|
||||
var container = builder.Build();
|
||||
_membershipService = container.Resolve<IMembershipService>();
|
||||
_container = builder.Build();
|
||||
_membershipService = _container.Resolve<IMembershipService>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -69,5 +72,52 @@ namespace Orchard.Tests.Packages.Users.Services {
|
||||
Assert.That(user.UserName, Is.EqualTo("a"));
|
||||
Assert.That(user.Email, Is.EqualTo("c"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DefaultPasswordFormatShouldBeHashedAndHaveSalt() {
|
||||
var user = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
|
||||
var userRepository = _container.Resolve<IRepository<UserRecord>>();
|
||||
var userRecord = userRepository.Get(user.Id);
|
||||
Assert.That(userRecord.PasswordFormat, Is.EqualTo(MembershipPasswordFormat.Hashed));
|
||||
Assert.That(userRecord.Password, Is.Not.EqualTo("b"));
|
||||
Assert.That(userRecord.PasswordSalt, Is.Not.Null);
|
||||
Assert.That(userRecord.PasswordSalt, Is.Not.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SaltAndPasswordShouldBeDifferentEvenWithSameSourcePassword() {
|
||||
var user1 = _membershipService.CreateUser(new CreateUserParams("a", "b", "c", null, null, true));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
var user2 = _membershipService.CreateUser(new CreateUserParams("d", "b", "e", null, null, true));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
var userRepository = _container.Resolve<IRepository<UserRecord>>();
|
||||
var user1Record = userRepository.Get(user1.Id);
|
||||
var user2Record = userRepository.Get(user2.Id);
|
||||
Assert.That(user1Record.PasswordSalt, Is.Not.EqualTo(user2Record.PasswordSalt));
|
||||
Assert.That(user1Record.Password, Is.Not.EqualTo(user2Record.Password));
|
||||
|
||||
Assert.That(_membershipService.ValidateUser("a", "b"), Is.Not.Null);
|
||||
Assert.That(_membershipService.ValidateUser("d", "b"), Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ValidateUserShouldReturnNullIfUserOrPasswordIsIncorrect() {
|
||||
_membershipService.CreateUser(new CreateUserParams("test-user", "test-password", "c", null, null, true));
|
||||
_session.Flush();
|
||||
_session.Clear();
|
||||
|
||||
var validate1 = _membershipService.ValidateUser("test-user", "bad-password");
|
||||
var validate2 = _membershipService.ValidateUser("bad-user", "test-password");
|
||||
var validate3 = _membershipService.ValidateUser("test-user", "test-password");
|
||||
|
||||
Assert.That(validate1, Is.Null);
|
||||
Assert.That(validate2, Is.Null);
|
||||
Assert.That(validate3, Is.Not.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,17 @@ using Orchard.Users.ViewModels;
|
||||
namespace Orchard.Users.Controllers {
|
||||
|
||||
public class AdminController : Controller, IModelUpdater {
|
||||
private readonly IMembershipService _membershipService;
|
||||
private readonly IModelManager _modelManager;
|
||||
private readonly IRepository<UserRecord> _userRepository;
|
||||
private readonly INotifier _notifier;
|
||||
|
||||
public AdminController(
|
||||
IMembershipService membershipService,
|
||||
IModelManager modelManager,
|
||||
IRepository<UserRecord> userRepository,
|
||||
INotifier notifier) {
|
||||
_membershipService = membershipService;
|
||||
_modelManager = modelManager;
|
||||
_userRepository = userRepository;
|
||||
_notifier = notifier;
|
||||
@@ -49,12 +52,17 @@ namespace Orchard.Users.Controllers {
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Create(UserCreateViewModel model) {
|
||||
if (model.Password != model.ConfirmPassword) {
|
||||
ModelState.AddModelError("ConfirmPassword", T("Password confirmation must match").ToString());
|
||||
}
|
||||
if (ModelState.IsValid == false) {
|
||||
return View(model);
|
||||
}
|
||||
var user = _modelManager.New("user");
|
||||
user.As<UserModel>().Record = new UserRecord { UserName = model.UserName, Email = model.Email };
|
||||
_modelManager.Create(user);
|
||||
var user = _membershipService.CreateUser(new CreateUserParams(
|
||||
model.UserName,
|
||||
model.Password,
|
||||
model.Email,
|
||||
null, null, true));
|
||||
return RedirectToAction("edit", new { user.Id });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
using System.Web.Security;
|
||||
using Orchard.Models.Records;
|
||||
|
||||
namespace Orchard.Users.Models {
|
||||
public class UserRecord : ModelPartRecord {
|
||||
public virtual string UserName { get; set; }
|
||||
public virtual string Email { get; set; }
|
||||
|
||||
public virtual string Password { get; set; }
|
||||
public virtual MembershipPasswordFormat PasswordFormat { get; set; }
|
||||
public virtual string PasswordSalt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
<Content Include="Package.txt" />
|
||||
<Content Include="Views\Admin\Edit.aspx" />
|
||||
<Content Include="Views\Admin\Create.aspx" />
|
||||
<Content Include="Views\Admin\EditorTemplates\inputPasswordLarge.ascx" />
|
||||
<Content Include="Views\Admin\EditorTemplates\UserEditViewModel.ascx" />
|
||||
<Content Include="Views\Admin\EditorTemplates\inputTextLarge.ascx" />
|
||||
<Content Include="Views\Admin\EditorTemplates\UserCreateViewModel.ascx" />
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web.Security;
|
||||
using Orchard.Data;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Models;
|
||||
@@ -26,11 +30,14 @@ namespace Orchard.Users.Services {
|
||||
|
||||
public IUser CreateUser(CreateUserParams createUserParams) {
|
||||
Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email);
|
||||
var user = _modelManager.New("user");
|
||||
user.As<UserModel>().Record = new UserRecord {
|
||||
var record = new UserRecord {
|
||||
UserName = createUserParams.Username,
|
||||
Email = createUserParams.Email
|
||||
};
|
||||
SetPassword(record, createUserParams.Password);
|
||||
|
||||
var user = _modelManager.New("user");
|
||||
user.As<UserModel>().Record = record;
|
||||
_modelManager.Create(user);
|
||||
return user.As<IUser>();
|
||||
}
|
||||
@@ -44,7 +51,104 @@ namespace Orchard.Users.Services {
|
||||
}
|
||||
|
||||
public IUser ValidateUser(string username, string password) {
|
||||
return GetUser(username);
|
||||
var userRecord = _userRepository.Get(x => x.UserName == username);
|
||||
if (userRecord == null || ValidatePassword(userRecord, password) == false)
|
||||
return null;
|
||||
|
||||
return _modelManager.Get(userRecord.Id).As<IUser>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void SetPassword(IUser user, string password) {
|
||||
if (!user.Is<UserModel>())
|
||||
throw new InvalidCastException();
|
||||
|
||||
var userRecord = user.As<UserModel>().Record;
|
||||
SetPassword(userRecord, password);
|
||||
}
|
||||
|
||||
|
||||
void SetPassword(UserRecord record, string password) {
|
||||
switch (GetSettings().PasswordFormat) {
|
||||
case MembershipPasswordFormat.Clear:
|
||||
SetPasswordClear(record, password);
|
||||
break;
|
||||
case MembershipPasswordFormat.Hashed:
|
||||
SetPasswordHashed(record, password);
|
||||
break;
|
||||
case MembershipPasswordFormat.Encrypted:
|
||||
SetPasswordEncrypted(record, password);
|
||||
break;
|
||||
default:
|
||||
throw new ApplicationException("Unexpected password format value");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidatePassword(UserRecord record, string password) {
|
||||
// Note - the password format stored with the record is used
|
||||
// otherwise changing the password format on the site would invalidate
|
||||
// all logins
|
||||
switch (record.PasswordFormat) {
|
||||
case MembershipPasswordFormat.Clear:
|
||||
return ValidatePasswordClear(record, password);
|
||||
case MembershipPasswordFormat.Hashed:
|
||||
return ValidatePasswordHashed(record, password);
|
||||
case MembershipPasswordFormat.Encrypted:
|
||||
return ValidatePasswordEncrypted(record, password);
|
||||
default:
|
||||
throw new ApplicationException("Unexpected password format value");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPasswordClear(UserRecord record, string password) {
|
||||
record.PasswordFormat = MembershipPasswordFormat.Clear;
|
||||
record.Password = password;
|
||||
record.PasswordSalt = null;
|
||||
}
|
||||
|
||||
private static bool ValidatePasswordClear(UserRecord record, string password) {
|
||||
return record.Password == password;
|
||||
}
|
||||
|
||||
private static void SetPasswordHashed(UserRecord record, string password) {
|
||||
|
||||
var saltBytes = new byte[0x10];
|
||||
var random = new RNGCryptoServiceProvider();
|
||||
random.GetBytes(saltBytes);
|
||||
|
||||
var passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||
|
||||
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
|
||||
|
||||
var hashAlgorithm = HashAlgorithm.Create("SHA1");
|
||||
var hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
|
||||
|
||||
record.PasswordFormat = MembershipPasswordFormat.Hashed;
|
||||
record.Password = Convert.ToBase64String(hashBytes);
|
||||
record.PasswordSalt = Convert.ToBase64String(saltBytes);
|
||||
}
|
||||
|
||||
private static bool ValidatePasswordHashed(UserRecord record, string password) {
|
||||
|
||||
var saltBytes = Convert.FromBase64String(record.PasswordSalt);
|
||||
|
||||
var passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||
|
||||
var combinedBytes = saltBytes.Concat(passwordBytes).ToArray();
|
||||
|
||||
var hashAlgorithm = HashAlgorithm.Create("SHA1");
|
||||
var hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
|
||||
|
||||
return record.Password == Convert.ToBase64String(hashBytes);
|
||||
}
|
||||
|
||||
private static void SetPasswordEncrypted(UserRecord record, string password) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static bool ValidatePasswordEncrypted(UserRecord record, string password) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,13 @@ namespace Orchard.Users.ViewModels {
|
||||
[Required]
|
||||
public string UserName { get; set; }
|
||||
|
||||
[Required]
|
||||
[Required, DataType(DataType.EmailAddress)]
|
||||
public string Email { get; set; }
|
||||
|
||||
[Required, DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
|
||||
[Required, DataType(DataType.Password)]
|
||||
public string ConfirmPassword { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,6 @@
|
||||
<ol>
|
||||
<%=Html.EditorFor(m=>m.UserName, "inputTextLarge") %>
|
||||
<%=Html.EditorFor(m=>m.Email, "inputTextLarge") %>
|
||||
<%=Html.EditorFor(m=>m.Password, "inputPasswordLarge") %>
|
||||
<%=Html.EditorFor(m=>m.ConfirmPassword, "inputPasswordLarge") %>
|
||||
</ol>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<string>" %>
|
||||
<li>
|
||||
<%=Html.LabelForModel() %>
|
||||
<%=Html.Password("",Model,new{@class="inputText inputTextLarge"}) %>
|
||||
<%=Html.ValidationMessage("","*")%>
|
||||
</li>
|
||||
@@ -8,6 +8,7 @@ namespace Orchard.Security {
|
||||
IUser GetUser(string username);
|
||||
|
||||
IUser ValidateUser(string username, string password);
|
||||
void SetPassword(IUser user, string password);
|
||||
}
|
||||
|
||||
public class MembershipSettings {
|
||||
|
||||
Reference in New Issue
Block a user