2024-09-18 06:00:07 +03:00
|
|
|
|
using Asp.Versioning;
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
2024-06-21 21:52:21 +03:00
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.Extensions.Options;
|
|
|
|
|
using Mirea.Api.Dto.Common;
|
|
|
|
|
using Mirea.Api.Dto.Requests;
|
2024-11-04 02:59:51 +03:00
|
|
|
|
using Mirea.Api.Dto.Responses;
|
2024-08-24 04:30:31 +03:00
|
|
|
|
using Mirea.Api.Endpoint.Common.Attributes;
|
2024-08-27 22:52:07 +03:00
|
|
|
|
using Mirea.Api.Endpoint.Common.Exceptions;
|
2024-11-02 01:06:58 +03:00
|
|
|
|
using Mirea.Api.Endpoint.Common.MapperDto;
|
2024-07-05 01:35:19 +03:00
|
|
|
|
using Mirea.Api.Endpoint.Common.Services;
|
2024-10-07 02:13:35 +03:00
|
|
|
|
using Mirea.Api.Endpoint.Configuration.Model;
|
2024-10-31 04:12:02 +03:00
|
|
|
|
using Mirea.Api.Security.Common.Domain;
|
2024-06-21 21:52:21 +03:00
|
|
|
|
using Mirea.Api.Security.Services;
|
|
|
|
|
using System;
|
2024-11-04 02:39:10 +03:00
|
|
|
|
using System.Collections.Generic;
|
2024-12-22 05:25:19 +03:00
|
|
|
|
using System.Diagnostics;
|
2024-12-26 08:47:56 +03:00
|
|
|
|
using System.Linq;
|
2024-06-21 21:52:21 +03:00
|
|
|
|
using System.Threading.Tasks;
|
2024-12-18 07:23:23 +03:00
|
|
|
|
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
2024-06-21 21:52:21 +03:00
|
|
|
|
|
|
|
|
|
namespace Mirea.Api.Endpoint.Controllers.V1;
|
|
|
|
|
|
|
|
|
|
[ApiVersion("1.0")]
|
2024-12-25 05:43:30 +03:00
|
|
|
|
public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<GeneralConfig> generalConfig, AuthService auth,
|
|
|
|
|
PasswordHashService passwordService, OAuthService oAuthService) : BaseController
|
2024-06-21 21:52:21 +03:00
|
|
|
|
{
|
2024-10-31 04:12:02 +03:00
|
|
|
|
private CookieOptionsParameters GetCookieParams() =>
|
|
|
|
|
new()
|
2024-06-21 21:52:21 +03:00
|
|
|
|
{
|
2024-08-10 23:11:43 +03:00
|
|
|
|
Domain = HttpContext.GetCurrentDomain(),
|
2024-10-31 04:12:02 +03:00
|
|
|
|
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
|
2024-06-21 21:52:21 +03:00
|
|
|
|
};
|
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
private static string GenerateHtmlResponse(
|
|
|
|
|
string title,
|
|
|
|
|
string message,
|
|
|
|
|
Uri? callback,
|
|
|
|
|
string traceId,
|
|
|
|
|
bool isError)
|
2024-12-18 07:40:07 +03:00
|
|
|
|
{
|
2024-12-26 08:51:22 +03:00
|
|
|
|
var callbackUrl = callback?.ToString();
|
|
|
|
|
|
|
|
|
|
var script = callback == null ? string.Empty :
|
|
|
|
|
$"<script>setTimeout(()=>{{window.location.href='{callbackUrl}';}}, {(isError ? 15000 : 5000)});</script>";
|
|
|
|
|
|
|
|
|
|
var blockInfo = "<p>" + (callback == null ?
|
|
|
|
|
"Вернитесь обратно и попробуйте снова позже.</p>" :
|
|
|
|
|
$"Если вы не будете автоматически перенаправлены, нажмите ниже.</p>" +
|
|
|
|
|
$"<a href=\"{callbackUrl}\" style=\"color:inherit;text-decoration:underline;\">Перейти вручную</a>");
|
|
|
|
|
|
|
|
|
|
return $"<!DOCTYPE html><html lang=ru><head><meta charset=UTF-8><meta content=\"width=device-width,initial-scale=1\"name=viewport><link href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap\"rel=stylesheet><style>body{{background-color:#121212;color:#fff;font-family:Roboto,sans-serif;display:flex;justify-content:center;align-items:center;height:100vh;margin:0;text-align:center}}.container{{max-width:600px;padding:20px;border-radius:8px;background-color:#1e1e1e;box-shadow:0 4px 20px rgba(0,0,0,.5)}}h1{{font-size:24px;margin-bottom:20px}}</style><title>{title}</title></head><body><div class=container><h1>{title}</h1>{blockInfo}<p style=font-size:14px;color:silver;>{message}</p><code style=font-size:12px;color:gray;>TraceId={traceId}</code></div>{script}</body></html>";
|
2024-12-18 07:40:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[HttpGet("OAuth2")]
|
|
|
|
|
[BadRequestResponse]
|
|
|
|
|
[Produces("text/html")]
|
|
|
|
|
[MaintenanceModeIgnore]
|
2024-12-26 08:51:22 +03:00
|
|
|
|
public async Task<ContentResult> OAuth2([FromQuery] string? code, [FromQuery] string? state)
|
2024-12-18 07:40:07 +03:00
|
|
|
|
{
|
2024-12-22 05:25:19 +03:00
|
|
|
|
var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
2024-12-18 07:40:07 +03:00
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
|
|
|
|
|
return Content(GenerateHtmlResponse(
|
|
|
|
|
"Ошибка передачи данных!",
|
|
|
|
|
"Провайдер OAuth не передал нужных данных.",
|
|
|
|
|
null,
|
|
|
|
|
traceId,
|
|
|
|
|
true), "text/html");
|
|
|
|
|
|
|
|
|
|
var result = await oAuthService.LoginOAuth(HttpContext, GetCookieParams(),
|
2024-12-18 07:40:07 +03:00
|
|
|
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
string? callbackUrl = null;
|
2024-12-18 07:40:07 +03:00
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
if (result.Callback != null)
|
|
|
|
|
callbackUrl = result.Callback + (result.Callback.Query.Length > 0 ? "&" : "?") +
|
|
|
|
|
$"result={Uri.EscapeDataString(result.Token)}";
|
2024-12-18 07:40:07 +03:00
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
string title, message;
|
2024-12-18 07:40:07 +03:00
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
if (!result.Success)
|
|
|
|
|
{
|
|
|
|
|
if (callbackUrl != null)
|
|
|
|
|
callbackUrl += $"&traceId={Uri.EscapeDataString(traceId)}";
|
2024-12-18 07:40:07 +03:00
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
title = "Ошибка авторизации!";
|
|
|
|
|
message = result.ErrorMessage ?? "Произошла ошибка. Попробуйте ещё раз.";
|
2024-12-18 07:40:07 +03:00
|
|
|
|
}
|
2024-12-26 08:51:22 +03:00
|
|
|
|
else
|
2024-12-18 07:40:07 +03:00
|
|
|
|
{
|
2024-12-26 08:51:22 +03:00
|
|
|
|
title = "Авторизация завершена!";
|
|
|
|
|
message = "Вы будете перенаправлены обратно через несколько секунд.";
|
2024-12-18 07:40:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-12-26 08:51:22 +03:00
|
|
|
|
return Content(GenerateHtmlResponse(
|
|
|
|
|
title,
|
|
|
|
|
message,
|
|
|
|
|
callbackUrl == null ? null : new Uri(callbackUrl),
|
|
|
|
|
traceId,
|
|
|
|
|
!result.Success), "text/html");
|
2024-12-18 07:40:07 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-04 03:14:42 +03:00
|
|
|
|
/// <summary>
|
2024-12-18 07:23:23 +03:00
|
|
|
|
/// Initiates the OAuth2 authorization process for the selected provider.
|
2024-11-04 03:14:42 +03:00
|
|
|
|
/// </summary>
|
2024-12-18 07:23:23 +03:00
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This method generates a redirect URL for the selected provider and redirects the user to it.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <param name="provider">The identifier of the OAuth provider to authorize with.</param>
|
2024-12-26 08:47:56 +03:00
|
|
|
|
/// <param name="callback">The address where the user will need to be redirected after the end of communication with the OAuth provider</param>
|
2024-12-18 07:23:23 +03:00
|
|
|
|
/// <returns>A redirect to the OAuth provider's authorization URL.</returns>
|
|
|
|
|
/// <exception cref="ControllerArgumentException">Thrown if the specified provider is not valid.</exception>
|
|
|
|
|
[HttpGet("AuthorizeOAuth2")]
|
|
|
|
|
[MaintenanceModeIgnore]
|
2024-12-26 08:47:56 +03:00
|
|
|
|
public ActionResult AuthorizeOAuth2([FromQuery] int provider, [FromQuery] Uri callback)
|
2024-12-18 07:23:23 +03:00
|
|
|
|
{
|
|
|
|
|
if (!Enum.IsDefined(typeof(OAuthProvider), provider))
|
|
|
|
|
throw new ControllerArgumentException("There is no selected provider");
|
|
|
|
|
|
2024-12-26 08:47:56 +03:00
|
|
|
|
if (!callback.IsAbsoluteUri)
|
|
|
|
|
throw new ControllerArgumentException("The callback URL must be absolute.");
|
|
|
|
|
|
|
|
|
|
return Redirect(oAuthService.GetProviderRedirect(HttpContext, GetCookieParams(),
|
|
|
|
|
HttpContext.GetApiUrl(Url.Action("OAuth2")!),
|
|
|
|
|
(OAuthProvider)provider,
|
|
|
|
|
callback).AbsoluteUri);
|
2024-12-18 07:23:23 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves a list of available OAuth providers with their corresponding authorization URLs.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <remarks>
|
|
|
|
|
/// This allows the client to fetch all possible OAuth options and the URLs required to initiate authorization.
|
|
|
|
|
/// </remarks>
|
|
|
|
|
/// <returns>A list of available providers and their redirect URLs.</returns>
|
|
|
|
|
[HttpGet("AvailableProviders")]
|
|
|
|
|
[MaintenanceModeIgnore]
|
2024-12-26 08:47:56 +03:00
|
|
|
|
public ActionResult<List<AvailableOAuthProvidersResponse>> AvailableProviders([FromQuery] Uri callback) =>
|
2024-11-04 02:59:51 +03:00
|
|
|
|
Ok(oAuthService
|
2024-12-22 07:25:41 +03:00
|
|
|
|
.GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!))
|
2024-12-26 08:47:56 +03:00
|
|
|
|
.Select(x =>
|
|
|
|
|
{
|
|
|
|
|
if (!callback.IsAbsoluteUri)
|
|
|
|
|
throw new ControllerArgumentException("The callback URL must be absolute.");
|
|
|
|
|
|
|
|
|
|
x.Redirect = new Uri(x.Redirect + "&callback=" + Uri.EscapeDataString(callback.AbsoluteUri));
|
|
|
|
|
return x;
|
|
|
|
|
})
|
2024-11-04 02:59:51 +03:00
|
|
|
|
.ConvertToDto());
|
|
|
|
|
|
2024-11-04 03:14:42 +03:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Logs in a user using their username or email and password.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="request">The login request containing username/email and password.</param>
|
|
|
|
|
/// <returns>A TwoFactorAuthentication token if the login is successful; otherwise, a BadRequest response.</returns>
|
2024-06-21 21:52:21 +03:00
|
|
|
|
[HttpPost("Login")]
|
2024-10-09 03:00:26 +03:00
|
|
|
|
[BadRequestResponse]
|
2024-11-02 00:50:10 +03:00
|
|
|
|
public async Task<ActionResult<TwoFactorAuthentication>> Login([FromBody] LoginRequest request)
|
2024-06-21 21:52:21 +03:00
|
|
|
|
{
|
|
|
|
|
var userEntity = user.Value;
|
|
|
|
|
|
2024-10-31 04:12:02 +03:00
|
|
|
|
var tokenResult = await auth.LoginAsync(
|
|
|
|
|
GetCookieParams(),
|
|
|
|
|
new User
|
|
|
|
|
{
|
2024-11-02 22:09:40 +03:00
|
|
|
|
Id = 1.ToString(),
|
2024-10-31 04:12:02 +03:00
|
|
|
|
Username = userEntity.Username,
|
|
|
|
|
Email = userEntity.Email,
|
|
|
|
|
PasswordHash = userEntity.PasswordHash,
|
|
|
|
|
Salt = userEntity.Salt,
|
2024-11-02 00:50:10 +03:00
|
|
|
|
TwoFactorAuthenticator = userEntity.TwoFactorAuthenticator,
|
2024-11-04 02:39:10 +03:00
|
|
|
|
SecondFactorToken = userEntity.Secret,
|
|
|
|
|
OAuthProviders = userEntity.OAuthProviders
|
2024-10-31 04:12:02 +03:00
|
|
|
|
},
|
2024-12-22 05:13:46 +03:00
|
|
|
|
HttpContext, request.Password, request.Username);
|
2024-10-31 04:12:02 +03:00
|
|
|
|
|
2024-11-02 01:06:58 +03:00
|
|
|
|
return Ok(tokenResult.ConvertToDto());
|
2024-10-31 04:12:02 +03:00
|
|
|
|
}
|
2024-06-21 21:52:21 +03:00
|
|
|
|
|
2024-11-04 03:14:42 +03:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Performs two-factor authentication for the user.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="request">The request containing the method and code for two-factor authentication.</param>
|
|
|
|
|
/// <returns>A boolean indicating whether the two-factor authentication was successful.</returns>
|
2024-11-02 01:09:15 +03:00
|
|
|
|
[HttpPost("2FA")]
|
2024-10-31 04:12:02 +03:00
|
|
|
|
[BadRequestResponse]
|
2024-11-02 22:09:40 +03:00
|
|
|
|
public async Task<ActionResult<bool>> TwoFactorAuth([FromBody] TwoFactorAuthRequest request)
|
2024-10-31 04:12:02 +03:00
|
|
|
|
{
|
2024-11-02 01:09:15 +03:00
|
|
|
|
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code);
|
2024-11-02 22:09:40 +03:00
|
|
|
|
return Ok(tokenResult);
|
2024-06-21 21:52:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 22:55:18 +03:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Refreshes the authentication token using the existing refresh token.
|
|
|
|
|
/// </summary>
|
2024-10-09 03:00:26 +03:00
|
|
|
|
/// <returns>User's AuthRoles.</returns>
|
2024-06-21 21:52:21 +03:00
|
|
|
|
[HttpGet("ReLogin")]
|
|
|
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
2024-10-09 03:00:26 +03:00
|
|
|
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
|
|
|
public async Task<ActionResult<AuthRoles>> ReLogin()
|
2024-06-21 21:52:21 +03:00
|
|
|
|
{
|
2024-10-31 04:12:02 +03:00
|
|
|
|
await auth.RefreshTokenAsync(GetCookieParams(), HttpContext);
|
|
|
|
|
return Ok(AuthRoles.Admin);
|
2024-06-21 21:52:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 22:55:18 +03:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Logs the user out by clearing the refresh token and performing any necessary cleanup.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>An Ok response if the logout was successful.</returns>
|
2024-06-21 21:52:21 +03:00
|
|
|
|
[HttpGet("Logout")]
|
|
|
|
|
public async Task<ActionResult> Logout()
|
|
|
|
|
{
|
2024-10-31 04:12:02 +03:00
|
|
|
|
await auth.LogoutAsync(GetCookieParams(), HttpContext);
|
2024-06-21 21:52:21 +03:00
|
|
|
|
return Ok();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-28 22:55:18 +03:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Retrieves the role of the authenticated user.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns>The role of the authenticated user.</returns>
|
2024-06-21 21:52:21 +03:00
|
|
|
|
[HttpGet("GetRole")]
|
|
|
|
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
|
|
|
|
[Authorize]
|
|
|
|
|
public ActionResult<AuthRoles> GetRole() => Ok(AuthRoles.Admin);
|
2024-08-27 22:52:07 +03:00
|
|
|
|
|
|
|
|
|
[HttpPost("RenewPassword")]
|
|
|
|
|
[ApiExplorerSettings(IgnoreApi = true)]
|
|
|
|
|
[Localhost]
|
|
|
|
|
[BadRequestResponse]
|
|
|
|
|
public ActionResult<string> RenewPassword([FromBody] string? password = null)
|
|
|
|
|
{
|
2024-12-18 07:27:57 +03:00
|
|
|
|
var passwordPolicy = generalConfig.Value.PasswordPolicy;
|
|
|
|
|
var passwordPolicyService = new PasswordPolicyService(passwordPolicy);
|
|
|
|
|
|
2024-08-27 22:52:07 +03:00
|
|
|
|
if (string.IsNullOrEmpty(password))
|
|
|
|
|
password = string.Empty;
|
2024-12-18 07:27:57 +03:00
|
|
|
|
else
|
|
|
|
|
passwordPolicyService.ValidatePasswordOrThrow(password);
|
2024-08-27 22:52:07 +03:00
|
|
|
|
|
2024-12-18 07:27:57 +03:00
|
|
|
|
while (!passwordPolicyService.TryValidatePassword(password))
|
|
|
|
|
password = GeneratorKey.GenerateAlphaNumeric(passwordPolicy.MinimumLength + 2, includes: "!@#%^");
|
2024-08-27 22:52:07 +03:00
|
|
|
|
|
|
|
|
|
var (salt, hash) = passwordService.HashPassword(password);
|
|
|
|
|
|
|
|
|
|
var admin = user.Value;
|
|
|
|
|
|
|
|
|
|
admin.Salt = salt;
|
|
|
|
|
admin.PasswordHash = hash;
|
|
|
|
|
admin.SaveSetting();
|
|
|
|
|
|
|
|
|
|
return Ok(password);
|
|
|
|
|
}
|
2024-06-21 21:52:21 +03:00
|
|
|
|
}
|