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; } } public class Redirect { public string RedirectTo { get; set; } } /// /// This MVC controller is used to support all user interactions when logging in and out of the system, including profile updates. /// [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; /// /// Default constructor used to inject dependencies /// /// /// /// /// 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"; /// /// Returns the login page for the system /// /// /// /// [Route("login")] [AllowAnonymous] [AccessKey(SecurityAccess.Everyone)] [HttpGet] public async Task 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 Ok(new Redirect { RedirectTo = url }); } login ??= new Login(); return LoginView(login); } private ActionResult LoginView(Login? login) { return PartialView("Login", login); } /// /// Used for processing the login attempts /// /// /// /// [Route("login")] [AllowAnonymous] [AccessKey(SecurityAccess.Everyone)] [HttpPost] [ValidateAntiForgeryToken] public async Task 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); } } /// /// Logs a user out of the system and tidies up any cookies. /// /// /// [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 Logout(CancellationToken cancellationToken) #pragma warning restore IDE0060 // Remove unused parameter { await _cookieManager.DeleteSessionCookie(Response); await _cookieManager.DeleteSsoIdCookie(Response); return Redirect("~/"); } /// /// Called by an identity provider when processing an OAuth2 identity request. /// /// /// /// /// /// /// /// [Route("auth/{ssoId}")] [HttpGet] [AllowAnonymous] public async Task 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("~/"); } } /// /// Get a new token to replace your current token. /// /// 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. /// [Route("refreshToken")] [HttpGet] [AccessKey(SecurityAccess.Everyone)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task RefreshToken(CancellationToken cancellationToken = default!) { var id = User.GeneralIdRef(); var loginResponse = await _userManager.RefreshToken(id, cancellationToken); await _cookieManager.CreateSessionCookie(Response, loginResponse); return Ok(); } /// /// Page used for making alterations to a users profile. /// /// /// [Route("profile")] [HttpGet] [AccessKey(SecurityAccess.Everyone)] public async Task 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); } /// /// Used to make updates to a users profile. /// /// /// /// [Route("profile")] [AccessKey(SecurityAccess.Everyone)] [HttpPost] [ValidateAntiForgeryToken] public async Task 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); } /// /// Page used for making alterations to a users profile. /// /// /// /// [Route("confirmaccount/{token}")] [HttpGet] [AllowAnonymous] [AccessKey(SecurityAccess.Everyone)] public async Task 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(jsonString); return emailActionToken; } /// /// Used to make updates to a users profile. /// /// /// /// /// [Route("confirmaccount/{token}")] [AllowAnonymous] [AccessKey(SecurityAccess.Everyone)] [HttpPost] [ValidateAntiForgeryToken] public async Task 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); } }