Backend/e-suite.API/eSuite.API/Controllers/AccountController.cs

395 lines
15 KiB
C#

using System.Text.Json;
using e_suite.API.Common;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.models;
using e_suite.Database.Audit;
using e_suite.Database.Core.Extensions;
using e_suite.Service.Sentinel;
using eSuite.API.Extensions;
using eSuite.API.security;
using eSuite.API.SingleSignOn;
using eSuite.API.Translation;
using eSuite.API.Utilities;
using eSuite.Core.Security;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace eSuite.API.Controllers;
public class ProfileViewModel
{
public Models.UserProfile Profile { get; set; }
public ITranslatorFactory TranslatorFactory { get; set; }
}
/// <summary>
/// This MVC controller is used to support all user interactions when logging in and out of the system, including profile updates.
/// </summary>
[Route("/[controller]")]
[ApiExplorerSettings(IgnoreApi = true)]
public class AccountController : ESuiteController
{
private readonly IUserManager _userManager;
private readonly ISentinel _sentinel;
private readonly ISingleSignOn _singleSignOn;
private readonly ICookieManager _cookieManager;
private readonly ITranslatorFactory _translatorFactory;
/// <summary>
/// Default constructor used to inject dependencies
/// </summary>
/// <param name="userManager"></param>
/// <param name="sentinel"></param>
/// <param name="singleSignOn"></param>
/// <param name="cookieManager"></param>
public AccountController(IUserManager userManager, ISentinel sentinel, ISingleSignOn singleSignOn, ICookieManager cookieManager, ITranslatorFactory translatorFactory)
{
_userManager = userManager;
_sentinel = sentinel;
_singleSignOn = singleSignOn;
_cookieManager = cookieManager;
_translatorFactory = translatorFactory;
}
private const string AccountProfileUrl = "~/account/profile";
private const string RootUrl = "/";
private const string ProfileUrl = "~/profile";
/// <summary>
/// Returns the login page for the system
/// </summary>
/// <param name="login"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("login")]
[AllowAnonymous]
[AccessKey(SecurityAccess.Everyone)]
[HttpGet]
public async Task<IActionResult> LoginGet(Login? login, CancellationToken cancellationToken)
{
var ssoId = await _cookieManager.GetSsoIdFromSsoIdCookie(Request);
if (ssoId.HasValue)
{
await _cookieManager.DeleteSsoIdCookie(Response);
var url = await _singleSignOn.StartSingleSignOn(ssoId.Value, cancellationToken);
return Redirect(url);
}
login ??= new Login();
return LoginView(login);
}
private ActionResult LoginView(Login? login)
{
return PartialView("Login", login);
}
/// <summary>
/// Used for processing the login attempts
/// </summary>
/// <param name="login"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("login")]
[AllowAnonymous]
[AccessKey(SecurityAccess.Everyone)]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginPost(Login login, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(login.Password))
{
var url = await _singleSignOn.StartSingleSignOn(login.Email, cancellationToken);
if (!string.IsNullOrWhiteSpace(url))
return Redirect(url);
if (login.ForgotPassword)
{
await _userManager.ForgotPassword(login.Email, cancellationToken);
}
return LoginView(login);
}
if (login.ForgotPassword)
{
await _userManager.ForgotPassword(login.Email, cancellationToken);
return LoginView(login);
}
var loginResponse = await _userManager.Login(login, cancellationToken);
ViewBag.loginResponse = loginResponse.Result;
switch (loginResponse.Result)
{
case LoginResult.Success:
await _cookieManager.CreateSessionCookie(Response, loginResponse);
return Redirect("/");
case LoginResult.EmailNotConfirmed:
case LoginResult.TwoFactorAuthenticationRemovalRequested:
case LoginResult.TwoFactorAuthenticationCodeRequired:
case LoginResult.TwoFactorAuthenticationCodeIncorrect:
return LoginView(login);
case LoginResult.Failed:
default:
await _sentinel.LogBadRequest(this, cancellationToken);
return LoginView(login);
}
}
/// <summary>
/// Logs a user out of the system and tidies up any cookies.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("logout")]
[HttpGet]
[AllowAnonymous]
#pragma warning disable IDE0060 // Remove unused parameter cancellationToken - used here as I want this parameter on all controller methods.
public async Task<IActionResult> Logout(CancellationToken cancellationToken)
#pragma warning restore IDE0060 // Remove unused parameter
{
await _cookieManager.DeleteSessionCookie(Response);
await _cookieManager.DeleteSsoIdCookie(Response);
return Redirect("~/");
}
/// <summary>
/// Called by an identity provider when processing an OAuth2 identity request.
/// </summary>
/// <param name="ssoId"></param>
/// <param name="code"></param>
/// <param name="scope"></param>
/// <param name="authuser"></param>
/// <param name="prompt"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("auth/{ssoId}")]
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> Auth([FromRoute] long ssoId, [FromQuery] string code, [FromQuery] string scope, [FromQuery] string authuser, [FromQuery] string prompt, CancellationToken cancellationToken)
{
var ssoUserId = await _singleSignOn.ExchangeAuthorisationToken(ssoId, code, cancellationToken);
var cookieLink = await _cookieManager.GetUserIdFromLinkCookie(Request, cancellationToken);
if (cookieLink?.User != null)
{
await _cookieManager.DeleteLinkCookie(Response);
//Creating my own Audit details as the JWT isn't present when being called from an outside source
var auditUserDetails = new AuditUserDetails
{
UserId = cookieLink.User.Id,
UserDisplayName = cookieLink.User.DisplayName
};
await _userManager.LinkSsoProfileToUser(auditUserDetails, cookieLink.User, ssoId, ssoUserId, cookieLink.LinkType == LinkType.NewUser , cancellationToken);
if (cookieLink.LinkType == LinkType.Profile)
return Redirect(ProfileUrl);
}
var loginResponse = await _userManager.LoginSso(ssoId, ssoUserId, cancellationToken);
switch (loginResponse.Result)
{
case LoginResult.Success:
await _cookieManager.CreateSessionCookie(Response, loginResponse);
await _cookieManager.CreateSsoIdCookie(Response, ssoId);
return Redirect("~/");
case LoginResult.EmailNotConfirmed:
case LoginResult.TwoFactorAuthenticationRemovalRequested:
case LoginResult.TwoFactorAuthenticationCodeRequired:
case LoginResult.TwoFactorAuthenticationCodeIncorrect:
return Redirect("~/");
case LoginResult.Failed:
default:
await _sentinel.LogBadRequest(this, cancellationToken);
return Redirect("~/");
}
}
/// <summary>
/// Get a new token to replace your current token.
/// </summary>
/// <remarks>e-suite authentication tokens are valid for a limited amount of time. To gain more time on your user session you will need to exchange your token for a new one.</remarks>
/// <returns></returns>
[Route("refreshToken")]
[HttpGet]
[AccessKey(SecurityAccess.Everyone)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> RefreshToken(CancellationToken cancellationToken = default!)
{
var id = User.GeneralIdRef();
var loginResponse = await _userManager.RefreshToken(id, cancellationToken);
await _cookieManager.CreateSessionCookie(Response, loginResponse);
return Ok();
}
/// <summary>
/// Page used for making alterations to a users profile.
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("profile")]
[HttpGet]
[AccessKey(SecurityAccess.Everyone)]
public async Task<IActionResult> ProfileGet(CancellationToken cancellationToken)
{
var profile = await _userManager.GetProfile(User.Email(), cancellationToken);
Models.UserProfile userProfile = profile.ToUserProfile();
_translatorFactory.Locale = profile.PreferredLocale;
var profileViewModel = new ProfileViewModel
{
Profile = userProfile,
TranslatorFactory = _translatorFactory
};
return PartialView("Profile", profileViewModel);
}
/// <summary>
/// Used to make updates to a users profile.
/// </summary>
/// <param name="userProfile"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("profile")]
[AccessKey(SecurityAccess.Everyone)]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ProfilePost(Models.UserProfile userProfile, CancellationToken cancellationToken)
{
var updatedUserProfile = new UpdatedUserProfile
{
Email = User.Email(),
Password = userProfile.Password ?? string.Empty,
FirstName = userProfile.FirstName ?? string.Empty,
MiddleNames = userProfile.MiddleNames ?? string.Empty,
LastName = userProfile.LastName ?? string.Empty,
SecurityCode = userProfile.SecurityCode ?? string.Empty,
UsingTwoFactorAuthentication = userProfile.UsingTwoFactorAuthentication
};
await _userManager.UpdateProfile(AuditUserDetails, User.Email(), updatedUserProfile, cancellationToken);
var profile = await _userManager.GetProfile(User.Email(), cancellationToken);
var id = User.GeneralIdRef();
if (profile.DomainSsoProviderId == null)
{
if (profile.SsoProviderId != userProfile.SsoProviderId)
{
if (userProfile.SsoProviderId != -1)
{
var url = await _singleSignOn.StartSingleSignOn(userProfile.SsoProviderId, cancellationToken);
if (!string.IsNullOrWhiteSpace(url))
{
await _cookieManager.CreateProfileLinkCookie(Response, AuditUserDetails, id, cancellationToken);
return Redirect(url);
}
}
else
{
await _userManager.TurnOfSsoForUser(AuditUserDetails, id, cancellationToken);
}
}
}
return Redirect(AccountProfileUrl);
}
/// <summary>
/// Page used for making alterations to a users profile.
/// </summary>
/// <param name="token"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("confirmaccount/{token}")]
[HttpGet]
[AllowAnonymous]
[AccessKey(SecurityAccess.Everyone)]
public async Task<IActionResult> ConfirmAccountGet([FromRoute] string token, CancellationToken cancellationToken)
{
var emailActionToken = DecodeToken(token);
var profile = await _userManager.GetProfile(emailActionToken?.Email!, cancellationToken);
var userProfile = profile.ToConfirmEmailAccount();
return View("ConfirmAccount", userProfile);
}
private static EmailActionToken? DecodeToken(string token)
{
var jsonString = Convert.FromBase64String(token);
var emailActionToken = JsonSerializer.Deserialize<EmailActionToken>(jsonString);
return emailActionToken;
}
/// <summary>
/// Used to make updates to a users profile.
/// </summary>
/// <param name="userProfile"></param>
/// <param name="token"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("confirmaccount/{token}")]
[AllowAnonymous]
[AccessKey(SecurityAccess.Everyone)]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ConfirmAccountPost(
Models.ConfirmEmailAccount userProfile,
[FromRoute] string token,
CancellationToken cancellationToken
)
{
var emailActionToken = DecodeToken(token);
var user = await _userManager.GetUserByEmailAsync(emailActionToken?.Email!, cancellationToken) ??
throw new NotFoundException("User not found");
var auditUserDetails = new AuditUserDetails
{
UserId = user.Id,
UserDisplayName = user.DisplayName
};
if (user.Domain.SsoProviderId is not null || userProfile.SsoProviderId != -1)
{
var url = await _singleSignOn.StartSingleSignOn(user.Domain.SsoProviderId ?? userProfile.SsoProviderId, cancellationToken);
if (!string.IsNullOrWhiteSpace(url))
{
await _cookieManager.CreateNewUserLinkCookie(Response, auditUserDetails, user.ToGeneralIdRef()!,
cancellationToken);
return Redirect(url);
}
}
else
{
await _userManager.TurnOfSsoForUser(auditUserDetails, user.ToGeneralIdRef()!, cancellationToken);
var userAuthenticationDetails = new UserAuthenticationDetails
{
Id = user.ToGeneralIdRef()!,
Password = userProfile.Password ?? string.Empty,
SecurityCode = userProfile.SecurityCode ?? string.Empty,
UsingTwoFactorAuthentication = userProfile.UsingTwoFactorAuthentication
};
await _userManager.SetAuthentication(auditUserDetails, userAuthenticationDetails, true, cancellationToken);
}
return Redirect(RootUrl);
}
}