Fixes anti-forgery, fixes tempdata, adds front-end security/identity, gets member macro snippets and controllers all working, removes old code, adds more props to the member identity

This commit is contained in:
Shannon
2021-04-09 15:24:12 +10:00
parent 461be27bb1
commit 8ea88a980a
60 changed files with 946 additions and 1693 deletions

View File

@@ -1,22 +1,25 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models.Security;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Cms.Web.Common.Models;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
namespace Umbraco.Cms.Web.Website.Controllers
{
public class UmbLoginController : SurfaceController
{
private readonly IUmbracoWebsiteSecurityAccessor _websiteSecurityAccessor;
private readonly IMemberSignInManager _signInManager;
public UmbLoginController(
IUmbracoContextAccessor umbracoContextAccessor,
@@ -25,10 +28,10 @@ namespace Umbraco.Cms.Web.Website.Controllers
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IUmbracoWebsiteSecurityAccessor websiteSecurityAccessor)
IMemberSignInManager signInManager)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_websiteSecurityAccessor = websiteSecurityAccessor;
_signInManager = signInManager;
}
[HttpPost]
@@ -41,27 +44,55 @@ namespace Umbraco.Cms.Web.Website.Controllers
return CurrentUmbracoPage();
}
if (await _websiteSecurityAccessor.WebsiteSecurity.LoginAsync(model.Username, model.Password) == false)
MergeRouteValuesToModel(model);
// Sign the user in with username/password, this also gives a chance for developers to
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
SignInResult result = await _signInManager.PasswordSignInAsync(
model.Username, model.Password, isPersistent: model.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
// Don't add a field level error, just model level.
ModelState.AddModelError("loginModel", "Invalid username or password");
return CurrentUmbracoPage();
TempData["LoginSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
{
// Validate the redirect URL.
// If it's not a local URL we'll redirect to the root of the current site.
return Redirect(Url.IsLocalUrl(model.RedirectUrl)
? model.RedirectUrl
: CurrentPage.AncestorOrSelf(1).Url(PublishedUrlProvider));
}
// Redirect to current page by default.
return RedirectToCurrentUmbracoPage();
}
TempData["LoginSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
if (result.RequiresTwoFactor)
{
// Validate the redirect URL.
// If it's not a local URL we'll redirect to the root of the current site.
return Redirect(Url.IsLocalUrl(model.RedirectUrl)
? model.RedirectUrl
: CurrentPage.AncestorOrSelf(1).Url(PublishedUrlProvider));
throw new NotImplementedException("Two factor support is not supported for Umbraco members yet");
}
// Redirect to current page by default.
return RedirectToCurrentUmbracoPage();
// TODO: We can check for these and respond differently if we think it's important
// result.IsLockedOut
// result.IsNotAllowed
// Don't add a field level error, just model level.
ModelState.AddModelError("loginModel", "Invalid username or password");
return CurrentUmbracoPage();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
private void MergeRouteValuesToModel(LoginModel model)
{
if (RouteData.Values.TryGetValue(nameof(LoginModel.RedirectUrl), out var redirectUrl) && redirectUrl != null)
{
model.RedirectUrl = redirectUrl.ToString();
}
}
}
}

View File

@@ -1,14 +1,14 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models.Security;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Cms.Web.Common.Models;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Website.Controllers
@@ -16,15 +16,18 @@ namespace Umbraco.Cms.Web.Website.Controllers
[UmbracoMemberAuthorize]
public class UmbLoginStatusController : SurfaceController
{
private readonly IUmbracoWebsiteSecurityAccessor _websiteSecurityAccessor;
private readonly IMemberSignInManager _signInManager;
public UmbLoginStatusController(IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches,
IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IUmbracoWebsiteSecurityAccessor websiteSecurityAccessor)
public UmbLoginStatusController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager signInManager)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_websiteSecurityAccessor = websiteSecurityAccessor;
}
=> _signInManager = signInManager;
[HttpPost]
[ValidateAntiForgeryToken]
@@ -36,9 +39,11 @@ namespace Umbraco.Cms.Web.Website.Controllers
return CurrentUmbracoPage();
}
if (_websiteSecurityAccessor.WebsiteSecurity.IsLoggedIn())
var isLoggedIn = HttpContext.User?.Identity?.IsAuthenticated ?? false;
if (isLoggedIn)
{
await _websiteSecurityAccessor.WebsiteSecurity.LogOutAsync();
await _signInManager.SignOutAsync();
}
TempData["LogoutSuccess"] = true;

View File

@@ -1,15 +1,18 @@
using System;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models.Security;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Cms.Web.Website.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Website.Controllers
@@ -17,14 +20,25 @@ namespace Umbraco.Cms.Web.Website.Controllers
[UmbracoMemberAuthorize]
public class UmbProfileController : SurfaceController
{
private readonly IUmbracoWebsiteSecurityAccessor _websiteSecurityAccessor;
private readonly IMemberManager _memberManager;
private readonly IMemberService _memberService;
private readonly IMemberTypeService _memberTypeService;
public UmbProfileController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory,
ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider, IUmbracoWebsiteSecurityAccessor websiteSecurityAccessor)
public UmbProfileController(
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberManager memberManager,
IMemberService memberService,
IMemberTypeService memberTypeService)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_websiteSecurityAccessor = websiteSecurityAccessor;
_memberManager = memberManager;
_memberService = memberService;
_memberTypeService = memberTypeService;
}
[HttpPost]
@@ -37,20 +51,23 @@ namespace Umbraco.Cms.Web.Website.Controllers
return CurrentUmbracoPage();
}
var result = await _websiteSecurityAccessor.WebsiteSecurity.UpdateMemberProfileAsync(model);
switch (result.Status)
MergeRouteValuesToModel(model);
MemberIdentityUser currentMember = await _memberManager.GetUserAsync(HttpContext.User);
if (currentMember == null)
{
case UpdateMemberProfileStatus.Success:
break;
case UpdateMemberProfileStatus.Error:
// Don't add a field level error, just model level.
ModelState.AddModelError("profileModel", result.ErrorMessage);
return CurrentUmbracoPage();
default:
throw new ArgumentOutOfRangeException();
// this shouldn't happen, we also don't want to return an error so just redirect to where we came from
return RedirectToCurrentUmbracoPage();
}
TempData["ProfileUpdateSuccess"] = true;
IdentityResult result = await UpdateMemberAsync(model, currentMember);
if (!result.Succeeded)
{
AddErrors(result);
return CurrentUmbracoPage();
}
TempData["FormSuccess"] = true;
// If there is a specified path to redirect to then use it.
if (model.RedirectUrl.IsNullOrWhiteSpace() == false)
@@ -61,5 +78,68 @@ namespace Umbraco.Cms.Web.Website.Controllers
// Redirect to current page by default.
return RedirectToCurrentUmbracoPage();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
private void MergeRouteValuesToModel(ProfileModel model)
{
if (RouteData.Values.TryGetValue(nameof(ProfileModel.RedirectUrl), out var redirectUrl) && redirectUrl != null)
{
model.RedirectUrl = redirectUrl.ToString();
}
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("profileModel", error.Description);
}
}
private async Task<IdentityResult> UpdateMemberAsync(ProfileModel model, MemberIdentityUser currentMember)
{
currentMember.Email = model.Email;
currentMember.Name = model.Name;
currentMember.UserName = model.UserName;
currentMember.Comments = model.Comments;
IdentityResult saveResult = await _memberManager.UpdateAsync(currentMember);
if (!saveResult.Succeeded)
{
return saveResult;
}
// now we can update the custom properties
// TODO: Ideally we could do this all through our MemberIdentityUser
IMember member = _memberService.GetByKey(currentMember.Key);
if (member == null)
{
// should never happen
throw new InvalidOperationException($"Could not find a member with key: {member.Key}.");
}
IMemberType memberType = _memberTypeService.Get(member.ContentTypeId);
if (model.MemberProperties != null)
{
foreach (MemberPropertyModel property in model.MemberProperties
//ensure the property they are posting exists
.Where(p => memberType.PropertyTypeExists(p.Alias))
.Where(property => member.Properties.Contains(property.Alias))
//needs to be editable
.Where(p => memberType.MemberCanEditProperty(p.Alias)))
{
member.Properties[property.Alias].SetValue(property.Value);
}
}
_memberService.Save(member);
return saveResult;
}
}
}

View File

@@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Security;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
@@ -14,28 +13,32 @@ using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Cms.Web.Common.Security;
using Umbraco.Cms.Web.Website.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Website.Controllers
{
public class UmbRegisterController : SurfaceController
{
private readonly MemberManager _memberManager;
private readonly IMemberManager _memberManager;
private readonly IMemberService _memberService;
private readonly IMemberSignInManager _memberSignInManager;
public UmbRegisterController(
MemberManager memberManager,
IMemberManager memberManager,
IMemberService memberService,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider)
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager memberSignInManager)
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
{
_memberManager = memberManager;
_memberService = memberService;
_memberSignInManager = memberSignInManager;
}
[HttpPost]
@@ -48,6 +51,8 @@ namespace Umbraco.Cms.Web.Website.Controllers
return CurrentUmbracoPage();
}
MergeRouteValuesToModel(model);
// U4-10762 Server error with "Register Member" snippet (Cannot save member with empty name)
// If name field is empty, add the email address instead.
if (string.IsNullOrEmpty(model.Name) && string.IsNullOrEmpty(model.Email) == false)
@@ -55,7 +60,7 @@ namespace Umbraco.Cms.Web.Website.Controllers
model.Name = model.Email;
}
IdentityResult result = await RegisterMemberAsync(model, model.LoginOnSuccess);
IdentityResult result = await RegisterMemberAsync(model, true);
if (result.Succeeded)
{
TempData["FormSuccess"] = true;
@@ -71,16 +76,38 @@ namespace Umbraco.Cms.Web.Website.Controllers
}
else
{
AddModelErrors(result, "registerModel");
AddErrors(result);
return CurrentUmbracoPage();
}
}
private void AddModelErrors(IdentityResult result, string prefix = "")
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
private void MergeRouteValuesToModel(RegisterModel model)
{
foreach (IdentityError error in result.Errors)
if (RouteData.Values.TryGetValue(nameof(RegisterModel.RedirectUrl), out var redirectUrl) && redirectUrl != null)
{
ModelState.AddModelError(prefix, error.Description);
model.RedirectUrl = redirectUrl.ToString();
}
if (RouteData.Values.TryGetValue(nameof(RegisterModel.MemberTypeAlias), out var memberTypeAlias) && memberTypeAlias != null)
{
model.MemberTypeAlias = memberTypeAlias.ToString();
}
if (RouteData.Values.TryGetValue(nameof(RegisterModel.UsernameIsEmail), out var usernameIsEmail) && usernameIsEmail != null)
{
model.UsernameIsEmail = usernameIsEmail.ToString() == "True";
}
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("registerModel", error.Description);
}
}
@@ -94,7 +121,7 @@ namespace Umbraco.Cms.Web.Website.Controllers
{
model.Username = (model.UsernameIsEmail || model.Username == null) ? model.Email : model.Username;
var identityUser = MembersIdentityUser.CreateNew(model.Username, model.Email, model.MemberTypeAlias, model.Name);
var identityUser = MemberIdentityUser.CreateNew(model.Username, model.Email, model.MemberTypeAlias, model.Name);
IdentityResult identityResult = await _memberManager.CreateAsync(
identityUser,
model.Password);
@@ -103,10 +130,15 @@ namespace Umbraco.Cms.Web.Website.Controllers
{
// Update the custom properties
// TODO: See TODO in MembersIdentityUser, Should we support custom member properties for persistence/retrieval?
IMember member = _memberService.GetByUsername(identityUser.UserName);
IMember member = _memberService.GetByKey(identityUser.Key);
if (member == null)
{
// should never happen
throw new InvalidOperationException($"Could not find a member with key: {member.Key}.");
}
if (model.MemberProperties != null)
{
foreach (UmbracoProperty property in model.MemberProperties.Where(p => p.Value != null)
foreach (MemberPropertyModel property in model.MemberProperties.Where(p => p.Value != null)
.Where(property => member.Properties.Contains(property.Alias)))
{
member.Properties[property.Alias].SetValue(property.Value);
@@ -116,8 +148,7 @@ namespace Umbraco.Cms.Web.Website.Controllers
if (logMemberIn)
{
// TODO: Log them in
throw new NotImplementedException("Implement MemberSignInManager");
await _memberSignInManager.SignInAsync(identityUser, false);
}
}