diff --git a/server/Controllers/AuthController.cs b/server/Controllers/AuthController.cs index 58b1de2..bb553e9 100644 --- a/server/Controllers/AuthController.cs +++ b/server/Controllers/AuthController.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Asp.Versioning; @@ -35,6 +36,10 @@ namespace Sockeye.Api.Controllers private readonly ApiServerState serverState; private const int JWT_LIFETIME_DAYS = 5; + //case 4710: wrong 2fa codes allowed per password entry, counted per user (in memory, a restart only resets the count) + private const int MAX_TFA_FAILURES = 5; + private static readonly ConcurrentDictionary TfaFailures = new ConcurrentDictionary(); + /// /// ctor /// @@ -100,6 +105,7 @@ namespace Sockeye.Api.Controllers //Generate a temporary token to identify and verify this is the same user u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", ""); await ct.SaveChangesAsync(); + TfaFailures.TryRemove(u.Id, out _); var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id); List TranslationKeysToFetch = new List { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel", "AuthPinInvalid" }; @@ -164,7 +170,14 @@ namespace Sockeye.Api.Controllers return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } - //Match to temp token that would have been set by initial credentialed login for 2fa User + //case 4710: the temp token is the only proof the password step was passed, so it must be present + if (string.IsNullOrWhiteSpace(pin.TempToken)) + { + await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY); + return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); + } + + //Match to temp token that would have been set by initial credentialed login for 2fa User var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.AllowLogin==true && z.TwoFactorEnabled == true).FirstOrDefaultAsync(); @@ -181,14 +194,22 @@ namespace Sockeye.Api.Controllers var tfa = new TwoFactorAuth("Sockeye"); if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim())) { + //case 4710: after too many wrong codes the password must be entered again + if (TfaFailures.AddOrUpdate(user.Id, 1, (k, n) => n + 1) >= MAX_TFA_FAILURES) + { + TfaFailures.TryRemove(user.Id, out _); + user.TempToken = null; + await ct.SaveChangesAsync(); + } await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY); return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED)); } //User is valid and authenticated - //clear temp token - user.TempToken = string.Empty; + //clear temp token (case 4710: null, not empty, so no value is left to match) + user.TempToken = null; await ct.SaveChangesAsync(); + TfaFailures.TryRemove(user.Id, out _); return await ReturnUserCredsOnSuccessfulAuthentication(user); }