2020-06-09 14:36:36 +10:00
using Microsoft.AspNetCore.Authentication ;
using Microsoft.AspNetCore.Mvc ;
2020-05-25 23:15:32 +10:00
using System ;
using System.Net ;
using System.Threading.Tasks ;
2020-06-09 14:36:36 +10:00
using Umbraco.Core ;
2020-05-25 23:15:32 +10:00
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 ) ;
}
2020-06-09 14:36:36 +10:00
/// <summary>
/// Logs the current user out
/// </summary>
/// <returns></returns>
[TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))]
public IActionResult PostLogout ( )
{
HttpContext . SignOutAsync ( Core . Constants . Security . BackOfficeAuthenticationType ) ;
_logger . Info < AuthenticationController > ( "User {UserName} from IP address {RemoteIpAddress} has logged out" , User . Identity = = null ? "UNKNOWN" : User . Identity . Name , HttpContext . Connection . RemoteIpAddress ) ;
_userManager . RaiseLogoutSuccessEvent ( User , int . Parse ( User . Identity . GetUserId ( ) ) ) ;
return Ok ( ) ;
}
2020-05-25 23:15:32 +10:00
/// <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
}
}