diff --git a/Controllers/AccountController.cs b/Controllers/AccountController.cs index f7896ba..9094659 100644 --- a/Controllers/AccountController.cs +++ b/Controllers/AccountController.cs @@ -7,6 +7,9 @@ using Microsoft.AspNetCore.Mvc; using QRRapidoApp.Models.ViewModels; using QRRapidoApp.Services; using System.Security.Claims; +using System.Text.Json; +using System.Text; +using Microsoft.AspNetCore.DataProtection; namespace QRRapidoApp.Controllers { @@ -16,14 +19,17 @@ namespace QRRapidoApp.Controllers private readonly AdDisplayService _adDisplayService; private readonly ILogger _logger; private readonly IConfiguration _configuration; + private readonly IDataProtector _protector; public AccountController(IUserService userService, AdDisplayService adDisplayService, - ILogger logger, IConfiguration configuration) + ILogger logger, IConfiguration configuration, + IDataProtectionProvider dataProtection) { _userService = userService; _adDisplayService = adDisplayService; _logger = logger; _configuration = configuration; + _protector = dataProtection.CreateProtector("OAuth.StateProtection"); } [HttpGet] @@ -38,11 +44,25 @@ namespace QRRapidoApp.Controllers public IActionResult LoginGoogle(string returnUrl = "/") { 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 { RedirectUri = $"{baseUrl}{Url.Action("GoogleCallback")}", - Items = { { "returnUrl", returnUrl } } + Items = { { "state", encodedState } } }; + return Challenge(properties, GoogleDefaults.AuthenticationScheme); } @@ -50,40 +70,84 @@ namespace QRRapidoApp.Controllers public IActionResult LoginMicrosoft(string returnUrl = "/") { 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); } [HttpGet] - public async Task GoogleCallback() + public async Task GoogleCallback(string state = null) { - return await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme); + var returnUrl = await HandleExternalLoginCallbackAsync(GoogleDefaults.AuthenticationScheme, state); + return Redirect(returnUrl ?? "/"); } [HttpGet] - public async Task MicrosoftCallback() + public async Task MicrosoftCallback(string state = null) { - return await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme); + var returnUrl = await HandleExternalLoginCallbackAsync(MicrosoftAccountDefaults.AuthenticationScheme, state); + return Redirect(returnUrl ?? "/"); } - private async Task HandleExternalLoginCallbackAsync(string scheme) + private async Task HandleExternalLoginCallbackAsync(string scheme, string state = null) { try { _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(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); if (!result.Succeeded) { _logger.LogWarning($"External authentication failed for scheme {scheme}"); - return RedirectToAction("Login"); + return "/Account/Login"; } var email = result.Principal?.FindFirst(ClaimTypes.Email)?.Value; @@ -93,7 +157,7 @@ namespace QRRapidoApp.Controllers if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(providerId)) { _logger.LogWarning($"Missing required claims from {scheme} authentication"); - return RedirectToAction("Login"); + return "/Account/Login"; } // Find or create user @@ -127,13 +191,12 @@ namespace QRRapidoApp.Controllers await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), authProperties); - var returnUrl = result.Properties?.Items != null && result.Properties.Items.ContainsKey("returnUrl") ? result.Properties?.Items["returnUrl"] : "/"; - return RedirectToAction("Index", "Home"); + return returnUrl; } catch (Exception ex) { _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 }); } } + + // Classe para dados do state + public class OAuthStateData + { + public string ReturnUrl { get; set; } = "/"; + public string Nonce { get; set; } = ""; + public long Timestamp { get; set; } + } } \ No newline at end of file