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.Model;
using Mirea.Api.Security.Services;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CookieOptions = Mirea.Api.Security.Common.Model.CookieOptions;
using OAuthProvider = Mirea.Api.Security.Common.Domain.OAuthProvider;
namespace Mirea.Api.Endpoint.Controllers.V1;
[ApiVersion("1.0")]
public class AuthController(IOptionsSnapshot user, IOptionsSnapshot generalConfig, AuthService auth,
PasswordHashService passwordService, OAuthService oAuthService) : BaseController
{
private CookieOptions GetCookieParams() =>
new()
{
Domain = HttpContext.GetCurrentDomain(),
Path = UrlHelper.GetSubPathWithoutFirstApiName + "api"
};
private static string GenerateHtmlResponse(
string title,
string message,
Uri? callback,
string traceId,
bool isError)
{
var callbackUrl = callback?.ToString();
var script = callback == null ? string.Empty :
$"";
var blockInfo = "
" + (callback == null ?
"Вернитесь обратно и попробуйте снова позже.
" :
$"Если вы не будете автоматически перенаправлены, нажмите ниже.
{script}";
}
///
/// Handles the callback from an OAuth2 provider and finalizes the authorization process.
///
///
/// This method processes the response from an OAuth provider after the user authorizes the application.
/// Upon successful authorization, it redirects the user back to the specified callback URL.
///
/// The authorization code returned by the OAuth provider.
/// The state parameter to ensure the request's integrity and prevent CSRF attacks.
///
/// An HTML response indicating the success or failure of the authorization process.
/// If a callback URL is provided, the user will be redirected to it.
///
[HttpGet("OAuth2")]
[BadRequestResponse]
[Produces("text/html")]
[MaintenanceModeIgnore]
public async Task OAuth2([FromQuery] string? code, [FromQuery] string? state)
{
var traceId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
return Content(GenerateHtmlResponse(
"Ошибка передачи данных!",
"Провайдер OAuth не передал нужных данных.",
null,
traceId,
true), "text/html");
var result = await oAuthService.LoginOAuth(GetCookieParams(), HttpContext,
HttpContext.GetApiUrl(Url.Action("OAuth2")!), code, state);
string? callbackUrl = null;
if (result.Callback != null)
callbackUrl = result.Callback + (result.Callback.Query.Length > 0 ? "&" : "?") +
$"result={Uri.EscapeDataString(result.Token)}";
string title, message;
if (!result.Success)
{
if (callbackUrl != null)
callbackUrl += $"&traceId={Uri.EscapeDataString(traceId)}";
title = "Ошибка авторизации!";
message = result.ErrorMessage ?? "Произошла ошибка. Попробуйте ещё раз.";
}
else
{
title = "Авторизация завершена!";
message = "Вы будете перенаправлены обратно через несколько секунд.";
}
return Content(GenerateHtmlResponse(
title,
message,
callbackUrl == null ? null : new Uri(callbackUrl),
traceId,
!result.Success), "text/html");
}
///
/// Initiates the OAuth2 authorization process for the selected provider.
///
///
/// This method generates a redirect URL for the selected provider and redirects the user to it.
///
/// The identifier of the OAuth provider to authorize with.
/// The address where the user will need to be redirected after the end of communication with the OAuth provider
/// A redirect to the OAuth provider's authorization URL.
/// Thrown if the specified provider is not valid.
[HttpGet("AuthorizeOAuth2")]
[MaintenanceModeIgnore]
public ActionResult AuthorizeOAuth2([FromQuery] int provider, [FromQuery] Uri callback)
{
if (!Enum.IsDefined(typeof(OAuthProvider), provider))
throw new ControllerArgumentException("There is no selected provider");
if (!callback.IsAbsoluteUri)
throw new ControllerArgumentException("The callback URL must be absolute.");
return Redirect(oAuthService.GetProviderRedirect(GetCookieParams(), HttpContext,
HttpContext.GetApiUrl(Url.Action("OAuth2")!),
(OAuthProvider)provider,
callback).AbsoluteUri);
}
///
/// Retrieves a list of available OAuth providers with their corresponding authorization URLs.
///
///
/// This allows the client to fetch all possible OAuth options and the URLs required to initiate authorization.
///
/// A list of available providers and their redirect URLs.
[HttpGet("AvailableProviders")]
[MaintenanceModeIgnore]
public ActionResult> AvailableProviders([FromQuery] Uri callback) =>
Ok(oAuthService
.GetAvailableProviders(HttpContext.GetApiUrl(Url.Action("AuthorizeOAuth2")!))
.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;
})
.ConvertToDto());
///
/// Logs in a user using their username or email and password.
///
/// The login request containing username/email and password.
/// A TwoFactorAuthentication token if the login is successful; otherwise, a BadRequest response.
[HttpPost("Login")]
[BadRequestResponse]
public async Task> Login([FromBody] LoginRequest request)
{
var userEntity = user.Value;
var tokenResult = await auth.LoginAsync(
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
},
request.Password, request.Username);
return Ok(tokenResult.ConvertToDto());
}
///
/// Performs two-factor authentication for the user.
///
/// The request containing the method and code for two-factor authentication.
/// A boolean indicating whether the two-factor authentication was successful.
[HttpPost("2FA")]
[BadRequestResponse]
public async Task> TwoFactorAuth([FromBody] TwoFactorAuthRequest request)
{
var tokenResult = await auth.LoginAsync(GetCookieParams(), HttpContext, request.Method.ConvertFromDto(), request.Code);
return Ok(tokenResult);
}
///
/// Refreshes the authentication token using the existing refresh token.
///
/// User's AuthRoles.
[HttpGet("ReLogin")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task> ReLogin()
{
await auth.RefreshTokenAsync(GetCookieParams(), HttpContext);
return Ok(AuthRoles.Admin);
}
///
/// Logs the user out by clearing the refresh token and performing any necessary cleanup.
///
/// An Ok response if the logout was successful.
[HttpGet("Logout")]
public async Task Logout()
{
await auth.LogoutAsync(GetCookieParams(), HttpContext);
return Ok();
}
///
/// Retrieves the role of the authenticated user.
///
/// The role of the authenticated user.
[HttpGet("GetRole")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[Authorize]
public ActionResult GetRole() => Ok(AuthRoles.Admin);
[HttpPost("RenewPassword")]
[ApiExplorerSettings(IgnoreApi = true)]
[Localhost]
[BadRequestResponse]
public ActionResult 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);
}
}