Member 2FA (#11889)
* Bugfix - Take ufprt from form data if the request has form content type, otherwise fallback to use the query * External linking for members * Changed migration to reuse old table * removed unnecessary web.config files * Cleanup * Extracted class to own file * Clean up * Rollback changes to Umbraco.Web.UI.csproj * Fixed migration for SqlCE * Added 2fa for members * Change notification handler to be on deleted * Update src/Umbraco.Infrastructure/Security/MemberUserStore.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * updated snippets * Fixed issue with errors not shown on member linking * fixed issue with errors * clean up * Fix issue where external logins could not be used to upgrade Umbraco, because the externalLogin table was expected to look different. (Like after the migration) * Fixed issue in Ignore legacy column now using result column. * Updated 2fa for members + publish notification when 2fa is requested. * Changed so only Members out of box supports 2fa * Cleanup * rollback of csproj file, that should not have been changed * Removed confirmed flag from db. It was not used. Handle case where a user is signed up for 2fa, but the provider do not exist anymore. Then it is just ignored until it shows up again Reintroduced ProviderName on interface, to ensure the class can be renamed safely * Bugfix * Registering DeleteTwoFactorLoginsOnMemberDeletedHandler * Rollback nuget packages added by mistake * Update src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Added providername to snippet Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -4,12 +4,13 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -27,9 +28,12 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
public class UmbExternalLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly ILogger<UmbExternalLoginController> _logger;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbExternalLoginController(
|
||||
ILogger<UmbExternalLoginController> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
@@ -37,7 +41,8 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager)
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
@@ -46,8 +51,10 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,14 +115,12 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
$"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}");
|
||||
}
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { userId = attemptedUser.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
|
||||
}
|
||||
|
||||
if (result == SignInResult.LockedOut)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
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.ActionsResults;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Web.Common.Filters;
|
||||
using Umbraco.Cms.Web.Common.Models;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
@@ -20,7 +26,29 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
public class UmbLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberSignInManager _signInManager;
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UmbLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManager signInManager,
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with all params")]
|
||||
public UmbLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
@@ -29,9 +57,11 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManager signInManager)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
: this(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider, signInManager,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberManager>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -74,15 +104,28 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
throw new NotImplementedException("Two factor support is not supported for Umbraco members yet");
|
||||
MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username);
|
||||
if (attemptedUser == null)
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"No local member found for username {model.Username}");
|
||||
}
|
||||
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Member is locked out");
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Member is not allowed");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Invalid username or password");
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -97,5 +140,7 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
model.RedirectUrl = redirectUrl.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
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.ActionsResults;
|
||||
using Umbraco.Cms.Web.Common.Filters;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Umbraco.Cms.Web.Website.Controllers
|
||||
{
|
||||
[UmbracoMemberAuthorize]
|
||||
public class UmbTwoFactorLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly ILogger<UmbTwoFactorLoginController> _logger;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbTwoFactorLoginController(
|
||||
ILogger<UmbTwoFactorLoginController> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
services,
|
||||
appCaches,
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to retrieve the 2FA providers for code submission
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<string>>> Get2FAProviders()
|
||||
{
|
||||
var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("Get2FAProviders :: No verified member found, returning 404");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user);
|
||||
return new ObjectResult(userFactors);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Verify2FACode(Verify2FACodeModel model, string returnUrl = null)
|
||||
{
|
||||
var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var result = await _memberSignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, model.RememberClient);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is locked out");
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is not allowed");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Invalid code");
|
||||
}
|
||||
}
|
||||
|
||||
//We need to set this, to ensure we show the 2fa login page
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ValidateAndSaveSetup(string providerName, string secret, string code, string returnUrl = null)
|
||||
{
|
||||
var member = await _memberManager.GetCurrentMemberAsync();
|
||||
|
||||
var isValid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code);
|
||||
|
||||
if (isValid == false)
|
||||
{
|
||||
ModelState.AddModelError(nameof(code), "Invalid Code");
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
var twoFactorLogin = new TwoFactorLogin()
|
||||
{
|
||||
Confirmed = true,
|
||||
Secret = secret,
|
||||
UserOrMemberKey = member.Key,
|
||||
ProviderName = providerName
|
||||
};
|
||||
|
||||
await _twoFactorLoginService.SaveAsync(twoFactorLogin);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Disable(string providerName, string returnUrl = null)
|
||||
{
|
||||
var member = await _memberManager.GetCurrentMemberAsync();
|
||||
|
||||
var success = await _twoFactorLoginService.DisableAsync(member.Key, providerName);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl) =>
|
||||
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user