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