using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.EntityFrameworkCore; using DysonNetwork.Sphere.Auth; using DysonNetwork.Sphere.Account; using NodaTime; namespace DysonNetwork.Sphere.Pages.Auth { public class VerifyFactorModel : PageModel { private readonly AppDatabase _db; private readonly AccountService _accounts; private readonly AuthService _auth; private readonly ActionLogService _als; private readonly IConfiguration _configuration; private readonly IHttpClientFactory _httpClientFactory; [BindProperty(SupportsGet = true)] public Guid Id { get; set; } [BindProperty(SupportsGet = true)] public Guid FactorId { get; set; } [BindProperty, Required] public string Code { get; set; } = string.Empty; public Challenge? AuthChallenge { get; set; } public AccountAuthFactor? Factor { get; set; } public AccountAuthFactorType FactorType => Factor?.Type ?? AccountAuthFactorType.EmailCode; public VerifyFactorModel( AppDatabase db, AccountService accounts, AuthService auth, ActionLogService als, IConfiguration configuration, IHttpClientFactory httpClientFactory) { _db = db; _accounts = accounts; _auth = auth; _als = als; _configuration = configuration; _httpClientFactory = httpClientFactory; } public async Task OnGetAsync() { await LoadChallengeAndFactor(); if (AuthChallenge == null) return NotFound("Challenge not found or expired."); if (Factor == null) return NotFound("Authentication method not found."); if (AuthChallenge.StepRemain == 0) return await ExchangeTokenAndRedirect(); return Page(); } public async Task OnPostAsync() { if (!ModelState.IsValid) { await LoadChallengeAndFactor(); return Page(); } await LoadChallengeAndFactor(); if (AuthChallenge == null) return NotFound("Challenge not found or expired."); if (Factor == null) return NotFound("Authentication method not found."); try { if (await _accounts.VerifyFactorCode(Factor, Code)) { AuthChallenge.StepRemain -= Factor.Trustworthy; AuthChallenge.StepRemain = Math.Max(0, AuthChallenge.StepRemain); AuthChallenge.BlacklistFactors.Add(Factor.Id); _db.Update(AuthChallenge); _als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, new Dictionary { { "challenge_id", AuthChallenge.Id }, { "factor_id", Factor?.Id.ToString() ?? string.Empty } }, Request, AuthChallenge.Account); await _db.SaveChangesAsync(); if (AuthChallenge.StepRemain == 0) { _als.CreateActionLogFromRequest(ActionLogType.NewLogin, new Dictionary { { "challenge_id", AuthChallenge.Id }, { "account_id", AuthChallenge.AccountId } }, Request, AuthChallenge.Account); return await ExchangeTokenAndRedirect(); } else { // If more steps are needed, redirect back to select factor return RedirectToPage("SelectFactor", new { id = Id }); } } else { throw new InvalidOperationException("Invalid verification code."); } } catch (Exception ex) { if (AuthChallenge != null) { AuthChallenge.FailedAttempts++; _db.Update(AuthChallenge); await _db.SaveChangesAsync(); _als.CreateActionLogFromRequest(ActionLogType.ChallengeFailure, new Dictionary { { "challenge_id", AuthChallenge.Id }, { "factor_id", Factor?.Id.ToString() ?? string.Empty } }, Request, AuthChallenge.Account); } ModelState.AddModelError(string.Empty, ex.Message); return Page(); } } private async Task LoadChallengeAndFactor() { AuthChallenge = await _db.AuthChallenges .Include(e => e.Account) .FirstOrDefaultAsync(e => e.Id == Id); if (AuthChallenge?.Account != null) { Factor = await _db.AccountAuthFactors .FirstOrDefaultAsync(e => e.Id == FactorId && e.AccountId == AuthChallenge.Account.Id && e.EnabledAt != null && e.Trustworthy > 0); } } private async Task ExchangeTokenAndRedirect() { var challenge = await _db.AuthChallenges .Include(e => e.Account) .FirstOrDefaultAsync(e => e.Id == Id); if (challenge == null) return BadRequest("Authorization code not found or expired."); if (challenge.StepRemain != 0) return BadRequest("Challenge not yet completed."); var session = await _db.AuthSessions .FirstOrDefaultAsync(e => e.ChallengeId == challenge.Id); if (session != null) return BadRequest("Session already exists for this challenge."); session = new Session { LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow), ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)), Account = challenge.Account, Challenge = challenge, }; _db.AuthSessions.Add(session); await _db.SaveChangesAsync(); var token = _auth.CreateToken(session); Response.Cookies.Append(AuthConstants.CookieTokenName, token, new() { HttpOnly = true, Secure = !_configuration.GetValue("Debug"), SameSite = SameSiteMode.Strict, Path = "/" }); return RedirectToPage("/Account/Profile"); } } }