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:
loudej
2009-11-17 05:52:23 +00:00
parent 0cc64ebe1c
commit e225650203
10 changed files with 195 additions and 10 deletions

View File

@@ -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;

View File

@@ -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);
}
} }
} }

View File

@@ -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 });
} }

View File

@@ -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; }
} }
} }

View File

@@ -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" />

View File

@@ -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();
} }
} }
} }

View File

@@ -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; }
} }
} }

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {