diff --git a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs index 18797432e..8ed59540f 100644 --- a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs +++ b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs @@ -26,7 +26,7 @@ namespace Orchard.Core.Messaging.Services { _channels = channels; } - public void Send(ContentItemRecord recipient, string type, string service = null) { + public void Send(ContentItemRecord recipient, string type, string service = null, Dictionary properties = null) { if ( !HasChannels() ) return; @@ -40,7 +40,7 @@ namespace Orchard.Core.Messaging.Services { try { // if the service is not explicit, use the default one, as per settings configuration - if ( String.IsNullOrWhiteSpace(service) ) { + if (String.IsNullOrWhiteSpace(service)) { service = messageSettings.DefaultChannelService; } @@ -50,6 +50,11 @@ namespace Orchard.Core.Messaging.Services { Service = service }; + if ( properties != null ) { + foreach (var key in properties.Keys) + context.Properties.Add(key, properties[key]); + } + _messageEventHandler.Sending(context); foreach ( var channel in _channels ) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 13e867d2f..87a18ec00 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -116,10 +116,16 @@ namespace Orchard.Users.Controllers { if (ValidateRegistration(userName, email, password, confirmPassword)) { // Attempt to register the user - var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, true)); - + var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false)); if (user != null) { + if ( user.As().EmailStatus == UserStatus.Pending ) { + string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As()); + _membershipService.SendChallengeEmail(user.As(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken }))); + + return RedirectToAction("ChallengeEmailSent"); + } + _authenticationService.SignIn(user, false /* createPersistentCookie */); return Redirect("~/"); } @@ -173,6 +179,21 @@ namespace Orchard.Users.Controllers { return View(new BaseViewModel()); } + public ActionResult ChallengeEmailSent() { + return View(new BaseViewModel()); + } + + public ActionResult ChallengeEmail(string token) { + var user = _membershipService.ValidateChallengeToken(token); + + if ( user != null ) { + _authenticationService.SignIn(user, false /* createPersistentCookie */); + return View("ChallengeEmailSuccess"); + } + + return View("ChallengeEmailFail"); + } + protected override void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext.HttpContext.User.Identity is WindowsIdentity) { throw new InvalidOperationException("Windows authentication is not supported."); diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs index bdc91bb9d..05119e766 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -10,6 +10,7 @@ using Orchard.Users.Drivers; using Orchard.Users.Models; using Orchard.Users.Services; using Orchard.Users.ViewModels; +using Orchard.Mvc.Extensions; namespace Orchard.Users.Controllers { [ValidateInput(false)] diff --git a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs index 9daeef15c..95b725ffb 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs @@ -24,7 +24,8 @@ namespace Orchard.Users.DataMigrations { // Adds registration fields to previous versions SchemaBuilder .AlterTable("UserPartRecord", table => table.AddColumn("RegistrationStatus", c => c.WithDefault("'Approved'"))) - .AlterTable("UserPartRecord", table => table.AddColumn("EmailStatus", c => c.WithDefault("'Approved'"))); + .AlterTable("UserPartRecord", table => table.AddColumn("EmailStatus", c => c.WithDefault("'Approved'"))) + .AlterTable("UserPartRecord", table => table.AddColumn("EmailChallengeToken")); // Site Settings record SchemaBuilder.CreateTable("RegistrationSettingsPartRecord", table => table diff --git a/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs index eb9a83ba7..ac2b0d46a 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs @@ -27,7 +27,7 @@ namespace Orchard.Users.Handlers { if ( context.Type == MessageTypes.Validation ) { context.MailMessage.Subject = "User account validation"; - context.MailMessage.Body = string.Format("Dear {0}, please click on the folowwing link to validate you email address: {1}", recipient.UserName, "http://foo"); + context.MailMessage.Body = string.Format("Dear {0}, please click here to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]); } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs index f9c7d29a4..c6c8c2cff 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs @@ -14,5 +14,6 @@ namespace Orchard.Users.Models { public virtual UserStatus RegistrationStatus { get; set; } public virtual UserStatus EmailStatus { get; set; } + public virtual string EmailChallengeToken { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index 465342676..8c037b7ac 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -45,6 +45,7 @@ 3.5 + @@ -98,6 +99,9 @@ + + + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 6431db7f4..046077d03 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -1,8 +1,10 @@ using System; +using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Web.Security; +using System.Xml.Linq; using JetBrains.Annotations; using Orchard.Data; using Orchard.Logging; @@ -13,12 +15,13 @@ using Orchard.Users.Events; using Orchard.Users.Models; using Orchard.Settings; using Orchard.Messaging.Services; -using System.Collections; using System.Collections.Generic; +using System.Web.Mvc; namespace Orchard.Users.Services { [UsedImplicitly] public class MembershipService : IMembershipService { + private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email private readonly IContentManager _contentManager; private readonly IMessageManager _messageManager; private readonly IEnumerable _userEventHandlers; @@ -53,8 +56,8 @@ namespace Orchard.Users.Services { user.Record.NormalizedUserName = createUserParams.Username.ToLower(); user.Record.HashAlgorithm = "SHA1"; SetPassword(user.Record, createUserParams.Password); - user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; - user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + user.Record.RegistrationStatus = registrationSettings.UsersAreModerated && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved; + user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved; var userContext = new UserContext {User = user, Cancel = false}; foreach(var userEventHandler in _userEventHandlers) { @@ -71,11 +74,7 @@ namespace Orchard.Users.Services { userEventHandler.Created(userContext); } - if ( registrationSettings.UsersMustValidateEmail ) { - SendEmailValidationMessage(user); - } - - if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration ) { + if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration && !createUserParams.IsApproved ) { var superUser = GetUser(CurrentSite.SuperUser); if(superUser != null) _messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation); @@ -84,8 +83,52 @@ namespace Orchard.Users.Services { return user; } - public void SendEmailValidationMessage(IUser user) { - _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation); + public void SendChallengeEmail(UserPart user, string url) { + _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "Email", new Dictionary { { "ChallengeUrl", url } }); + } + + public IUser ValidateChallengeToken(string challengeToken) { + string username; + DateTime validateByUtc; + + if(!DecryptChallengeToken(challengeToken, out username, out validateByUtc)) { + return null; + } + + if ( validateByUtc < DateTime.UtcNow ) + return null; + + var user = GetUser(username); + if ( user == null ) + return null; + + user.As().EmailStatus = UserStatus.Approved; + + return user; + } + + public string GetEncryptedChallengeToken(UserPart user) { + var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", DateTime.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString(); + var data = Encoding.UTF8.GetBytes(challengeToken); + return MachineKey.Encode(data, MachineKeyProtection.All); + } + + private static bool DecryptChallengeToken(string challengeToken, out string username, out DateTime validateByUtc) { + username = null; + validateByUtc = DateTime.UtcNow; + + try { + var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All); + var xml = Encoding.UTF8.GetString(data); + var element = XElement.Parse(xml); + username = element.Attribute("username").Value; + validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture); + return true; + } + catch { + return false; + } + } public IUser GetUser(string username) { @@ -143,7 +186,7 @@ namespace Orchard.Users.Services { } } - private bool ValidatePassword(UserPartRecord partRecord, string password) { + private static bool ValidatePassword(UserPartRecord partRecord, string password) { // Note - the password format stored with the record is used // otherwise changing the password format on the site would invalidate // all logins @@ -197,7 +240,7 @@ namespace Orchard.Users.Services { var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm); var hashBytes = hashAlgorithm.ComputeHash(combinedBytes); - + return partRecord.Password == Convert.ToBase64String(hashBytes); } @@ -208,5 +251,6 @@ namespace Orchard.Users.Services { private static bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password) { throw new NotImplementedException(); } + } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx new file mode 100644 index 000000000..dbbd4a223 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

+

<%: T("Your email address could not be validated.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx new file mode 100644 index 000000000..1f4c973ea --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email Sent").ToString()) %>

+

<%: T("An email has been sent to you. Please click on the link it contains in order to have access on this site.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx new file mode 100644 index 000000000..c2f9d0734 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

+

<%: T("Your email address has been validated.") %>

diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index 20f62d942..a11a2a1d0 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -19,6 +19,10 @@ + - - - - - - - - - diff --git a/src/Orchard/Messaging/Services/IMessageManager.cs b/src/Orchard/Messaging/Services/IMessageManager.cs index 28a3becd2..efcbad622 100644 --- a/src/Orchard/Messaging/Services/IMessageManager.cs +++ b/src/Orchard/Messaging/Services/IMessageManager.cs @@ -6,7 +6,7 @@ namespace Orchard.Messaging.Services { /// /// Sends a message to a channel /// - void Send(ContentItemRecord recipient, string type, string service = null); + void Send(ContentItemRecord recipient, string type, string service = null, Dictionary properties = null); /// /// Wether at least one channel is active on the current site diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 091baccbf..379d9c615 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -93,6 +93,9 @@ False ..\..\lib\linqnhibernate\NHibernate.Linq.dll + + ..\Orchard.Web\Modules\Orchard.Email\Bin\Orchard.Users.dll + 3.5 @@ -746,7 +749,6 @@ - diff --git a/src/Orchard/Security/IMembershipService.cs b/src/Orchard/Security/IMembershipService.cs index 0821fcdb1..3f0914c5d 100644 --- a/src/Orchard/Security/IMembershipService.cs +++ b/src/Orchard/Security/IMembershipService.cs @@ -1,4 +1,6 @@ -namespace Orchard.Security { +using Orchard.Users.Models; + +namespace Orchard.Security { public interface IMembershipService : IDependency { MembershipSettings GetSettings(); @@ -7,5 +9,9 @@ IUser ValidateUser(string userNameOrEmail, string password); void SetPassword(IUser user, string password); + + IUser ValidateChallengeToken(string challengeToken); + void SendChallengeEmail(UserPart user, string url); + string GetEncryptedChallengeToken(UserPart user); } } diff --git a/src/Orchard/Security/Providers/OrchardMembershipProvider.cs b/src/Orchard/Security/Providers/OrchardMembershipProvider.cs deleted file mode 100644 index 5f09ddb45..000000000 --- a/src/Orchard/Security/Providers/OrchardMembershipProvider.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Web.Security; -using Orchard.Environment; - -namespace Orchard.Security.Providers { - public class OrchardMembershipProvider : MembershipProvider { - - static IMembershipService GetService() { - throw new NotImplementedException("The OrchardMemberShipProvider is not supported anymore. Use the IMembershipService interface instead."); - } - - static MembershipSettings GetSettings() { - return GetService().GetSettings(); - } - - private MembershipUser BuildMembershipUser(IUser user) { - return new MembershipUser(Name, - user.UserName, - user.Id, - user.Email, - null, - null, - true, - false, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow); - } - - public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { - var user = GetService().CreateUser(new CreateUserParams(username, password, email, passwordQuestion, passwordAnswer, isApproved)); - - if (user == null) { - status = MembershipCreateStatus.ProviderError; - return null; - } - - status = MembershipCreateStatus.Success; - return BuildMembershipUser(user); - } - - - public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { - throw new NotImplementedException(); - } - - public override string GetPassword(string username, string answer) { - throw new NotImplementedException(); - } - - public override bool ChangePassword(string username, string oldPassword, string newPassword) { - var service = GetService(); - var user = service.ValidateUser(username, oldPassword); - if (user == null) - return false; - - service.SetPassword(user, newPassword); - return true; - } - - public override string ResetPassword(string username, string answer) { - throw new NotImplementedException(); - } - - public override void UpdateUser(MembershipUser user) { - throw new NotImplementedException(); - } - - public override bool ValidateUser(string username, string password) { - return (GetService().ValidateUser(username, password) != null); - } - - public override bool UnlockUser(string userName) { - throw new NotImplementedException(); - } - - public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { - throw new NotImplementedException(); - } - - public override MembershipUser GetUser(string username, bool userIsOnline) { - throw new NotImplementedException(); - } - - public override string GetUserNameByEmail(string email) { - throw new NotImplementedException(); - } - - public override bool DeleteUser(string username, bool deleteAllRelatedData) { - throw new NotImplementedException(); - } - - public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override int GetNumberOfUsersOnline() { - throw new NotImplementedException(); - } - - public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override string ApplicationName { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public override bool EnablePasswordRetrieval { get { return GetSettings().EnablePasswordRetrieval; } } - public override bool EnablePasswordReset { get { return GetSettings().EnablePasswordReset; } } - public override bool RequiresQuestionAndAnswer { get { return GetSettings().RequiresQuestionAndAnswer; } } - public override int MaxInvalidPasswordAttempts { get { return GetSettings().MaxInvalidPasswordAttempts; } } - public override int PasswordAttemptWindow { get { return GetSettings().PasswordAttemptWindow; } } - public override bool RequiresUniqueEmail { get { return GetSettings().RequiresUniqueEmail; } } - public override MembershipPasswordFormat PasswordFormat { get { return GetSettings().PasswordFormat; } } - public override int MinRequiredPasswordLength { get { return GetSettings().MinRequiredPasswordLength; } } - public override int MinRequiredNonAlphanumericCharacters { get { return GetSettings().MinRequiredNonAlphanumericCharacters; } } - public override string PasswordStrengthRegularExpression { get { return GetSettings().PasswordStrengthRegularExpression; } } - } -}