using e_suite.API.Common; using e_suite.API.Common.exceptions; using e_suite.API.Common.models; using e_suite.Service.Sentinel; using eSuite.API.Models; using eSuite.API.security; using eSuite.API.Utilities; using eSuite.Core.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace eSuite.API.Controllers; /// /// This part of the API is responsible for creating and maintaining user sessions within e-suite /// [Route("api/[controller]")] [ApiController] public class AuthenticationController : ESuiteControllerBase { private readonly IUserManager _userManager; private readonly ISentinel _sentinel; private const string AccessDeniedText = "Access denied"; private const string LoginAcceptedText = "Login accepted"; /// /// Default constructor used for dependency injection /// /// /// public AuthenticationController(IUserManager userManager, ISentinel sentinel) { _userManager = userManager; _sentinel = sentinel; } /// /// Request an password recovery email is sent /// /// A password recovery email allows someone to gain access to the system after they have lost their password. It will allow them to reset their password. /// [Route("forgotPassword")] [HttpPost] [AccessKey(SecurityAccess.Everyone)] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task ForgotPassword( [FromBody] EmailAddress forgotPasswordRequest, CancellationToken cancellationToken = default! ) { try { var securityResult = await _sentinel.CheckSecurity(this, cancellationToken); if (securityResult != null) return securityResult; await _userManager.ForgotPassword(forgotPasswordRequest.Email, cancellationToken); return Ok(); } catch (NotFoundException) { await _sentinel.LogBadRequest(this, cancellationToken); throw; } catch (EmailNotConfirmedException) { await _sentinel.LogBadRequest(this, cancellationToken); return Unauthorized(new ProblemDetails { Title = AccessDeniedText, Detail = "Email not confirmed" }); } } /// /// Login with username, password and optionally Two Factor Authentication code /// /// Log into e-suite using username, password and optionally Two factor Authentication. This call can also be used to attempt to request two factor authentication is disabled. /// This is my example /// /// Authentication successful. The "instance" property will contain the token to include to access secure api calls /// Username and password accepted. Two factor (Tfa) security code required, or request to disable Tfa processed. See the detail property. /// Authentication is unsuccessful [Route("login")] [HttpPost] [AccessKey(SecurityAccess.Everyone)] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(SuccessfulLogin))] [ProducesResponseType(StatusCodes.Status202Accepted)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [Produces("application/json")] [Obsolete("Login is dealt with via SSO now. Only username and password auth will work with this call. Will be replaced with user created API Keys.")] public async Task Login([FromBody] Login login, CancellationToken cancellationToken = default!) { var securityResult = await _sentinel.CheckSecurity(this, cancellationToken); if (securityResult != null) return securityResult; var loginResponse = await _userManager.Login(login, cancellationToken); switch (loginResponse.Result) { case LoginResult.EmailNotConfirmed: return Unauthorized(new ProblemDetails { Title = AccessDeniedText, Detail = "Email not confirmed" }); case LoginResult.TwoFactorAuthenticationRemovalRequested: return Accepted(new ProblemDetails { Title = LoginAcceptedText, Detail = "Request to remove two factor authentication accepted" }); case LoginResult.TwoFactorAuthenticationCodeRequired: return Accepted(new ProblemDetails { Title = LoginAcceptedText, Detail = "Please supply Two Factor Authentication code" }); case LoginResult.TwoFactorAuthenticationCodeIncorrect: await _sentinel.LogBadRequest(this, cancellationToken); return Unauthorized(new ProblemDetails { Title = AccessDeniedText, Detail = "Incorrect authorisation code" }); case LoginResult.Success: return Ok(new SuccessfulLogin { Title = LoginAcceptedText, Token = loginResponse.Token }); case LoginResult.Failed: default: await _sentinel.LogBadRequest(this, cancellationToken); return Unauthorized(new ProblemDetails { Title = AccessDeniedText, Detail = "Email and/or Password has been entered incorrectly" }); } } /// /// Call this to complete the action for an email with an action link. /// /// There are several functions in the system that will result in an email being sent to the user with a link so they can confirm. Currently these are to for a forgotten password, to disable the two factor authentication and for a new user to confirm their e-mail /// /// /// [Route("completeEmailAction")] [HttpPost] [AccessKey(SecurityAccess.Everyone)] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] public async Task CompleteEmailAction( [FromBody] EmailActionToken token, CancellationToken cancellationToken = default! ) { var securityResult = await _sentinel.CheckSecurity(this, cancellationToken); if (securityResult != null) return securityResult; try { await _userManager.CompleteEmailAction(token, cancellationToken); return Ok(); } catch (TokenInvalidException) { await _sentinel.LogBadRequest(this, cancellationToken); throw; } catch (InvalidEmailException) { await _sentinel.LogBadRequest(this, cancellationToken); throw; } } /// /// 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 userId = User.GeneralIdRef(); var loginResponse = await _userManager.RefreshToken(userId, cancellationToken); if (loginResponse.Result != LoginResult.Success) return Unauthorized(new ProblemDetails { Title = AccessDeniedText, Detail = "Username or Password incorrect" }); return Ok(new SuccessfulLogin { Title = "Access Granted", Token = loginResponse.Token }); } }