🐛 Fixes and overhaul the auth experience

This commit is contained in:
LittleSheep 2025-06-07 12:14:42 +08:00
parent 2f051d0615
commit 0e78f7f7d2
4 changed files with 23 additions and 14 deletions

View File

@ -141,6 +141,8 @@ public class AccountAuthFactor : ModelBase
case AccountAuthFactorType.TimedCode: case AccountAuthFactorType.TimedCode:
var otp = new Totp(Base32Encoding.ToBytes(Secret)); var otp = new Totp(Base32Encoding.ToBytes(Secret));
return otp.VerifyTotp(DateTime.UtcNow, password, out _, new VerificationWindow(previous: 5, future: 5)); return otp.VerifyTotp(DateTime.UtcNow, password, out _, new VerificationWindow(previous: 5, future: 5));
case AccountAuthFactorType.EmailCode:
case AccountAuthFactorType.InAppCode:
default: default:
throw new InvalidOperationException("Unsupported verification type, use CheckDeliveredCode instead."); throw new InvalidOperationException("Unsupported verification type, use CheckDeliveredCode instead.");
} }

View File

@ -357,7 +357,7 @@ public class AccountCurrentController(
[HttpPost("factors/{id:guid}/enable")] [HttpPost("factors/{id:guid}/enable")]
[Authorize] [Authorize]
public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string code) public async Task<ActionResult<AccountAuthFactor>> EnableAuthFactor(Guid id, [FromBody] string? code)
{ {
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();

View File

@ -157,13 +157,16 @@ public class AccountService(
return factor; return factor;
} }
public async Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string code) public async Task<AccountAuthFactor> EnableAuthFactor(AccountAuthFactor factor, string? code)
{ {
if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled."); if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled.");
if (!factor.VerifyPassword(code)) if (factor.Type is AccountAuthFactorType.Password or AccountAuthFactorType.TimedCode)
throw new InvalidOperationException( {
"Invalid code, you need to enter the correct code to enable the factor." if (code is null || !factor.VerifyPassword(code))
); throw new InvalidOperationException(
"Invalid code, you need to enter the correct code to enable the factor."
);
}
factor.EnabledAt = SystemClock.Instance.GetCurrentInstant(); factor.EnabledAt = SystemClock.Instance.GetCurrentInstant();
db.Update(factor); db.Update(factor);
@ -186,7 +189,7 @@ public class AccountService(
factor.EnabledAt = null; factor.EnabledAt = null;
db.Update(factor); db.Update(factor);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return factor; return factor;
} }
@ -219,7 +222,7 @@ public class AccountService(
case AccountAuthFactorType.InAppCode: case AccountAuthFactorType.InAppCode:
if (await _GetFactorCode(factor) is not null) if (await _GetFactorCode(factor) is not null)
throw new InvalidOperationException("A factor code has been sent and in active duration."); throw new InvalidOperationException("A factor code has been sent and in active duration.");
await nty.SendNotification( await nty.SendNotification(
account, account,
"auth.verification", "auth.verification",
@ -233,7 +236,7 @@ public class AccountService(
case AccountAuthFactorType.EmailCode: case AccountAuthFactorType.EmailCode:
if (await _GetFactorCode(factor) is not null) if (await _GetFactorCode(factor) is not null)
throw new InvalidOperationException("A factor code has been sent and in active duration."); throw new InvalidOperationException("A factor code has been sent and in active duration.");
ArgumentNullException.ThrowIfNull(hint); ArgumentNullException.ThrowIfNull(hint);
hint = hint.Replace("@", "").Replace(".", "").Replace("+", "").Replace("%", ""); hint = hint.Replace("@", "").Replace(".", "").Replace("+", "").Replace("%", "");
if (string.IsNullOrWhiteSpace(hint)) if (string.IsNullOrWhiteSpace(hint))
@ -263,8 +266,8 @@ public class AccountService(
} }
await email.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>( await email.SendTemplatedEmailAsync<VerificationEmail, VerificationEmailModel>(
account.Nick,
contact.Content, contact.Content,
localizer["EmailVerificationTitle"],
localizer["VerificationEmail"], localizer["VerificationEmail"],
new VerificationEmailModel new VerificationEmailModel
{ {

View File

@ -53,7 +53,7 @@ public class AuthController(
var challenge = new Challenge var challenge = new Challenge
{ {
ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)), ExpiredAt = Instant.FromDateTimeUtc(DateTime.UtcNow.AddHours(1)),
StepTotal = 1, StepTotal = 3,
Platform = request.Platform, Platform = request.Platform,
Audiences = request.Audiences, Audiences = request.Audiences,
Scopes = request.Scopes, Scopes = request.Scopes,
@ -80,10 +80,11 @@ public class AuthController(
var challenge = await db.AuthChallenges var challenge = await db.AuthChallenges
.Include(e => e.Account) .Include(e => e.Account)
.Include(e => e.Account.AuthFactors) .Include(e => e.Account.AuthFactors)
.Where(e => e.Id == id).FirstOrDefaultAsync(); .Where(e => e.Id == id)
.FirstOrDefaultAsync();
return challenge is null return challenge is null
? NotFound("Auth challenge was not found.") ? NotFound("Auth challenge was not found.")
: challenge.Account.AuthFactors.ToList(); : challenge.Account.AuthFactors.Where(e => e.EnabledAt != null).ToList();
} }
[HttpPost("challenge/{id:guid}/factors/{factorId:guid}")] [HttpPost("challenge/{id:guid}/factors/{factorId:guid}")]
@ -131,6 +132,8 @@ public class AuthController(
var factor = await db.AccountAuthFactors.FindAsync(request.FactorId); var factor = await db.AccountAuthFactors.FindAsync(request.FactorId);
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.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)) if (challenge.ExpiredAt.HasValue && challenge.ExpiredAt.Value < Instant.FromDateTimeUtc(DateTime.UtcNow))
@ -140,7 +143,8 @@ public class AuthController(
{ {
if (await accounts.VerifyFactorCode(factor, request.Password)) if (await accounts.VerifyFactorCode(factor, request.Password))
{ {
challenge.StepRemain--; challenge.StepRemain -= factor.Trustworthy;
challenge.StepRemain = Math.Max(0, challenge.StepRemain);
challenge.BlacklistFactors.Add(factor.Id); challenge.BlacklistFactors.Add(factor.Id);
db.Update(challenge); db.Update(challenge);
als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess, als.CreateActionLogFromRequest(ActionLogType.ChallengeSuccess,