Backend/e-suite.API/eSuite.API/Controllers/AuthenticationController.cs
2026-01-20 21:50:10 +00:00

213 lines
8.6 KiB
C#

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;
/// <summary>
/// This part of the API is responsible for creating and maintaining user sessions within e-suite
/// </summary>
[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";
/// <summary>
/// Default constructor used for dependency injection
/// </summary>
/// <param name="userManager"></param>
/// <param name="sentinel"></param>
public AuthenticationController(IUserManager userManager, ISentinel sentinel)
{
_userManager = userManager;
_sentinel = sentinel;
}
/// <summary>
/// Request an password recovery email is sent
/// </summary>
/// <remarks>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.</remarks>
/// <returns></returns>
[Route("forgotPassword")]
[HttpPost]
[AccessKey(SecurityAccess.Everyone)]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> 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"
});
}
}
/// <summary>
/// Login with username, password and optionally Two Factor Authentication code
/// </summary>
/// <remarks>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.</remarks>
/// <example>This is my example</example>
/// <returns></returns>
/// <response code="200">Authentication successful. The "instance" property will contain the token to include to access secure api calls</response>
/// <response code="202">Username and password accepted. Two factor (Tfa) security code required, or request to disable Tfa processed. See the detail property.</response>
/// <response code="401">Authentication is unsuccessful</response>
[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<IActionResult> 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"
});
}
}
/// <summary>
/// Call this to complete the action for an email with an action link.
/// </summary>
/// <remarks>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</remarks>
/// <param name="token"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Route("completeEmailAction")]
[HttpPost]
[AccessKey(SecurityAccess.Everyone)]
[AllowAnonymous]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))]
public async Task<IActionResult> 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;
}
}
/// <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 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 });
}
}