diff --git a/DysonNetwork.Pass/Auth/AuthController.cs b/DysonNetwork.Pass/Auth/AuthController.cs index c58a8e2..5d00de5 100644 --- a/DysonNetwork.Pass/Auth/AuthController.cs +++ b/DysonNetwork.Pass/Auth/AuthController.cs @@ -172,15 +172,23 @@ public class AuthController( .FirstOrDefaultAsync(e => e.Id == id); if (challenge is null) return NotFound("Auth challenge was not found."); - var factor = await db.AccountAuthFactors.FindAsync(request.FactorId); + var factor = await db.AccountAuthFactors + .Where(f => f.Id == request.FactorId) + .Where(f => f.AccountId == challenge.AccountId) + .FirstOrDefaultAsync(); if (factor is null) return NotFound("Auth factor was not found."); if (factor.EnabledAt is null) return BadRequest("Auth factor is not enabled."); if (factor.Trustworthy <= 0) return BadRequest("Auth factor is not trustworthy."); if (challenge.StepRemain == 0) return challenge; - if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow)) + var now = SystemClock.Instance.GetCurrentInstant(); + if (challenge.ExpiredAt.HasValue && now > challenge.ExpiredAt.Value) return BadRequest(); + // prevent reusing the same factor in one challenge + if (challenge.BlacklistFactors.Contains(factor.Id)) + return BadRequest("Auth factor already used."); + try { if (await accounts.VerifyFactorCode(factor, request.Password)) @@ -272,37 +280,15 @@ public class AuthController( .FirstOrDefaultAsync(); if (challenge is null) return BadRequest("Authorization code not found or expired."); - if (challenge.StepRemain != 0) - return BadRequest("Challenge not yet completed."); - - var session = await db.AuthSessions - .Where(e => e.Challenge == challenge) - .FirstOrDefaultAsync(); - if (session is not null) - return BadRequest("Session already exists for this challenge."); - - session = new AuthSession + try { - 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 tk = auth.CreateToken(session); - Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions + var tk = await auth.CreateSessionAndIssueToken(challenge); + return Ok(new TokenExchangeResponse { Token = tk }); + } + catch (ArgumentException ex) { - HttpOnly = true, - Secure = true, - SameSite = SameSiteMode.Lax, - Domain = _cookieDomain, - Expires = DateTime.UtcNow.AddDays(30) - }); - - return Ok(new TokenExchangeResponse { Token = tk }); + return BadRequest(ex.Message); + } default: // Since we no longer need the refresh token // This case is blank for now, thinking to mock it if the OIDC standard requires it diff --git a/DysonNetwork.Pass/Auth/AuthService.cs b/DysonNetwork.Pass/Auth/AuthService.cs index fa7c519..6d5209c 100644 --- a/DysonNetwork.Pass/Auth/AuthService.cs +++ b/DysonNetwork.Pass/Auth/AuthService.cs @@ -189,6 +189,53 @@ public class AuthService( return CreateCompactToken(session.Id, rsa); } + /// + /// Create a session for a completed challenge, persist it, issue a token, and set the auth cookie. + /// Keeps behavior identical to previous controller implementation. + /// + /// Completed challenge + /// Signed compact token + /// If challenge not completed or session already exists + public async Task CreateSessionAndIssueToken(AuthChallenge challenge) + { + if (challenge.StepRemain != 0) + throw new ArgumentException("Challenge not yet completed."); + + var hasSession = await db.AuthSessions + .AnyAsync(e => e.ChallengeId == challenge.Id); + if (hasSession) + throw new ArgumentException("Session already exists for this challenge."); + + var now = SystemClock.Instance.GetCurrentInstant(); + var session = new AuthSession + { + LastGrantedAt = now, + // Never expire server-side + ExpiredAt = null, + AccountId = challenge.AccountId, + ChallengeId = challenge.Id + }; + + db.AuthSessions.Add(session); + await db.SaveChangesAsync(); + + var tk = CreateToken(session); + + // Set cookie using HttpContext + var cookieDomain = config["AuthToken:CookieDomain"]!; + HttpContext.Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = SameSiteMode.Lax, + Domain = cookieDomain, + // Effectively never expire client-side (20 years) + Expires = DateTime.UtcNow.AddYears(20) + }); + + return tk; + } + private string CreateCompactToken(Guid sessionId, RSA rsa) { // Create the payload: just the session ID