refactor: transfer logic

All logic related to token manipulation has been transferred to the AuthService. Also added TOTP 2FA and rethought the logic of logging into the application
This commit is contained in:
2024-10-31 04:12:02 +03:00
parent 0f47a98ad9
commit cd6f25deba
19 changed files with 371 additions and 278 deletions

View File

@ -2,109 +2,64 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Options;
using Mirea.Api.Dto.Common;
using Mirea.Api.Dto.Requests;
using Mirea.Api.Dto.Responses;
using Mirea.Api.Endpoint.Common.Attributes;
using Mirea.Api.Endpoint.Common.Exceptions;
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Endpoint.Configuration.Core.Middleware;
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Security.Common.Dto.Requests;
using Mirea.Api.Security.Common.Domain;
using Mirea.Api.Security.Services;
using System;
using System.Security;
using System.Threading.Tasks;
namespace Mirea.Api.Endpoint.Controllers.V1;
[ApiVersion("1.0")]
public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, PasswordHashService passwordService) : BaseController, IActionFilter
public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, PasswordHashService passwordService) : BaseController
{
private string Fingerprint { get; set; } = string.Empty;
private string Ip { get; set; } = string.Empty;
private string UserAgent { get; set; } = string.Empty;
private string RefreshToken { get; set; } = string.Empty;
private void SetCookie(string name, string value, DateTimeOffset? expires = null)
{
var cookieOptions = new CookieOptions
private CookieOptionsParameters GetCookieParams() =>
new()
{
Expires = expires,
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api",
Domain = HttpContext.GetCurrentDomain(),
HttpOnly = true,
#if !DEBUG
Secure = true
#endif
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
};
Response.Cookies.Append(name, value, cookieOptions);
}
private void SetRefreshToken(string value, DateTimeOffset? expires = null)
{
SetCookie("refresh_token", value, expires);
SetCookie("user_key", Fingerprint);
}
private void SetFirstToken(string value, DateTimeOffset? expires = null)
{
SetCookie("authentication_token", value, expires);
SetCookie("user_key", Fingerprint);
}
private void SetAuthToken(string value, DateTimeOffset? expires = null)
{
SetCookie(CookieAuthorizationMiddleware.JwtAuthorizationName, value, expires);
SetCookie("user_key", Fingerprint);
}
[ApiExplorerSettings(IgnoreApi = true)]
public void OnActionExecuting(ActionExecutingContext context)
{
Ip = HttpContext.Connection.RemoteIpAddress?.ToString()!;
UserAgent = Request.Headers.UserAgent.ToString();
Fingerprint = Request.Cookies["user_key"] ?? string.Empty;
RefreshToken = Request.Cookies["refresh_token"] ?? string.Empty;
if (!string.IsNullOrWhiteSpace(Fingerprint)) return;
Fingerprint = Guid.NewGuid().ToString().Replace("-", "");
}
[ApiExplorerSettings(IgnoreApi = true)]
public void OnActionExecuted(ActionExecutedContext context) { }
/// <summary>
/// Handles user authentication by verifying the username/email and password,
/// then generating and returning an authentication token if successful.
/// </summary>
/// <param name="request">The login request containing the username/email and password.</param>
/// <returns>User's AuthRoles.</returns>
[HttpPost("Login")]
[BadRequestResponse]
public async Task<ActionResult<AuthRoles>> Login([FromBody] LoginRequest request)
public async Task<ActionResult<AuthenticationStep>> Login([FromBody] LoginRequest request)
{
var userEntity = user.Value;
if (!userEntity.Username.Equals(request.Username, StringComparison.OrdinalIgnoreCase) &&
!userEntity.Email.Equals(request.Username, StringComparison.OrdinalIgnoreCase) ||
!passwordService.VerifyPassword(request.Password, userEntity.Salt, userEntity.PasswordHash))
!userEntity.Email.Equals(request.Username, StringComparison.OrdinalIgnoreCase))
return BadRequest("Invalid username/email or password");
var token = await auth.GenerateAuthTokensAsync(new TokenRequest
{
Fingerprint = Fingerprint,
Ip = Ip,
UserAgent = UserAgent
}, "1");
var tokenResult = await auth.LoginAsync(
GetCookieParams(),
new User
{
Id = 1,
Username = userEntity.Username,
Email = userEntity.Email,
PasswordHash = userEntity.PasswordHash,
Salt = userEntity.Salt,
SecondFactor = userEntity.SecondFactor,
SecondFactorToken = userEntity.Secret
},
HttpContext, request.Password);
SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn);
SetAuthToken(token.AccessToken, token.AccessExpiresIn);
return Ok(tokenResult ? AuthenticationStep.None : AuthenticationStep.TotpRequired);
}
return Ok(AuthRoles.Admin);
[HttpGet("Login")]
[BadRequestResponse]
public async Task<ActionResult<AuthenticationStep>> Login([FromQuery] string code)
{
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, code);
return Ok(tokenResult ? AuthenticationStep.None : AuthenticationStep.TotpRequired);
}
/// <summary>
@ -116,30 +71,8 @@ public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, Pass
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<AuthRoles>> ReLogin()
{
if (string.IsNullOrEmpty(RefreshToken))
return Unauthorized();
try
{
var token = await auth.RefreshTokenAsync(
new TokenRequest
{
Ip = Ip,
UserAgent = UserAgent,
Fingerprint = Fingerprint
},
RefreshToken
);
SetRefreshToken(token.RefreshToken, token.RefreshExpiresIn);
SetAuthToken(token.AccessToken, token.AccessExpiresIn);
return Ok(AuthRoles.Admin);
}
catch (SecurityException)
{
return Forbid();
}
await auth.RefreshTokenAsync(GetCookieParams(), HttpContext);
return Ok(AuthRoles.Admin);
}
/// <summary>
@ -151,11 +84,7 @@ public class AuthController(IOptionsSnapshot<Admin> user, AuthService auth, Pass
[Authorize]
public async Task<ActionResult> Logout()
{
SetRefreshToken("", DateTimeOffset.MinValue);
SetFirstToken("", DateTimeOffset.MinValue);
SetAuthToken("", DateTimeOffset.MinValue);
await auth.LogoutAsync(Fingerprint);
await auth.LogoutAsync(GetCookieParams(), HttpContext);
return Ok();
}