♻️ Refactored auth controller
This commit is contained in:
@@ -172,15 +172,23 @@ public class AuthController(
|
|||||||
.FirstOrDefaultAsync(e => e.Id == id);
|
.FirstOrDefaultAsync(e => e.Id == id);
|
||||||
if (challenge is null) return NotFound("Auth challenge was not found.");
|
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 is null) return NotFound("Auth factor was not found.");
|
||||||
if (factor.EnabledAt is null) return BadRequest("Auth factor is not enabled.");
|
if (factor.EnabledAt is null) return BadRequest("Auth factor is not enabled.");
|
||||||
if (factor.Trustworthy <= 0) return BadRequest("Auth factor is not trustworthy.");
|
if (factor.Trustworthy <= 0) return BadRequest("Auth factor is not trustworthy.");
|
||||||
|
|
||||||
if (challenge.StepRemain == 0) return challenge;
|
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();
|
return BadRequest();
|
||||||
|
|
||||||
|
// prevent reusing the same factor in one challenge
|
||||||
|
if (challenge.BlacklistFactors.Contains(factor.Id))
|
||||||
|
return BadRequest("Auth factor already used.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await accounts.VerifyFactorCode(factor, request.Password))
|
if (await accounts.VerifyFactorCode(factor, request.Password))
|
||||||
@@ -272,37 +280,15 @@ public class AuthController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (challenge is null)
|
if (challenge is null)
|
||||||
return BadRequest("Authorization code not found or expired.");
|
return BadRequest("Authorization code not found or expired.");
|
||||||
if (challenge.StepRemain != 0)
|
try
|
||||||
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
|
|
||||||
{
|
{
|
||||||
LastGrantedAt = Instant.FromDateTimeUtc(DateTime.UtcNow),
|
var tk = await auth.CreateSessionAndIssueToken(challenge);
|
||||||
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(30)),
|
return Ok(new TokenExchangeResponse { Token = tk });
|
||||||
Account = challenge.Account,
|
}
|
||||||
Challenge = challenge,
|
catch (ArgumentException ex)
|
||||||
};
|
|
||||||
|
|
||||||
db.AuthSessions.Add(session);
|
|
||||||
await db.SaveChangesAsync();
|
|
||||||
|
|
||||||
var tk = auth.CreateToken(session);
|
|
||||||
Response.Cookies.Append(AuthConstants.CookieTokenName, tk, new CookieOptions
|
|
||||||
{
|
{
|
||||||
HttpOnly = true,
|
return BadRequest(ex.Message);
|
||||||
Secure = true,
|
}
|
||||||
SameSite = SameSiteMode.Lax,
|
|
||||||
Domain = _cookieDomain,
|
|
||||||
Expires = DateTime.UtcNow.AddDays(30)
|
|
||||||
});
|
|
||||||
|
|
||||||
return Ok(new TokenExchangeResponse { Token = tk });
|
|
||||||
default:
|
default:
|
||||||
// Since we no longer need the refresh token
|
// Since we no longer need the refresh token
|
||||||
// This case is blank for now, thinking to mock it if the OIDC standard requires it
|
// This case is blank for now, thinking to mock it if the OIDC standard requires it
|
||||||
|
@@ -189,6 +189,53 @@ public class AuthService(
|
|||||||
return CreateCompactToken(session.Id, rsa);
|
return CreateCompactToken(session.Id, rsa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a session for a completed challenge, persist it, issue a token, and set the auth cookie.
|
||||||
|
/// Keeps behavior identical to previous controller implementation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="challenge">Completed challenge</param>
|
||||||
|
/// <returns>Signed compact token</returns>
|
||||||
|
/// <exception cref="ArgumentException">If challenge not completed or session already exists</exception>
|
||||||
|
public async Task<string> 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)
|
private string CreateCompactToken(Guid sessionId, RSA rsa)
|
||||||
{
|
{
|
||||||
// Create the payload: just the session ID
|
// Create the payload: just the session ID
|
||||||
|
Reference in New Issue
Block a user