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.UI.Notify;
|
||||||
using Orchard.Users.Controllers;
|
using Orchard.Users.Controllers;
|
||||||
using Orchard.Users.Models;
|
using Orchard.Users.Models;
|
||||||
|
using Orchard.Users.Services;
|
||||||
using Orchard.Users.ViewModels;
|
using Orchard.Users.ViewModels;
|
||||||
|
|
||||||
namespace Orchard.Tests.Packages.Users.Controllers {
|
namespace Orchard.Tests.Packages.Users.Controllers {
|
||||||
@@ -26,6 +27,7 @@ namespace Orchard.Tests.Packages.Users.Controllers {
|
|||||||
public override void Register(ContainerBuilder builder) {
|
public override void Register(ContainerBuilder builder) {
|
||||||
builder.Register<AdminController>();
|
builder.Register<AdminController>();
|
||||||
builder.Register<DefaultModelManager>().As<IModelManager>();
|
builder.Register<DefaultModelManager>().As<IModelManager>();
|
||||||
|
builder.Register<MembershipService>().As<IMembershipService>();
|
||||||
builder.Register<UserDriver>().As<IModelDriver>();
|
builder.Register<UserDriver>().As<IModelDriver>();
|
||||||
builder.Register(new Mock<INotifier>().Object);
|
builder.Register(new Mock<INotifier>().Object);
|
||||||
}
|
}
|
||||||
@@ -76,7 +78,7 @@ namespace Orchard.Tests.Packages.Users.Controllers {
|
|||||||
[Test]
|
[Test]
|
||||||
public void CreateShouldAddUserAndRedirect() {
|
public void CreateShouldAddUserAndRedirect() {
|
||||||
var controller = _container.Resolve<AdminController>();
|
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>());
|
Assert.That(result, Is.TypeOf<RedirectToRouteResult>());
|
||||||
|
|
||||||
var redirect = (RedirectToRouteResult)result;
|
var redirect = (RedirectToRouteResult)result;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Web.Security;
|
||||||
|
using Autofac;
|
||||||
using Autofac.Builder;
|
using Autofac.Builder;
|
||||||
using Autofac.Modules;
|
using Autofac.Modules;
|
||||||
using NHibernate;
|
using NHibernate;
|
||||||
@@ -20,6 +22,7 @@ namespace Orchard.Tests.Packages.Users.Services {
|
|||||||
private IMembershipService _membershipService;
|
private IMembershipService _membershipService;
|
||||||
private ISessionFactory _sessionFactory;
|
private ISessionFactory _sessionFactory;
|
||||||
private ISession _session;
|
private ISession _session;
|
||||||
|
private IContainer _container;
|
||||||
|
|
||||||
|
|
||||||
public class TestSessionLocator : ISessionLocator {
|
public class TestSessionLocator : ISessionLocator {
|
||||||
@@ -59,8 +62,8 @@ namespace Orchard.Tests.Packages.Users.Services {
|
|||||||
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
|
builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>));
|
||||||
_session = _sessionFactory.OpenSession();
|
_session = _sessionFactory.OpenSession();
|
||||||
builder.Register(new TestSessionLocator(_session)).As<ISessionLocator>();
|
builder.Register(new TestSessionLocator(_session)).As<ISessionLocator>();
|
||||||
var container = builder.Build();
|
_container = builder.Build();
|
||||||
_membershipService = container.Resolve<IMembershipService>();
|
_membershipService = _container.Resolve<IMembershipService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -69,5 +72,52 @@ namespace Orchard.Tests.Packages.Users.Services {
|
|||||||
Assert.That(user.UserName, Is.EqualTo("a"));
|
Assert.That(user.UserName, Is.EqualTo("a"));
|
||||||
Assert.That(user.Email, Is.EqualTo("c"));
|
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 {
|
namespace Orchard.Users.Controllers {
|
||||||
|
|
||||||
public class AdminController : Controller, IModelUpdater {
|
public class AdminController : Controller, IModelUpdater {
|
||||||
|
private readonly IMembershipService _membershipService;
|
||||||
private readonly IModelManager _modelManager;
|
private readonly IModelManager _modelManager;
|
||||||
private readonly IRepository<UserRecord> _userRepository;
|
private readonly IRepository<UserRecord> _userRepository;
|
||||||
private readonly INotifier _notifier;
|
private readonly INotifier _notifier;
|
||||||
|
|
||||||
public AdminController(
|
public AdminController(
|
||||||
|
IMembershipService membershipService,
|
||||||
IModelManager modelManager,
|
IModelManager modelManager,
|
||||||
IRepository<UserRecord> userRepository,
|
IRepository<UserRecord> userRepository,
|
||||||
INotifier notifier) {
|
INotifier notifier) {
|
||||||
|
_membershipService = membershipService;
|
||||||
_modelManager = modelManager;
|
_modelManager = modelManager;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
_notifier = notifier;
|
_notifier = notifier;
|
||||||
@@ -49,12 +52,17 @@ namespace Orchard.Users.Controllers {
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public ActionResult Create(UserCreateViewModel model) {
|
public ActionResult Create(UserCreateViewModel model) {
|
||||||
|
if (model.Password != model.ConfirmPassword) {
|
||||||
|
ModelState.AddModelError("ConfirmPassword", T("Password confirmation must match").ToString());
|
||||||
|
}
|
||||||
if (ModelState.IsValid == false) {
|
if (ModelState.IsValid == false) {
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
var user = _modelManager.New("user");
|
var user = _membershipService.CreateUser(new CreateUserParams(
|
||||||
user.As<UserModel>().Record = new UserRecord { UserName = model.UserName, Email = model.Email };
|
model.UserName,
|
||||||
_modelManager.Create(user);
|
model.Password,
|
||||||
|
model.Email,
|
||||||
|
null, null, true));
|
||||||
return RedirectToAction("edit", new { user.Id });
|
return RedirectToAction("edit", new { user.Id });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
using System.Web.Security;
|
||||||
using Orchard.Models.Records;
|
using Orchard.Models.Records;
|
||||||
|
|
||||||
namespace Orchard.Users.Models {
|
namespace Orchard.Users.Models {
|
||||||
public class UserRecord : ModelPartRecord {
|
public class UserRecord : ModelPartRecord {
|
||||||
public virtual string UserName { get; set; }
|
public virtual string UserName { get; set; }
|
||||||
public virtual string Email { 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="Package.txt" />
|
||||||
<Content Include="Views\Admin\Edit.aspx" />
|
<Content Include="Views\Admin\Edit.aspx" />
|
||||||
<Content Include="Views\Admin\Create.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\UserEditViewModel.ascx" />
|
||||||
<Content Include="Views\Admin\EditorTemplates\inputTextLarge.ascx" />
|
<Content Include="Views\Admin\EditorTemplates\inputTextLarge.ascx" />
|
||||||
<Content Include="Views\Admin\EditorTemplates\UserCreateViewModel.ascx" />
|
<Content Include="Views\Admin\EditorTemplates\UserCreateViewModel.ascx" />
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Web.Security;
|
||||||
using Orchard.Data;
|
using Orchard.Data;
|
||||||
using Orchard.Logging;
|
using Orchard.Logging;
|
||||||
using Orchard.Models;
|
using Orchard.Models;
|
||||||
@@ -26,11 +30,14 @@ namespace Orchard.Users.Services {
|
|||||||
|
|
||||||
public IUser CreateUser(CreateUserParams createUserParams) {
|
public IUser CreateUser(CreateUserParams createUserParams) {
|
||||||
Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email);
|
Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email);
|
||||||
var user = _modelManager.New("user");
|
var record = new UserRecord {
|
||||||
user.As<UserModel>().Record = new UserRecord {
|
|
||||||
UserName = createUserParams.Username,
|
UserName = createUserParams.Username,
|
||||||
Email = createUserParams.Email
|
Email = createUserParams.Email
|
||||||
};
|
};
|
||||||
|
SetPassword(record, createUserParams.Password);
|
||||||
|
|
||||||
|
var user = _modelManager.New("user");
|
||||||
|
user.As<UserModel>().Record = record;
|
||||||
_modelManager.Create(user);
|
_modelManager.Create(user);
|
||||||
return user.As<IUser>();
|
return user.As<IUser>();
|
||||||
}
|
}
|
||||||
@@ -44,7 +51,104 @@ namespace Orchard.Users.Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IUser ValidateUser(string username, string password) {
|
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]
|
[Required]
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required, DataType(DataType.EmailAddress)]
|
||||||
public string Email { get; set; }
|
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>
|
<ol>
|
||||||
<%=Html.EditorFor(m=>m.UserName, "inputTextLarge") %>
|
<%=Html.EditorFor(m=>m.UserName, "inputTextLarge") %>
|
||||||
<%=Html.EditorFor(m=>m.Email, "inputTextLarge") %>
|
<%=Html.EditorFor(m=>m.Email, "inputTextLarge") %>
|
||||||
|
<%=Html.EditorFor(m=>m.Password, "inputPasswordLarge") %>
|
||||||
|
<%=Html.EditorFor(m=>m.ConfirmPassword, "inputPasswordLarge") %>
|
||||||
</ol>
|
</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 GetUser(string username);
|
||||||
|
|
||||||
IUser ValidateUser(string username, string password);
|
IUser ValidateUser(string username, string password);
|
||||||
|
void SetPassword(IUser user, string password);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MembershipSettings {
|
public class MembershipSettings {
|
||||||
|
|||||||
Reference in New Issue
Block a user