2020-05-13 16:09:54 +10:00
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using Umbraco.Core.BackOffice;
|
|
|
|
|
|
using Umbraco.Core.Configuration;
|
2020-06-03 17:47:32 +10:00
|
|
|
|
using Umbraco.Core.Logging;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
using Umbraco.Core.Mapping;
|
|
|
|
|
|
using Umbraco.Core.Models.Membership;
|
|
|
|
|
|
using Umbraco.Core.Services;
|
|
|
|
|
|
using Umbraco.Extensions;
|
2020-06-03 17:47:32 +10:00
|
|
|
|
using Umbraco.Net;
|
2020-06-03 12:47:40 +10:00
|
|
|
|
using Umbraco.Web.BackOffice.Filters;
|
2020-05-13 16:09:54 +10:00
|
|
|
|
using Umbraco.Web.Common.Attributes;
|
2020-05-19 09:52:58 +02:00
|
|
|
|
using Umbraco.Web.Common.Controllers;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
using Umbraco.Web.Common.Exceptions;
|
2020-05-13 16:09:54 +10:00
|
|
|
|
using Umbraco.Web.Common.Filters;
|
2020-06-02 13:28:30 +10:00
|
|
|
|
using Umbraco.Web.Common.Security;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
using Umbraco.Web.Models;
|
|
|
|
|
|
using Umbraco.Web.Models.ContentEditing;
|
2020-05-19 09:52:58 +02:00
|
|
|
|
using Umbraco.Web.Security;
|
2020-05-14 17:04:16 +10:00
|
|
|
|
using Constants = Umbraco.Core.Constants;
|
2020-05-13 16:09:54 +10:00
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Web.BackOffice.Controllers
|
|
|
|
|
|
{
|
2020-05-14 21:12:41 +10:00
|
|
|
|
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)] // TODO: Maybe this could be applied with our Application Model conventions
|
2020-05-13 16:09:54 +10:00
|
|
|
|
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
|
2020-06-08 13:14:23 +02:00
|
|
|
|
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
|
2020-05-14 17:04:16 +10:00
|
|
|
|
[IsBackOffice] // TODO: This could be applied with our Application Model conventions
|
2020-05-25 23:15:32 +10:00
|
|
|
|
public class AuthenticationController : UmbracoApiControllerBase
|
2020-05-13 16:09:54 +10:00
|
|
|
|
{
|
2020-06-04 13:53:06 +02:00
|
|
|
|
private readonly IWebSecurity _webSecurity;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
private readonly BackOfficeUserManager _userManager;
|
|
|
|
|
|
private readonly BackOfficeSignInManager _signInManager;
|
|
|
|
|
|
private readonly IUserService _userService;
|
|
|
|
|
|
private readonly UmbracoMapper _umbracoMapper;
|
|
|
|
|
|
private readonly IGlobalSettings _globalSettings;
|
2020-06-03 17:47:32 +10:00
|
|
|
|
private readonly ILogger _logger;
|
|
|
|
|
|
private readonly IIpResolver _ipResolver;
|
2020-05-19 09:52:58 +02:00
|
|
|
|
|
2020-05-25 23:15:32 +10:00
|
|
|
|
// TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController
|
2020-05-27 18:27:49 +10:00
|
|
|
|
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
|
2020-05-25 23:15:32 +10:00
|
|
|
|
|
|
|
|
|
|
public AuthenticationController(
|
2020-06-04 13:53:06 +02:00
|
|
|
|
IWebSecurity webSecurity,
|
2020-05-25 23:15:32 +10:00
|
|
|
|
BackOfficeUserManager backOfficeUserManager,
|
|
|
|
|
|
BackOfficeSignInManager signInManager,
|
|
|
|
|
|
IUserService userService,
|
|
|
|
|
|
UmbracoMapper umbracoMapper,
|
2020-06-03 17:47:32 +10:00
|
|
|
|
IGlobalSettings globalSettings,
|
|
|
|
|
|
ILogger logger, IIpResolver ipResolver)
|
2020-05-19 09:52:58 +02:00
|
|
|
|
{
|
2020-06-04 13:53:06 +02:00
|
|
|
|
_webSecurity = webSecurity;
|
2020-05-25 23:15:32 +10:00
|
|
|
|
_userManager = backOfficeUserManager;
|
|
|
|
|
|
_signInManager = signInManager;
|
|
|
|
|
|
_userService = userService;
|
|
|
|
|
|
_umbracoMapper = umbracoMapper;
|
|
|
|
|
|
_globalSettings = globalSettings;
|
2020-06-03 17:47:32 +10:00
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_ipResolver = ipResolver;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public double GetRemainingTimeoutSeconds()
|
|
|
|
|
|
{
|
|
|
|
|
|
var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity();
|
|
|
|
|
|
var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds();
|
|
|
|
|
|
if (remainingSeconds <= 30 && backOfficeIdentity != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
//NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in
|
|
|
|
|
|
// the timeout process.
|
|
|
|
|
|
|
|
|
|
|
|
_logger.Info<AuthenticationController>(
|
|
|
|
|
|
"User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}",
|
|
|
|
|
|
backOfficeIdentity.Name,
|
|
|
|
|
|
_ipResolver.GetCurrentRequestIpAddress());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return remainingSeconds;
|
2020-05-19 09:52:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
[HttpGet]
|
|
|
|
|
|
public bool IsAuthenticated()
|
|
|
|
|
|
{
|
2020-06-04 13:53:06 +02:00
|
|
|
|
var attempt = _webSecurity.AuthorizeRequest();
|
2020-05-19 09:52:58 +02:00
|
|
|
|
if (attempt == ValidateRequestAttempt.Success)
|
|
|
|
|
|
{
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2020-05-25 23:15:32 +10:00
|
|
|
|
|
2020-06-03 12:47:40 +10:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Returns the currently logged in Umbraco user
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user
|
|
|
|
|
|
/// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session
|
|
|
|
|
|
/// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies.
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
[UmbracoAuthorize]
|
|
|
|
|
|
[TypeFilter(typeof(SetAngularAntiForgeryTokens))]
|
|
|
|
|
|
//[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level
|
|
|
|
|
|
public UserDetail GetCurrentUser()
|
|
|
|
|
|
{
|
2020-06-09 12:35:31 +10:00
|
|
|
|
var user = _webSecurity.CurrentUser;
|
2020-06-03 12:47:40 +10:00
|
|
|
|
var result = _umbracoMapper.Map<UserDetail>(user);
|
|
|
|
|
|
|
|
|
|
|
|
//set their remaining seconds
|
|
|
|
|
|
result.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds();
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2020-05-25 23:15:32 +10:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Logs a user in
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
[TypeFilter(typeof(SetAngularAntiForgeryTokens))]
|
|
|
|
|
|
public async Task<UserDetail> PostLogin(LoginModel loginModel)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 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
|
|
|
|
|
|
var result = await _signInManager.PasswordSignInAsync(
|
|
|
|
|
|
loginModel.Username, loginModel.Password, isPersistent: true, lockoutOnFailure: true);
|
|
|
|
|
|
|
|
|
|
|
|
if (result.Succeeded)
|
|
|
|
|
|
{
|
2020-05-27 18:27:49 +10:00
|
|
|
|
// return the user detail
|
|
|
|
|
|
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
|
2020-05-25 23:15:32 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (result.RequiresTwoFactor)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new NotImplementedException("Implement MFA/2FA, we need to have some IOptions or similar to configure this");
|
|
|
|
|
|
|
|
|
|
|
|
//var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions;
|
|
|
|
|
|
//if (twofactorOptions == null)
|
|
|
|
|
|
//{
|
|
|
|
|
|
// throw new HttpResponseException(
|
|
|
|
|
|
// Request.CreateErrorResponse(
|
|
|
|
|
|
// HttpStatusCode.BadRequest,
|
|
|
|
|
|
// "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions)));
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
//var twofactorView = twofactorOptions.GetTwoFactorView(
|
|
|
|
|
|
// owinContext,
|
|
|
|
|
|
// UmbracoContext,
|
|
|
|
|
|
// loginModel.Username);
|
|
|
|
|
|
|
|
|
|
|
|
//if (twofactorView.IsNullOrWhiteSpace())
|
|
|
|
|
|
//{
|
|
|
|
|
|
// throw new HttpResponseException(
|
|
|
|
|
|
// Request.CreateErrorResponse(
|
|
|
|
|
|
// HttpStatusCode.BadRequest,
|
|
|
|
|
|
// typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string"));
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
//var attemptedUser = Services.UserService.GetByUsername(loginModel.Username);
|
|
|
|
|
|
|
|
|
|
|
|
//// create a with information to display a custom two factor send code view
|
|
|
|
|
|
//var verifyResponse = Request.CreateResponse(HttpStatusCode.PaymentRequired, new
|
|
|
|
|
|
//{
|
|
|
|
|
|
// twoFactorView = twofactorView,
|
|
|
|
|
|
// userId = attemptedUser.Id
|
|
|
|
|
|
//});
|
|
|
|
|
|
|
|
|
|
|
|
//_userManager.RaiseLoginRequiresVerificationEvent(User, attemptedUser.Id);
|
|
|
|
|
|
|
|
|
|
|
|
//return verifyResponse;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
|
|
|
|
|
// by our angular helper because it thinks that we need to re-perform the request once we are
|
|
|
|
|
|
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
|
|
|
|
|
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
|
|
|
|
|
throw new HttpResponseException(HttpStatusCode.BadRequest);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2020-05-27 18:27:49 +10:00
|
|
|
|
/// Return the <see cref="UserDetail"/> for the given <see cref="IUser"/>
|
2020-05-25 23:15:32 +10:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="user"></param>
|
|
|
|
|
|
/// <param name="principal"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2020-05-27 18:27:49 +10:00
|
|
|
|
private UserDetail GetUserDetail(IUser user)
|
2020-05-25 23:15:32 +10:00
|
|
|
|
{
|
2020-05-27 18:27:49 +10:00
|
|
|
|
if (user == null) throw new ArgumentNullException(nameof(user));
|
2020-05-25 23:15:32 +10:00
|
|
|
|
|
|
|
|
|
|
var userDetail = _umbracoMapper.Map<UserDetail>(user);
|
|
|
|
|
|
// update the userDetail and set their remaining seconds
|
|
|
|
|
|
userDetail.SecondsUntilTimeout = TimeSpan.FromMinutes(_globalSettings.TimeOutInMinutes).TotalSeconds;
|
|
|
|
|
|
|
|
|
|
|
|
return userDetail;
|
|
|
|
|
|
}
|
2020-05-13 16:09:54 +10:00
|
|
|
|
}
|
|
|
|
|
|
}
|