Added email challenge

--HG--
branch : dev
This commit is contained in:
Sebastien Ros
2010-09-01 14:39:28 -07:00
parent 067c5db740
commit 98d11e81f0
16 changed files with 119 additions and 175 deletions

View File

@@ -26,7 +26,7 @@ namespace Orchard.Core.Messaging.Services {
_channels = channels; _channels = channels;
} }
public void Send(ContentItemRecord recipient, string type, string service = null) { public void Send(ContentItemRecord recipient, string type, string service = null, Dictionary<string, string> properties = null) {
if ( !HasChannels() ) if ( !HasChannels() )
return; return;
@@ -40,7 +40,7 @@ namespace Orchard.Core.Messaging.Services {
try { try {
// if the service is not explicit, use the default one, as per settings configuration // 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; service = messageSettings.DefaultChannelService;
} }
@@ -50,6 +50,11 @@ namespace Orchard.Core.Messaging.Services {
Service = service Service = service
}; };
if ( properties != null ) {
foreach (var key in properties.Keys)
context.Properties.Add(key, properties[key]);
}
_messageEventHandler.Sending(context); _messageEventHandler.Sending(context);
foreach ( var channel in _channels ) { foreach ( var channel in _channels ) {

View File

@@ -116,10 +116,16 @@ namespace Orchard.Users.Controllers {
if (ValidateRegistration(userName, email, password, confirmPassword)) { if (ValidateRegistration(userName, email, password, confirmPassword)) {
// Attempt to register the user // 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 != null) {
if ( user.As<UserPart>().EmailStatus == UserStatus.Pending ) {
string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As<UserPart>());
_membershipService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
return RedirectToAction("ChallengeEmailSent");
}
_authenticationService.SignIn(user, false /* createPersistentCookie */); _authenticationService.SignIn(user, false /* createPersistentCookie */);
return Redirect("~/"); return Redirect("~/");
} }
@@ -173,6 +179,21 @@ namespace Orchard.Users.Controllers {
return View(new BaseViewModel()); 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) { protected override void OnActionExecuting(ActionExecutingContext filterContext) {
if (filterContext.HttpContext.User.Identity is WindowsIdentity) { if (filterContext.HttpContext.User.Identity is WindowsIdentity) {
throw new InvalidOperationException("Windows authentication is not supported."); throw new InvalidOperationException("Windows authentication is not supported.");

View File

@@ -10,6 +10,7 @@ using Orchard.Users.Drivers;
using Orchard.Users.Models; using Orchard.Users.Models;
using Orchard.Users.Services; using Orchard.Users.Services;
using Orchard.Users.ViewModels; using Orchard.Users.ViewModels;
using Orchard.Mvc.Extensions;
namespace Orchard.Users.Controllers { namespace Orchard.Users.Controllers {
[ValidateInput(false)] [ValidateInput(false)]

View File

@@ -24,7 +24,8 @@ namespace Orchard.Users.DataMigrations {
// Adds registration fields to previous versions // Adds registration fields to previous versions
SchemaBuilder SchemaBuilder
.AlterTable("UserPartRecord", table => table.AddColumn<string>("RegistrationStatus", c => c.WithDefault("'Approved'"))) .AlterTable("UserPartRecord", table => table.AddColumn<string>("RegistrationStatus", c => c.WithDefault("'Approved'")))
.AlterTable("UserPartRecord", table => table.AddColumn<string>("EmailStatus", c => c.WithDefault("'Approved'"))); .AlterTable("UserPartRecord", table => table.AddColumn<string>("EmailStatus", c => c.WithDefault("'Approved'")))
.AlterTable("UserPartRecord", table => table.AddColumn<string>("EmailChallengeToken"));
// Site Settings record // Site Settings record
SchemaBuilder.CreateTable("RegistrationSettingsPartRecord", table => table SchemaBuilder.CreateTable("RegistrationSettingsPartRecord", table => table

View File

@@ -27,7 +27,7 @@ namespace Orchard.Users.Handlers {
if ( context.Type == MessageTypes.Validation ) { if ( context.Type == MessageTypes.Validation ) {
context.MailMessage.Subject = "User account 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 <a href=\"{1}\">click here</a> to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]);
} }
} }

View File

@@ -14,5 +14,6 @@ namespace Orchard.Users.Models {
public virtual UserStatus RegistrationStatus { get; set; } public virtual UserStatus RegistrationStatus { get; set; }
public virtual UserStatus EmailStatus { get; set; } public virtual UserStatus EmailStatus { get; set; }
public virtual string EmailChallengeToken { get; set; }
} }
} }

View File

@@ -45,6 +45,7 @@
<RequiredTargetFramework>3.5</RequiredTargetFramework> <RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference> </Reference>
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Security" />
<Reference Include="System.Web.ApplicationServices" /> <Reference Include="System.Web.ApplicationServices" />
<Reference Include="System.Web.DynamicData" /> <Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Entity" /> <Reference Include="System.Web.Entity" />
@@ -98,6 +99,9 @@
<Content Include="Views\Account\ChangePassword.ascx" /> <Content Include="Views\Account\ChangePassword.ascx" />
<Content Include="Views\Account\ChangePasswordSuccess.ascx" /> <Content Include="Views\Account\ChangePasswordSuccess.ascx" />
<Content Include="Views\Account\AccessDenied.ascx" /> <Content Include="Views\Account\AccessDenied.ascx" />
<Content Include="Views\Account\ChallengeEmailSuccess.ascx" />
<Content Include="Views\Account\ChallengeEmailSent.ascx" />
<Content Include="Views\Account\ChallengeEmailFail.ascx" />
<Content Include="Views\Account\LogOn.ascx" /> <Content Include="Views\Account\LogOn.ascx" />
<Content Include="Views\Account\Register.ascx" /> <Content Include="Views\Account\Register.ascx" />
<Content Include="Views\Admin\Edit.aspx" /> <Content Include="Views\Admin\Edit.aspx" />

View File

@@ -1,8 +1,10 @@
using System; using System;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Web.Security; using System.Web.Security;
using System.Xml.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Orchard.Data; using Orchard.Data;
using Orchard.Logging; using Orchard.Logging;
@@ -13,12 +15,13 @@ using Orchard.Users.Events;
using Orchard.Users.Models; using Orchard.Users.Models;
using Orchard.Settings; using Orchard.Settings;
using Orchard.Messaging.Services; using Orchard.Messaging.Services;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Web.Mvc;
namespace Orchard.Users.Services { namespace Orchard.Users.Services {
[UsedImplicitly] [UsedImplicitly]
public class MembershipService : IMembershipService { 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 IContentManager _contentManager;
private readonly IMessageManager _messageManager; private readonly IMessageManager _messageManager;
private readonly IEnumerable<IUserEventHandler> _userEventHandlers; private readonly IEnumerable<IUserEventHandler> _userEventHandlers;
@@ -53,8 +56,8 @@ namespace Orchard.Users.Services {
user.Record.NormalizedUserName = createUserParams.Username.ToLower(); user.Record.NormalizedUserName = createUserParams.Username.ToLower();
user.Record.HashAlgorithm = "SHA1"; user.Record.HashAlgorithm = "SHA1";
SetPassword(user.Record, createUserParams.Password); SetPassword(user.Record, createUserParams.Password);
user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; user.Record.RegistrationStatus = registrationSettings.UsersAreModerated && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved;
user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved;
var userContext = new UserContext {User = user, Cancel = false}; var userContext = new UserContext {User = user, Cancel = false};
foreach(var userEventHandler in _userEventHandlers) { foreach(var userEventHandler in _userEventHandlers) {
@@ -71,11 +74,7 @@ namespace Orchard.Users.Services {
userEventHandler.Created(userContext); userEventHandler.Created(userContext);
} }
if ( registrationSettings.UsersMustValidateEmail ) { if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration && !createUserParams.IsApproved ) {
SendEmailValidationMessage(user);
}
if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration ) {
var superUser = GetUser(CurrentSite.SuperUser); var superUser = GetUser(CurrentSite.SuperUser);
if(superUser != null) if(superUser != null)
_messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation); _messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation);
@@ -84,8 +83,52 @@ namespace Orchard.Users.Services {
return user; return user;
} }
public void SendEmailValidationMessage(IUser user) { public void SendChallengeEmail(UserPart user, string url) {
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation); _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "Email", new Dictionary<string, string> { { "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<UserPart>().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) { 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 // Note - the password format stored with the record is used
// otherwise changing the password format on the site would invalidate // otherwise changing the password format on the site would invalidate
// all logins // all logins
@@ -197,7 +240,7 @@ namespace Orchard.Users.Services {
var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm); var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm);
var hashBytes = hashAlgorithm.ComputeHash(combinedBytes); var hashBytes = hashAlgorithm.ComputeHash(combinedBytes);
return partRecord.Password == Convert.ToBase64String(hashBytes); return partRecord.Password == Convert.ToBase64String(hashBytes);
} }
@@ -208,5 +251,6 @@ namespace Orchard.Users.Services {
private static bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password) { private static bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password) {
throw new NotImplementedException(); throw new NotImplementedException();
} }
} }
} }

View File

@@ -0,0 +1,3 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<bool>" %>
<h1><%: Html.TitleForPage(T("Challenge Email").ToString()) %></h1>
<p><%: T("Your email address could not be validated.") %></p>

View File

@@ -0,0 +1,3 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<object>" %>
<h1><%: Html.TitleForPage(T("Challenge Email Sent").ToString()) %></h1>
<p><%: T("An email has been sent to you. Please click on the link it contains in order to have access on this site.") %></p>

View File

@@ -0,0 +1,3 @@
<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl<bool>" %>
<h1><%: Html.TitleForPage(T("Challenge Email").ToString()) %></h1>
<p><%: T("Your email address has been validated.") %></p>

View File

@@ -19,6 +19,10 @@
<defaultSettings timeout="00:30:00"/> <defaultSettings timeout="00:30:00"/>
</system.transactions> </system.transactions>
<system.web> <system.web>
<machineKey validationKey="00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
decryptionKey="0000000000000000000000000000000000000000000000000000000000000000"
validation="SHA1"
decryption="AES" />
<httpRuntime requestValidationMode="2.0" /> <httpRuntime requestValidationMode="2.0" />
<!-- <!--
Set compilation debug="true" to insert debugging Set compilation debug="true" to insert debugging
@@ -45,33 +49,6 @@
<authentication mode="Forms"> <authentication mode="Forms">
<forms loginUrl="~/Users/Account/AccessDenied" timeout="2880"/> <forms loginUrl="~/Users/Account/AccessDenied" timeout="2880"/>
</authentication> </authentication>
<membership defaultProvider="OrchardMembershipProvider">
<providers>
<clear/>
<!--<add name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="ApplicationServices"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
passwordFormat="Hashed"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
passwordStrengthRegularExpression=""
applicationName="/"
/>-->
<add name="OrchardMembershipProvider" type="Orchard.Security.Providers.OrchardMembershipProvider, Orchard.Framework" applicationName="/"/>
</providers>
</membership>
<profile>
<providers>
<clear/>
<add name="AspNetSqlProfileProvider" type="System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="ApplicationServices" applicationName="/"/>
</providers>
</profile>
<roleManager enabled="false"> <roleManager enabled="false">
<providers> <providers>
<clear/> <clear/>

View File

@@ -6,7 +6,7 @@ namespace Orchard.Messaging.Services {
/// <summary> /// <summary>
/// Sends a message to a channel /// Sends a message to a channel
/// </summary> /// </summary>
void Send(ContentItemRecord recipient, string type, string service = null); void Send(ContentItemRecord recipient, string type, string service = null, Dictionary<string, string> properties = null);
/// <summary> /// <summary>
/// Wether at least one channel is active on the current site /// Wether at least one channel is active on the current site

View File

@@ -93,6 +93,9 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>..\..\lib\linqnhibernate\NHibernate.Linq.dll</HintPath> <HintPath>..\..\lib\linqnhibernate\NHibernate.Linq.dll</HintPath>
</Reference> </Reference>
<Reference Include="Orchard.Users">
<HintPath>..\Orchard.Web\Modules\Orchard.Email\Bin\Orchard.Users.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations"> <Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework> <RequiredTargetFramework>3.5</RequiredTargetFramework>
@@ -746,7 +749,6 @@
<Compile Include="Security\IUser.cs" /> <Compile Include="Security\IUser.cs" />
<Compile Include="Security\Permissions\IPermissionProvider.cs" /> <Compile Include="Security\Permissions\IPermissionProvider.cs" />
<Compile Include="Security\Permissions\Permission.cs" /> <Compile Include="Security\Permissions\Permission.cs" />
<Compile Include="Security\Providers\OrchardMembershipProvider.cs" />
<Compile Include="Security\Providers\OrchardRoleProvider.cs" /> <Compile Include="Security\Providers\OrchardRoleProvider.cs" />
<Compile Include="Services\Clock.cs" /> <Compile Include="Services\Clock.cs" />
<Compile Include="FileSystems\Media\FileSystemStorageProvider.cs" /> <Compile Include="FileSystems\Media\FileSystemStorageProvider.cs" />

View File

@@ -1,4 +1,6 @@
namespace Orchard.Security { using Orchard.Users.Models;
namespace Orchard.Security {
public interface IMembershipService : IDependency { public interface IMembershipService : IDependency {
MembershipSettings GetSettings(); MembershipSettings GetSettings();
@@ -7,5 +9,9 @@
IUser ValidateUser(string userNameOrEmail, string password); IUser ValidateUser(string userNameOrEmail, string password);
void SetPassword(IUser user, string password); void SetPassword(IUser user, string password);
IUser ValidateChallengeToken(string challengeToken);
void SendChallengeEmail(UserPart user, string url);
string GetEncryptedChallengeToken(UserPart user);
} }
} }

View File

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