fix: authcontroller
All checks were successful
Deploy QR Rapido / test (push) Successful in 54s
Deploy QR Rapido / build-and-push (push) Successful in 7m1s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m36s

This commit is contained in:
Ricardo Carneiro 2025-08-25 00:02:40 -03:00
parent b8ab7948a9
commit c59e6711c0

View File

@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc;
using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Models.ViewModels;
using QRRapidoApp.Services; using QRRapidoApp.Services;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
namespace QRRapidoApp.Controllers namespace QRRapidoApp.Controllers
{ {
@ -16,14 +19,17 @@ namespace QRRapidoApp.Controllers
private readonly AdDisplayService _adDisplayService; private readonly AdDisplayService _adDisplayService;
private readonly ILogger<AccountController> _logger; private readonly ILogger<AccountController> _logger;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly IDataProtector _protector;
public AccountController(IUserService userService, AdDisplayService adDisplayService, public AccountController(IUserService userService, AdDisplayService adDisplayService,
ILogger<AccountController> logger, IConfiguration configuration) ILogger<AccountController> logger, IConfiguration configuration,
IDataProtectionProvider dataProtection)
{ {
_userService = userService; _userService = userService;
_adDisplayService = adDisplayService; _adDisplayService = adDisplayService;
_logger = logger; _logger = logger;
_configuration = configuration; _configuration = configuration;
_protector = dataProtection.CreateProtector("OAuth.StateProtection");
} }
[HttpGet] [HttpGet]
@ -38,11 +44,25 @@ namespace QRRapidoApp.Controllers
public IActionResult LoginGoogle(string returnUrl = "/") public IActionResult LoginGoogle(string returnUrl = "/")
{ {
var baseUrl = _configuration.GetSection("App:BaseUrl").Value; var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
// Criar state com dados criptografados em vez de sessão
var stateData = new OAuthStateData
{
ReturnUrl = returnUrl,
Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var properties = new AuthenticationProperties var properties = new AuthenticationProperties
{ {
RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}", RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}",
Items = { { "returnUrl", returnUrl } } Items = { { "state", encodedState } }
}; };
return Challenge(properties, GoogleDefaults.AuthenticationScheme); return Challenge(properties, GoogleDefaults.AuthenticationScheme);
} }
@ -50,40 +70,84 @@ namespace QRRapidoApp.Controllers
public IActionResult LoginMicrosoft(string returnUrl = "/") public IActionResult LoginMicrosoft(string returnUrl = "/")
{ {
var baseUrl = _configuration.GetSection("App:BaseUrl").Value; var baseUrl = _configuration.GetSection("App:BaseUrl").Value;
//var redirectUrl = Url.Action("MicrosoftCallback", "Account", new { returnUrl });
var redirectUrl = "";
if (returnUrl == "/") // Mesmo processo para Microsoft
var stateData = new OAuthStateData
{ {
redirectUrl = $"{baseUrl}/Account/MicrosoftCallback"; ReturnUrl = returnUrl,
} Nonce = Guid.NewGuid().ToString(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var stateJson = JsonSerializer.Serialize(stateData);
var protectedState = _protector.Protect(stateJson);
var encodedState = Convert.ToBase64String(Encoding.UTF8.GetBytes(protectedState));
var redirectUrl = returnUrl == "/"
? $"{baseUrl}/Account/MicrosoftCallback"
: $"{baseUrl}/Account/MicrosoftCallback";
var properties = new AuthenticationProperties
{
RedirectUri = redirectUrl,
Items = { { "state", encodedState } }
};
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> GoogleCallback() public async Task<IActionResult> GoogleCallback(string state = null)
{ {
return await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme); var returnUrl = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
} }
[HttpGet] [HttpGet]
public async Task<IActionResult> MicrosoftCallback() public async Task<IActionResult> MicrosoftCallback(string state = null)
{ {
return await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme); var returnUrl = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, state);
return Redirect(returnUrl ?? "/");
} }
private async Task<IActionResult> HandleExternalLoginCallbackAsync(string scheme) private async Task<string> HandleExternalLoginCallbackAsync(string scheme, string state = null)
{ {
try try
{ {
_adDisplayService.SetViewBagAds(ViewBag); _adDisplayService.SetViewBagAds(ViewBag);
// Recuperar returnUrl do state em vez da sessão
string returnUrl = "/";
if (!string.IsNullOrEmpty(state))
{
try
{
var decodedState = Encoding.UTF8.GetString(Convert.FromBase64String(state));
var unprotectedState = _protector.Unprotect(decodedState);
var stateData = JsonSerializer.Deserialize<OAuthStateData>(unprotectedState);
// Validar timestamp (não mais que 10 minutos)
var maxAge = DateTimeOffset.UtcNow.ToUnixTimeSeconds() - (10 * 60);
if (stateData.Timestamp > maxAge)
{
returnUrl = stateData.ReturnUrl ?? "/";
}
else
{
_logger.LogWarning($"OAuth state expired for scheme {scheme}");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to decode OAuth state for scheme {scheme}");
}
}
var result = await HttpContext.AuthenticateAsync(scheme); var result = await HttpContext.AuthenticateAsync(scheme);
if (!result.Succeeded) if (!result.Succeeded)
{ {
_logger.LogWarning($"External authentication failed for scheme {scheme}"); _logger.LogWarning($"External authentication failed for scheme {scheme}");
return RedirectToAction("Login"); return "/Account/Login";
} }
var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value; var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value;
@ -93,7 +157,7 @@ namespace QRRapidoApp.Controllers
if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId)) if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId))
{ {
_logger.LogWarning($"Missing required claims from {scheme} authentication"); _logger.LogWarning($"Missing required claims from {scheme} authentication");
return RedirectToAction("Login"); return "/Account/Login";
} }
// Find or create user // Find or create user
@ -127,13 +191,12 @@ namespace QRRapidoApp.Controllers
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity), authProperties); new ClaimsPrincipal(claimsIdentity), authProperties);
var returnUrl = result.Properties?.Items != null && result.Properties.Items.ContainsKey("returnUrl") ? result.Properties?.Items["returnUrl"] : "/"; return returnUrl;
return RedirectToAction("Index", "Home");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, $"Error in external login callback for {scheme}"); _logger.LogError(ex, $"Error in external login callback for {scheme}");
return RedirectToAction("Login"); return "/Account/Login";
} }
} }
@ -252,4 +315,12 @@ namespace QRRapidoApp.Controllers
return Json(new { success = false }); return Json(new { success = false });
} }
} }
// Classe para dados do state
public class OAuthStateData
{
public string ReturnUrl { get; set; } = "/";
public string Nonce { get; set; } = "";
public long Timestamp { get; set; }
}
} }