diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts')
3 files changed, 77 insertions, 26 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs index ea6bab1..062ed93 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs @@ -53,7 +53,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints [ConfigurationName("login_endpoint")] internal sealed class LoginEndpoint : UnprotectedWebEndpoint { - public const string INVALID_MESSAGE = "Please check your email or password."; + public const string INVALID_MESSAGE = "Please check your email or password. You may get locked out."; public const string LOCKED_ACCOUNT_MESSAGE = "You have been timed out, please try again later"; public const string MFA_ERROR_MESSAGE = "Invalid or expired request."; @@ -159,7 +159,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } //Inc failed login count - user.FailedLoginIncrement(); + user.FailedLoginIncrement(entity.RequestedTimeUtc); webm.Result = INVALID_MESSAGE; Cleanup: @@ -181,8 +181,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { return false; } - //Reset flc for account - user.FailedLoginCount(0); + + //Reset flc for account, either the user will be authorized, or the mfa will be triggered, but the flc should be reset + user.ClearFailedLoginCount(); + try { if (user.Status == UserStatus.Active) @@ -342,7 +344,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { webm.Result = "Please check your code."; //Increment flc and update the user in the store - user.FailedLoginIncrement(); + user.FailedLoginIncrement(entity.RequestedTimeUtc); return; } //Valid, complete @@ -401,7 +403,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (flc.LastModified.Add(FailedCountTimeout) < now) { //clear flc flag - user.FailedLoginCount(0); + user.ClearFailedLoginCount(); return false; } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs index 06ccd60..e7c8a86 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs @@ -120,7 +120,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints JsonWebToken jwt; try { - //We can try to recover the jwt data + //We can try to recover the jwt data, if the data is invalid, jwt = JsonWebToken.Parse(login.LoginJwt); } catch (KeyNotFoundException) @@ -197,7 +197,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(user.PKIVerifyUserJWT(jwt, authInfo.KeyId) == true, INVALID_MESSAGE)) { //increment flc on invalid signature - user.FailedLoginIncrement(); + user.FailedLoginIncrement(entity.RequestedTimeUtc); await user.ReleaseAsync(); entity.CloseResponse(webm); @@ -399,7 +399,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (flc.LastModified.AddSeconds(_config.FailedCountTimeoutSec) < now) { //clear flc flag - user.FailedLoginCount(0); + user.ClearFailedLoginCount(); return false; } @@ -430,7 +430,11 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints RuleFor(l => l.LoginJwt) .NotEmpty() .MinimumLength(50) - .IllegalCharacters(); + //Token should not contain illegal chars, only base64url + '.' + .IllegalCharacters() + //Make sure the jwt contains exacly 2 '.' chracters + .Must(static l => l.Where(static c => c == '.').Count() == 2) + .WithMessage("Your credential is not a valid Json Web Token"); } } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs index 728bc42..eadebcc 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs @@ -153,21 +153,18 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider return false; } - switch (level) + //Reconcile cookies on request + ReconcileCookies(entity); + + return level switch { //Accept the client token or the cookie as any/medium - case AuthorzationCheckLevel.Any: - case AuthorzationCheckLevel.Medium: - return VerifyLoginCookie(entity) || VerifyClientToken(entity); - + AuthorzationCheckLevel.Any or AuthorzationCheckLevel.Medium => VerifyLoginCookie(entity) || VerifyClientToken(entity), //Critical requires that the client cookie is set and the token is set - case AuthorzationCheckLevel.Critical: - return VerifyLoginCookie(entity) && VerifyClientToken(entity); - + AuthorzationCheckLevel.Critical => VerifyLoginCookie(entity) && VerifyClientToken(entity), //Default to false condition - default: - return false; - } + _ => false, + }; } IClientAuthorization IAccountSecurityProvider.ReAuthorizeClient(HttpEntity entity) @@ -366,6 +363,21 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider #endregion #region Cookies + + private void ReconcileCookies(HttpEntity entity) + { + //Only handle cookies if session is loaded and is a web based session + if (!entity.Session.IsSet || entity.Session.SessionType != SessionType.Web) + { + return; + } + + //If the session is new, or not supposed to be logged in, clear the login cookies if they were set + if (entity.Session.IsNew || string.IsNullOrEmpty(entity.Session.LoginHash) || string.IsNullOrEmpty(entity.Session.Token)) + { + ExpireCookies(entity); + } + } private bool VerifyLoginCookie(HttpEntity entity) { @@ -389,11 +401,11 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider //Alloc buffer for decoding the base64 signatures - using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(2 * entity.Session.LoginHash.Length, true); + using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(2 * _config.LoginCookieSize, true); //Slice up buffers Span<byte> cookieBuffer = buffer.Span[.._config.LoginCookieSize]; - Span<byte> sessionBuffer = buffer.Span.Slice(_config.LoginCookieSize, _config.LoginCookieSize); + Span<byte> sessionBuffer = buffer.AsSpan(_config.LoginCookieSize, _config.LoginCookieSize); //Convert cookie and session hash value if (Convert.TryFromBase64Chars(cookie, cookieBuffer, out int cookieBytesWriten) @@ -405,6 +417,8 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider return true; } } + //Clear login cookie if failed + ExpireCookies(entity); return false; } @@ -413,17 +427,48 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider //Expire login cookie if set if (entity.Server.RequestCookies.ContainsKey(_config.LoginCookieName)) { - entity.Server.ExpireCookie(_config.LoginCookieName, sameSite: CookieSameSite.SameSite); + HttpCookie pkCookie = new(_config.LoginCookieName, string.Empty) + { + Domain = _config.CookieDomain, + Path = _config.CookiePath, + ValidFor = TimeSpan.Zero, + SameSite = CookieSameSite.SameSite, + HttpOnly = true, + Secure = true + }; + + entity.Server.SetCookie(in pkCookie); } //Expire the LI cookie if set if (entity.Server.RequestCookies.ContainsKey(_config.ClientStatusCookieName)) { - entity.Server.ExpireCookie(_config.ClientStatusCookieName, sameSite: CookieSameSite.SameSite); + HttpCookie pkCookie = new(_config.ClientStatusCookieName, string.Empty) + { + Domain = _config.CookieDomain, + Path = _config.CookiePath, + ValidFor = TimeSpan.Zero, + SameSite = CookieSameSite.SameSite, + HttpOnly = true, + Secure = true + }; + + entity.Server.SetCookie(in pkCookie); } //Expire pupkey cookie if (entity.Server.RequestCookies.ContainsKey(_config.PubKeyCookieName)) { - entity.Server.ExpireCookie(_config.PubKeyCookieName, sameSite: CookieSameSite.SameSite); + //Init exipiration cookie + HttpCookie pkCookie = new(_config.PubKeyCookieName, string.Empty) + { + Domain = _config.CookieDomain, + Path = _config.CookiePath, + ValidFor = TimeSpan.Zero, + SameSite = CookieSameSite.SameSite, + HttpOnly = true, + Secure = true + }; + + entity.Server.SetCookie(in pkCookie); } } |