Refactoring users challenge emails

- Adding unit tests
- Creating a stub for email messages
- Adding tenant's name to nonces in order to prevent cross-tenants substitution

--HG--
branch : dev
This commit is contained in:
Sébastien Ros
2010-11-28 18:34:13 -08:00
parent a376b7b40b
commit e2adccc598
11 changed files with 493 additions and 18 deletions

View File

@@ -120,8 +120,7 @@ namespace Orchard.Users.Controllers {
if (user != null) {
if ( user.As<UserPart>().EmailStatus == UserStatus.Pending ) {
string challengeToken = _userService.GetNonce(user.As<UserPart>());
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken })));
_userService.SendChallengeEmail(user.As<UserPart>(), nonce => Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", nonce = nonce })));
return RedirectToAction("ChallengeEmailSent");
}

View File

@@ -197,8 +197,7 @@ namespace Orchard.Users.Controllers {
var user = Services.ContentManager.Get(id);
if ( user != null ) {
string challengeToken = _userService.GetNonce(user.As<UserPart>());
_userService.SendChallengeEmail(user.As<UserPart>(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken})));
_userService.SendChallengeEmail(user.As<UserPart>(), nonce => Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", nonce = nonce})));
}
Services.Notifier.Information(T("Challenge email sent"));

View File

@@ -5,12 +5,13 @@ namespace Orchard.Users.Services {
string VerifyUserUnicity(string userName, string email);
string VerifyUserUnicity(int id, string userName, string email);
void SendChallengeEmail(IUser user, string url);
void SendChallengeEmail(IUser user, Func<string, string> createUrl);
IUser ValidateChallenge(string challengeToken);
bool SendLostPasswordEmail(string usernameOrEmail, Func<string, string> createUrl);
IUser ValidateLostPassword(string nonce);
string GetNonce(IUser user);
string CreateNonce(IUser user, TimeSpan delay);
bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc);
}
}

View File

@@ -19,7 +19,6 @@ using Orchard.Services;
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 IOrchardServices _orchardServices;
private readonly IMessageManager _messageManager;
private readonly IEnumerable<IUserEventHandler> _userEventHandlers;

View File

@@ -12,22 +12,26 @@ using System.Globalization;
using System.Text;
using System.Web.Security;
using Orchard.Messaging.Services;
using Orchard.Environment.Configuration;
namespace Orchard.Users.Services {
[UsedImplicitly]
public class UserService : IUserService {
private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email
private static readonly TimeSpan DelayToResetPassword = new TimeSpan(1, 0, 0, 0); // 24 hours to validate email
private readonly IContentManager _contentManager;
private readonly IMembershipService _membershipService;
private readonly IClock _clock;
private readonly IMessageManager _messageManager;
private readonly ShellSettings _shellSettings;
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager) {
public UserService(IContentManager contentManager, IMembershipService membershipService, IClock clock, IMessageManager messageManager, ShellSettings shellSettings) {
_contentManager = contentManager;
_membershipService = membershipService;
_clock = clock;
_messageManager = messageManager;
_shellSettings = shellSettings;
Logger = NullLogger.Instance;
}
@@ -63,23 +67,25 @@ namespace Orchard.Users.Services {
return null;
}
public string GetNonce(IUser user) {
var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", _clock.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.UTF8.GetBytes(challengeToken);
public string CreateNonce(IUser user, TimeSpan delay) {
// the tenant's name is added to the token to prevent cross-tenant requests
var challengeToken = new XElement("n", new XAttribute("s", _shellSettings.Name), new XAttribute("un", user.UserName), new XAttribute("utc", _clock.UtcNow.ToUniversalTime().Add(delay).ToString(CultureInfo.InvariantCulture))).ToString();
var data = Encoding.Unicode.GetBytes(challengeToken);
return MachineKey.Encode(data, MachineKeyProtection.All);
}
private bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc) {
public bool DecryptNonce(string challengeToken, out string username, out DateTime validateByUtc) {
username = null;
validateByUtc = _clock.UtcNow;
try {
var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All);
var xml = Encoding.UTF8.GetString(data);
var xml = Encoding.Unicode.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;
var tenant = element.Attribute("s").Value;
username = element.Attribute("un").Value;
validateByUtc = DateTime.Parse(element.Attribute("utc").Value, CultureInfo.InvariantCulture);
return String.Equals(_shellSettings.Name, tenant, StringComparison.Ordinal) && _clock.UtcNow <= validateByUtc;
}
catch {
return false;
@@ -107,7 +113,9 @@ namespace Orchard.Users.Services {
return user;
}
public void SendChallengeEmail(IUser user, string url) {
public void SendChallengeEmail(IUser user, Func<string, string> createUrl) {
string nonce = CreateNonce(user, DelayToValidate);
string url = createUrl(nonce);
_messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "email", new Dictionary<string, string> { { "ChallengeUrl", url } });
}
@@ -116,7 +124,7 @@ namespace Orchard.Users.Services {
var user = _contentManager.Query<UserPart, UserPartRecord>().Where(u => u.NormalizedUserName == lowerName || u.Email == lowerName).List().FirstOrDefault();
if (user != null) {
string nonce = GetNonce(user);
string nonce = CreateNonce(user, DelayToResetPassword);
string url = createUrl(nonce);
_messageManager.Send(user.ContentItem.Record, MessageTypes.LostPassword, "email", new Dictionary<string, string> { { "LostPasswordUrl", url } });