261 lines
12 KiB
C#
261 lines
12 KiB
C#
using Asp.Versioning;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
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.MapperDto;
|
||
using Mirea.Api.Endpoint.Common.Services;
|
||
using Mirea.Api.Endpoint.Configuration.Model;
|
||
using Mirea.Api.Security.Common.Domain;
|
||
using Mirea.Api.Security.Services;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Diagnostics;
|
||
using System.Security.Claims;
|
||
using System.Threading.Tasks;
|
||
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
|
||
|
||
namespace Mirea.Api.Endpoint.Controllers.V1;
|
||
|
||
[ApiVersion("1.0")]
|
||
public class AuthController(IOptionsSnapshot<Admin> user, IOptionsSnapshot<GeneralConfig> generalConfig, AuthService auth, PasswordHashService passwordService, OAuthService oAuthService) : BaseController
|
||
{
|
||
private CookieOptionsParameters GetCookieParams() =>
|
||
new()
|
||
{
|
||
Domain = HttpContext.GetCurrentDomain(),
|
||
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
|
||
};
|
||
|
||
private static string GenerateHtmlResponse(string title, string message, OAuthProvider? provider, bool isError = false, string? traceId = null)
|
||
{
|
||
string messageColor = isError ? "red" : "white";
|
||
string script = "<script>setTimeout(()=>{if(window.opener){window.opener.postMessage(" +
|
||
"{success:" + (!isError).ToString().ToLower() +
|
||
",provider:'" + (provider == null ? "null" : (int)provider) +
|
||
"',providerName:'" + (provider == null ? "null" : Enum.GetName(provider.Value)) +
|
||
"',message:'" + message.Replace("'", "\\'") +
|
||
"'},'*');}window.close();}, 15000);</script>";
|
||
|
||
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}}p{{font-size:16px;color:{messageColor}}}</style><title>{title}</title></head><body><div class=container><h1>{title}</h1><p>{message}</p><p style=font-size:14px;color:silver;>Это информационная страница. Вы можете закрыть её.</p>{(!string.IsNullOrEmpty(traceId) ? $"<code style=font-size:12px;color:gray;>TraceId={traceId}</code>" : string.Empty)}</div>{script}</body></html>";
|
||
}
|
||
|
||
[HttpGet("OAuth2")]
|
||
[BadRequestResponse]
|
||
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
|
||
[Produces("text/html")]
|
||
[MaintenanceModeIgnore]
|
||
public async Task<ContentResult> OAuth2([FromQuery] string code, [FromQuery] string state)
|
||
{
|
||
var userId = HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
string title;
|
||
string message;
|
||
OAuthProvider provider;
|
||
OAuthUser oAuthUser;
|
||
var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
|
||
|
||
try
|
||
{
|
||
(provider, oAuthUser) = await oAuthService.LoginOAuth(HttpContext, GetCookieParams(),
|
||
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
title = "Произошла ошибка при общении с провайдером OAuth!";
|
||
message = e.Message;
|
||
return Content(GenerateHtmlResponse(title, message, null, true, traceId), "text/html");
|
||
}
|
||
|
||
var userEntity = user.Value;
|
||
|
||
if (userId != null)
|
||
{
|
||
userEntity.OAuthProviders ??= [];
|
||
|
||
if (!userEntity.OAuthProviders.TryAdd(provider, oAuthUser))
|
||
{
|
||
title = "Ошибка связи аккаунта!";
|
||
message = "Этот OAuth провайдер уже связан с вашей учетной записью. Пожалуйста, используйте другого провайдера или удалите связь с аккаунтом.";
|
||
return Content(GenerateHtmlResponse(title, message, provider, true, traceId), "text/html");
|
||
}
|
||
|
||
userEntity.SaveSetting();
|
||
|
||
title = "Учетная запись успешно связана.";
|
||
message = "Вы успешно связали свою учетную запись с провайдером OAuth. Вы можете продолжить использовать приложение.";
|
||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
||
}
|
||
|
||
if (userEntity.OAuthProviders != null &&
|
||
userEntity.OAuthProviders.TryGetValue(provider, out var userOAuth) &&
|
||
userOAuth.Id == oAuthUser.Id)
|
||
{
|
||
await auth.LoginOAuthAsync(GetCookieParams(), HttpContext, new User
|
||
{
|
||
Id = 1.ToString(),
|
||
Username = userEntity.Username,
|
||
Email = userEntity.Email,
|
||
PasswordHash = userEntity.PasswordHash,
|
||
Salt = userEntity.Salt,
|
||
TwoFactorAuthenticator = userEntity.TwoFactorAuthenticator,
|
||
SecondFactorToken = userEntity.Secret,
|
||
OAuthProviders = userEntity.OAuthProviders
|
||
});
|
||
|
||
title = "Успешный вход в аккаунт.";
|
||
message = "Вы успешно вошли в свою учетную запись. Добро пожаловать!";
|
||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
||
}
|
||
|
||
title = "Вы успешно зарегистрированы.";
|
||
message = "Процесс завершен. Вы можете закрыть эту страницу.";
|
||
userEntity.Email = string.IsNullOrEmpty(oAuthUser.Email) ? string.Empty : oAuthUser.Email;
|
||
userEntity.Username = string.IsNullOrEmpty(oAuthUser.Username) ? string.Empty : oAuthUser.Username;
|
||
userEntity.OAuthProviders ??= [];
|
||
userEntity.OAuthProviders.Add(provider, oAuthUser);
|
||
userEntity.SaveSetting();
|
||
return Content(GenerateHtmlResponse(title, message, provider), "text/html");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Initiates the OAuth2 authorization process for the selected provider.
|
||
/// </summary>
|
||
/// <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>
|
||
/// <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]
|
||
public ActionResult AuthorizeOAuth2([FromQuery] int provider)
|
||
{
|
||
if (!Enum.IsDefined(typeof(OAuthProvider), provider))
|
||
throw new ControllerArgumentException("There is no selected provider");
|
||
|
||
return Redirect(oAuthService.GetProviderRedirect(HttpContext, GetCookieParams(), HttpContext.GetApiUrl(Url.Action("OAuth2")!), (OAuthProvider)provider).AbsoluteUri);
|
||
}
|
||
|
||
/// <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]
|
||
public ActionResult<List<AvailableOAuthProvidersResponse>> AvailableProviders() =>
|
||
Ok(oAuthService
|
||
.GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!))
|
||
.ConvertToDto());
|
||
|
||
/// <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>
|
||
[HttpPost("Login")]
|
||
[BadRequestResponse]
|
||
public async Task<ActionResult<TwoFactorAuthentication>> Login([FromBody] LoginRequest request)
|
||
{
|
||
var userEntity = user.Value;
|
||
|
||
var tokenResult = await auth.LoginAsync(
|
||
GetCookieParams(),
|
||
new User
|
||
{
|
||
Id = 1.ToString(),
|
||
Username = userEntity.Username,
|
||
Email = userEntity.Email,
|
||
PasswordHash = userEntity.PasswordHash,
|
||
Salt = userEntity.Salt,
|
||
TwoFactorAuthenticator = userEntity.TwoFactorAuthenticator,
|
||
SecondFactorToken = userEntity.Secret,
|
||
OAuthProviders = userEntity.OAuthProviders
|
||
},
|
||
HttpContext, request.Password, request.Username);
|
||
|
||
return Ok(tokenResult.ConvertToDto());
|
||
}
|
||
|
||
/// <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>
|
||
[HttpPost("2FA")]
|
||
[BadRequestResponse]
|
||
public async Task<ActionResult<bool>> TwoFactorAuth([FromBody] TwoFactorAuthRequest request)
|
||
{
|
||
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code);
|
||
return Ok(tokenResult);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Refreshes the authentication token using the existing refresh token.
|
||
/// </summary>
|
||
/// <returns>User's AuthRoles.</returns>
|
||
[HttpGet("ReLogin")]
|
||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||
public async Task<ActionResult<AuthRoles>> ReLogin()
|
||
{
|
||
await auth.RefreshTokenAsync(GetCookieParams(), HttpContext);
|
||
return Ok(AuthRoles.Admin);
|
||
}
|
||
|
||
/// <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>
|
||
[HttpGet("Logout")]
|
||
public async Task<ActionResult> Logout()
|
||
{
|
||
await auth.LogoutAsync(GetCookieParams(), HttpContext);
|
||
return Ok();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Retrieves the role of the authenticated user.
|
||
/// </summary>
|
||
/// <returns>The role of the authenticated user.</returns>
|
||
[HttpGet("GetRole")]
|
||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||
[Authorize]
|
||
public ActionResult<AuthRoles> GetRole() => Ok(AuthRoles.Admin);
|
||
|
||
[HttpPost("RenewPassword")]
|
||
[ApiExplorerSettings(IgnoreApi = true)]
|
||
[Localhost]
|
||
[BadRequestResponse]
|
||
public ActionResult<string> RenewPassword([FromBody] string? password = null)
|
||
{
|
||
var passwordPolicy = generalConfig.Value.PasswordPolicy;
|
||
var passwordPolicyService = new PasswordPolicyService(passwordPolicy);
|
||
|
||
if (string.IsNullOrEmpty(password))
|
||
password = string.Empty;
|
||
else
|
||
passwordPolicyService.ValidatePasswordOrThrow(password);
|
||
|
||
while (!passwordPolicyService.TryValidatePassword(password))
|
||
password = GeneratorKey.GenerateAlphaNumeric(passwordPolicy.MinimumLength + 2, includes: "!@#%^");
|
||
|
||
var (salt, hash) = passwordService.HashPassword(password);
|
||
|
||
var admin = user.Value;
|
||
|
||
admin.Salt = salt;
|
||
admin.PasswordHash = hash;
|
||
admin.SaveSetting();
|
||
|
||
return Ok(password);
|
||
}
|
||
}
|