From 5ddef0fcb742e77b99a0e17015d2eea0a1d4131a Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 9 Mar 2023 01:48:28 -0500 Subject: Omega cache, session, and account provider complete overhaul --- .../src/Accounts/AccountUtils.cs | 807 ++++++--------------- .../src/Accounts/AuthorzationCheckLevel.cs | 56 ++ .../src/Accounts/ClientSecurityToken.cs | 43 ++ .../src/Accounts/IAccountSecurityProvider.cs | 90 +++ .../src/Accounts/IClientAuthorization.cs | 45 ++ .../src/Accounts/IClientSecInfo.cs | 46 ++ .../src/Accounts/IPasswordHashingProvider.cs | 80 ++ .../src/Accounts/LoginMessage.cs | 12 +- .../src/Accounts/PasswordChallengeResult.cs | 38 + .../src/Accounts/PasswordHashing.cs | 157 ++-- .../src/Endpoints/ProtectedWebEndpoint.cs | 14 +- lib/Plugins.Essentials/src/EventProcessor.cs | 35 +- .../src/Extensions/EssentialHttpEventExtensions.cs | 12 +- .../src/Extensions/HttpCookie.cs | 26 +- lib/Plugins.Essentials/src/HttpEntity.cs | 4 +- lib/Plugins.Essentials/src/IEpProcessingOptions.cs | 8 +- lib/Plugins.Essentials/src/IWebProcessorInfo.cs | 82 +++ .../src/Oauth/OauthHttpExtensions.cs | 137 +--- lib/Plugins.Essentials/src/Sessions/SessionBase.cs | 20 +- lib/Plugins.Essentials/src/Sessions/SessionInfo.cs | 165 +++-- 20 files changed, 1032 insertions(+), 845 deletions(-) create mode 100644 lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs create mode 100644 lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs create mode 100644 lib/Plugins.Essentials/src/IWebProcessorInfo.cs (limited to 'lib/Plugins.Essentials/src') diff --git a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs index 610d646..75c3388 100644 --- a/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs +++ b/lib/Plugins.Essentials/src/Accounts/AccountUtils.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials -* File: AccountManager.cs +* File: AccountUtil.cs * -* AccountManager.cs is part of VNLib.Plugins.Essentials which is part of the larger +* AccountUtil.cs is part of VNLib.Plugins.Essentials which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify @@ -23,21 +23,19 @@ */ using System; -using System.IO; -using System.Text; +using System.Buffers; +using System.Threading; using System.Threading.Tasks; using System.Security.Cryptography; using System.Text.RegularExpressions; using System.Runtime.CompilerServices; using VNLib.Hashing; -using VNLib.Net.Http; using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Users; using VNLib.Plugins.Essentials.Sessions; -using VNLib.Plugins.Essentials.Extensions; #nullable enable @@ -51,60 +49,24 @@ namespace VNLib.Plugins.Essentials.Accounts /// public static partial class AccountUtil { - public const int MAX_EMAIL_CHARS = 50; - public const int ID_FIELD_CHARS = 65; - public const int STREET_ADDR_CHARS = 150; - public const int MAX_LOGIN_COUNT = 10; - public const int MAX_FAILED_RESET_ATTEMPS = 5; - /// - /// The maximum time in seconds for a login message to be considered valid - /// - public const double MAX_TIME_DIFF_SECS = 10.00; /// /// The size in bytes of the random passwords generated when invoking the /// public const int RANDOM_PASS_SIZE = 128; - /// - /// The name of the header that will identify a client's identiy - /// - public const string LOGIN_TOKEN_HEADER = "X-Web-Token"; + /// /// The origin string of a local user account. This value will be set if an /// account is created through the VNLib.Plugins.Essentials.Accounts library /// public const string LOCAL_ACCOUNT_ORIGIN = "local"; - /// - /// The size (in bytes) of the challenge secret - /// - public const int CHALLENGE_SIZE = 64; - /// - /// The size (in bytes) of the sesssion long user-password challenge - /// - public const int SESSION_CHALLENGE_SIZE = 128; - - //The buffer size to use when decoding the base64 public key from the user - private const int PUBLIC_KEY_BUFFER_SIZE = 1024; - /// - /// The name of the login cookie set when a user logs in - /// - public const string LOGIN_COOKIE_NAME = "VNLogin"; - /// - /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in) - /// - public const string LOGIN_COOKIE_IDENTIFIER = "li"; - - private const int LOGIN_COOKIE_SIZE = 64; - + + //Session entry keys private const string BROWSER_ID_ENTRY = "acnt.bid"; - private const string CLIENT_PUB_KEY_ENTRY = "acnt.pbk"; - private const string CHALLENGE_HMAC_ENTRY = "acnt.cdig"; private const string FAILED_LOGIN_ENTRY = "acnt.flc"; private const string LOCAL_ACCOUNT_ENTRY = "acnt.ila"; private const string ACC_ORIGIN_ENTRY = "__.org"; - private const string TOKEN_UPDATE_TIME_ENTRY = "acnt.tut"; - //private const string CHALLENGE_HASH_ENTRY = "acnt.chl"; //Privlage masks public const ulong READ_MSK = 0x0000000000000001L; @@ -122,19 +84,6 @@ namespace VNLib.Plugins.Essentials.Accounts public const ulong MINIMUM_LEVEL = 0x0000000100000001L; - //Timeouts - public static readonly TimeSpan LoginCookieLifespan = TimeSpan.FromHours(1); - public static readonly TimeSpan RegenIdPeriod = TimeSpan.FromMinutes(25); - - /// - /// The client data encryption padding. - /// - public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256; - - /// - /// The size (in bytes) of the web-token hash size - /// - private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8); /// /// Speical character regual expresion for basic checks @@ -154,7 +103,7 @@ namespace VNLib.Plugins.Essentials.Accounts /// /// /// - public static async Task SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) + public static async Task SetRandomPasswordAsync(this IPasswordHashingProvider passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE) { _ = manager ?? throw new ArgumentNullException(nameof(manager)); _ = user ?? throw new ArgumentNullException(nameof(user)); @@ -208,512 +157,292 @@ namespace VNLib.Plugins.Essentials.Accounts [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetRandomUserId() => RandomHash.GetRandomHash(HashAlg.SHA1, 64, HashEncodingMode.Hexadecimal); - #endregion - - #region Client Auth Extensions - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables + /// Generates a cryptographically secure random password, then hashes it + /// and returns the hash of the new password /// - /// The connection and session to log-in - /// The message of the client to set the log-in status of - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user) + /// + /// The size (in bytes) of the new random password + /// A that contains the new password hash + public static PrivateString GetRandomPassword(this IPasswordHashingProvider hashing, int size = RANDOM_PASS_SIZE) { - return GenerateAuthorization(ev, loginMessage.ClientPublicKey, loginMessage.ClientID, user); - } + //Get random bytes + byte[] randBuffer = ArrayPool.Shared.Rent(size); + try + { + Span span = randBuffer.AsSpan(0, size); - /// - /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables - /// - /// The connection and session to log-in - /// The clients base64 public key - /// The browser/client id - /// The user to log-in - /// The encrypted base64 token secret data to send to the client - /// - /// - /// - public static string GenerateAuthorization(this HttpEntity ev, string base64PubKey, string clientId, IUser user) - { - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) + //Generate random password + RandomHash.GetRandomBytes(span); + + //hash the password + return hashing.Hash(span); + } + finally { - throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); + //Zero the block and return to pool + MemoryUtil.InitializeBlock(randBuffer.AsSpan()); + ArrayPool.Shared.Return(randBuffer); } - //Update session-id for "upgrade" - ev.Session.RegenID(); - //derrive token from login data - TryGenerateToken(base64PubKey, out string base64ServerToken, out string base64ClientData); - //Clear flags - user.FailedLoginCount(0); - //Get the "local" account flag from the user object - bool localAccount = user.IsLocalAccount(); - //Set login cookie and session login hash - ev.SetLogin(localAccount); - //Store variables - ev.Session.UserID = user.UserID; - ev.Session.Privilages = user.Privilages; - //Store browserid/client id if specified - SetBrowserID(in ev.Session, clientId); - //Store the clients public key - SetBrowserPubKey(in ev.Session, base64PubKey); - //Set local account flag - ev.Session.HasLocalAccount(localAccount); - //Store the base64 server key to compute the hmac later - ev.Session.Token = base64ServerToken; - //Update the last token upgrade time - ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); - //Return the client encrypted data - return base64ClientData; } - /* - * Notes for RSA client token generator code below - * - * To log-in a client with the following API the calling code - * must have already determined that the client should be - * logged in (verified passwords or auth tokens). - * - * The client will send a LoginMessage object that will - * contain the following Information. - * - * - The clients RSA public key in base64 subject-key info format - * - The client browser's id hex string - * - The clients local-time - * - * The TryGenerateToken method, will generate a random-byte token, - * encrypt it using the clients RSA public key, return the encrypted - * token data to the client, and only the client will be able to - * decrypt the token data. - * - * The token data is also hashed with SHA-256 (for future use) and - * stored in the client's session store. The client must decrypt - * the token data, hash it, and return it as a header for verification. - * - * Ideally the client should sign the data and send the signature or - * hash back, but it wont prevent MITM, and for now I think it just - * adds extra overhead for every connection during the HttpEvent.TokenMatches() - * check extension method - */ - - private ref struct TokenGenBuffers + /// + /// Asynchronously verifies the desired user's password. If the user is not found or the password is not found + /// returns false. Returns true if the user exist's has a valid password hash and matches the supplied password value. + /// + /// + /// The id of the user to check the password against + /// The raw password of the user to compare hashes against + /// The password hashing tools + /// A token to cancel the operation + /// A task that completes with the value of the password hashing match. + /// + public static async Task VerifyPasswordAsync(this IUserManager manager, string userId, PrivateString rawPassword, IPasswordHashingProvider hashing, CancellationToken cancellation) { - public readonly Span Buffer { private get; init; } - public readonly Span SignatureBuffer => Buffer[..64]; - - - - public int ClientPbkWritten; - public readonly Span ClientPublicKeyBuffer => Buffer.Slice(64, 1024); - public readonly ReadOnlySpan ClientPbkOutput => ClientPublicKeyBuffer[..ClientPbkWritten]; + _ = userId ?? throw new ArgumentNullException(nameof(userId)); + _ = rawPassword ?? throw new ArgumentNullException(nameof(rawPassword)); + _ = hashing ?? throw new ArgumentNullException(nameof(hashing)); + //Get the user, may be null if the user does not exist + using IUser? user = await manager.GetUserAndPassFromIDAsync(userId, cancellation); - - public int ClientEncBytesWritten; - public readonly Span ClientEncOutputBuffer => Buffer[(64 + 1024)..]; - public readonly ReadOnlySpan EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten]; + return user != null && hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan()); } - + /// - /// Computes a random buffer, encrypts it with the client's public key, - /// computes the digest of that key and returns the base64 encoded strings - /// of those components + /// Verifies the user's raw password against the hashed password using the specified + /// instance /// - /// The user's public key credential - /// The base64 encoded digest of the secret that was encrypted - /// The client's user-agent header value - /// A string representing a unique signed token for a given login context - /// - /// - private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData) + /// + /// + /// The provider instance + /// True if the password + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool VerifyPassword(this IUser user, PrivateString rawPassword, IPasswordHashingProvider hashing) { - //Temporary work buffer - using IMemoryHandle buffer = MemoryUtil.SafeAlloc(4096, true); - /* - * Create a new token buffer for bin buffers. - * This buffer struct is used to break up - * a single block of memory into individual - * non-overlapping (important!) buffer windows - * for named purposes - */ - TokenGenBuffers tokenBuf = new() - { - Buffer = buffer.Span - }; - //Recover the clients public key from its base64 encoding - if (!Convert.TryFromBase64String(base64clientPublicKey, tokenBuf.ClientPublicKeyBuffer, out tokenBuf.ClientPbkWritten)) - { - throw new InternalBufferOverflowException("Failed to recover the clients RSA public key"); - } - /* - * Fill signature buffer with random data - * this signature will be stored and used to verify - * signed client messages. It will also be encryped - * using the clients RSA keys - */ - RandomHash.GetRandomBytes(tokenBuf.SignatureBuffer); - /* - * Setup a new RSA Crypto provider that is initialized with the clients - * supplied public key. RSA will be used to encrypt the server secret - * that only the client will be able to decrypt for the current connection - */ - using RSA rsa = RSA.Create(); - //Setup rsa from the users public key - rsa.ImportSubjectPublicKeyInfo(tokenBuf.ClientPbkOutput, out _); - //try to encypte output data - if (!rsa.TryEncrypt(tokenBuf.SignatureBuffer, tokenBuf.ClientEncOutputBuffer, RSAEncryptionPadding.OaepSHA256, out tokenBuf.ClientEncBytesWritten)) - { - throw new InternalBufferOverflowException("Failed to encrypt the server secret"); - } - //Compute the digest of the raw server key - base64Digest = ManagedHash.ComputeBase64Hash(tokenBuf.SignatureBuffer, HashAlg.SHA384); - /* - * The client will send a hash of the decrypted key and will be used - * as a comparison to the hash string above ^ - */ - base64ClientData = Convert.ToBase64String(tokenBuf.EncryptedOutput, Base64FormattingOptions.None); + return user.PassHash != null && hashing.Verify(user.PassHash, rawPassword); } /// - /// Determines if the client sent a token header, and it maches against the current session + /// Verifies a password against its previously encoded hash. /// - /// true if the client set the token header, the session is loaded, and the token matches the session, false otherwise - public static bool TokenMatches(this HttpEntity ev) + /// + /// Previously hashed password + /// Raw password to compare against + /// True if bytes derrived from password match the hash, false otherwise + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Verify(this IPasswordHashingProvider provider, PrivateString passHash, PrivateString password) { - //Get the token from the client header, the client should always sent this - string? clientDigest = ev.Server.Headers[LOGIN_TOKEN_HEADER]; - //Make sure a session is loaded - if (!ev.Session.IsSet || ev.Session.IsNew || string.IsNullOrWhiteSpace(clientDigest)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * The buffer just needs to be large enoguh for the size of the hashes - * that are stored in base64 format. - * - * The values in the buffers will be the raw hash of the client's key - * and the stored key sent during initial authorziation. If the hashes - * are equal it should mean that the client must have the private - * key that generated the public key that was sent - */ - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(TokenHashSize * 2, true); - //Slice up buffers - Span headerBuffer = buffer.Span[..TokenHashSize]; - Span sessionBuffer = buffer.Span[TokenHashSize..]; - //Convert the header token and the session token - if (Convert.TryFromBase64String(clientDigest, headerBuffer, out int headerTokenLen) - && Convert.TryFromBase64String(ev.Session.Token, sessionBuffer, out int sessionTokenLen)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - if(CryptographicOperations.FixedTimeEquals(headerBuffer[..headerTokenLen], sessionBuffer[..sessionTokenLen])) - { - return true; - } - } - - /* - * If the token does not match, or cannot be found, check if the client - * has login cookies set, if not remove them. - * - * This does not affect the session, but allows for a web client to update - * its login state if its no-longer logged in - */ - - //Expire login cookie if set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) - { - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite); - } - //Expire the LI cookie if set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite); - } - - return false; + //Casting PrivateStrings to spans will reference the base string directly + return provider.Verify((ReadOnlySpan)passHash, (ReadOnlySpan)password); } /// - /// Regenerates the user's login token with the public key stored - /// during initial logon + /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. /// - /// The base64 of the newly encrypted secret - public static string? RegenerateClientToken(this HttpEntity ev) + /// + /// Password to be hashed + /// + /// A of the hashed and encoded password + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PrivateString Hash(this IPasswordHashingProvider provider, PrivateString password) { - if(!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return null; - } - //Get the client's stored public key - string clientPublicKey = ev.Session.GetBrowserPubKey(); - //Make sure its set - if (string.IsNullOrWhiteSpace(clientPublicKey)) - { - return null; - } - //Generate a new token using the stored public key - TryGenerateToken(clientPublicKey, out string base64Digest, out string base64ClientData); - //store the token to the user's session - ev.Session.Token = base64Digest; - //Update the last token upgrade time - ev.Session.LastTokenUpgrade(ev.RequestedTimeUtc); - //return the clients encrypted secret - return base64ClientData; + return provider.Hash((ReadOnlySpan)password); } - /// - /// Tries to encrypt the specified data using the stored public key and store the encrypted data into - /// the output buffer. - /// - /// - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// stored. - /// - /// - public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan data, in Span outputBuffer) + #endregion + + + + #region Client Auth Extensions + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IAccountSecurityProvider GetSecProviderOrThrow(this HttpEntity entity) { - if (!session.IsSet) - { - return false; - } - //try to get the public key from the client - string base64PubKey = session.GetBrowserPubKey(); - return TryEncryptClientData(base64PubKey, data, in outputBuffer); + return entity.RequestedRoot.AccountSecurity + ?? throw new NotSupportedException("The processor this connection originated from does not have an account security provider loaded"); } + /// - /// Tries to encrypt the specified data using the specified public key + /// Determines if the current client has the authroziation level to access a given resource /// - /// A base64 encoded public key used to encrypt client data - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// - /// - public static ERRNO TryEncryptClientData(ReadOnlySpan base64PubKey, ReadOnlySpan data, in Span outputBuffer) + /// + /// The authoziation level + /// True if the connection has the desired authorization status + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsClientAuthorized(this HttpEntity entity, AuthorzationCheckLevel mode = AuthorzationCheckLevel.Critical) { - if (base64PubKey.IsEmpty) - { - return false; - } - //Alloc a buffer for decoding the public key - using UnsafeMemoryHandle pubKeyBuffer = MemoryUtil.UnsafeAlloc(PUBLIC_KEY_BUFFER_SIZE, true); - //Decode the public key - ERRNO pbkBytesWritten = VnEncoding.TryFromBase64Chars(base64PubKey, pubKeyBuffer); - //Try to encrypt the data - return pbkBytesWritten ? TryEncryptClientData(pubKeyBuffer.Span[..(int)pbkBytesWritten], data, in outputBuffer) : false; + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + return prov.IsClientAuthorized(entity, mode); } + /// - /// Tries to encrypt the specified data using the specified public key + /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables /// - /// The raw SKI public key - /// Data to encrypt - /// The buffer to store encrypted data in - /// - /// The number of encrypted bytes written to the output buffer, - /// or false (0) if the operation failed, or if no credential is - /// specified. - /// + /// The connection and session to log-in + /// The clients login security information + /// The user to log-in + /// The encrypted base64 token secret data to send to the client + /// + /// /// - public static ERRNO TryEncryptClientData(ReadOnlySpan rawPubKey, ReadOnlySpan data, in Span outputBuffer) + /// + public static IClientAuthorization GenerateAuthorization(this HttpEntity entity, IClientSecInfo secInfo, IUser user) { - if (rawPubKey.IsEmpty) + _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo)); + + if (!entity.Session.IsSet || entity.Session.SessionType != SessionType.Web) { - return false; + throw new InvalidOperationException("The session is not set or the session is not a web-based session type"); } - //Setup new empty rsa - using RSA rsa = RSA.Create(); - //Import the public key - rsa.ImportSubjectPublicKeyInfo(rawPubKey, out _); - //Encrypt data with OaepSha256 as configured in the browser - return rsa.TryEncrypt(data, outputBuffer, ClientEncryptonPadding, out int bytesWritten) ? bytesWritten : false; - } - /// - /// Stores the clients public key specified during login - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey; + IAccountSecurityProvider provider = entity.GetSecProviderOrThrow(); - /// - /// Gets the clients stored public key that was specified during login - /// - /// The base64 encoded public key string specified at login - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string GetBrowserPubKey(this in SessionInfo session) => session[CLIENT_PUB_KEY_ENTRY]; + //Regen the session id + entity.Session.RegenID(); + + //Authorize client + IClientAuthorization auth = provider.AuthorizeClient(entity, secInfo, user); + + //Clear flags + user.FailedLoginCount(0); + + //Store variables + entity.Session.UserID = user.UserID; + entity.Session.Privilages = user.Privilages; + + //Store client id for later use + entity.Session[BROWSER_ID_ENTRY] = secInfo.ClientId; + + //Get the "local" account flag from the user object + bool localAccount = user.IsLocalAccount(); + + //Set local account flag + entity.Session.HasLocalAccount(localAccount); + + //Return the client encrypted data + return auth; + } /// - /// Stores the login key as a cookie in the current session as long as the session exists - /// / - /// The event to log-in - /// Does the session belong to a local user account + /// Generates a client authorization from the supplied security info + /// using the default and + /// stored the required variables in the + /// response + /// + /// + /// The client's used to authorize the client + /// The user requesting the authenticated use + /// The response to store variables in + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetLogin(this HttpEntity ev, bool? localAccount = null) + public static void GenerateAuthorization(this HttpEntity entity, IClientSecInfo secInfo, IUser user, WebMessage response) { - //Make sure the session is loaded - if (!ev.Session.IsSet) - { - return; - } - string loginString = RandomHash.GetRandomBase64(LOGIN_COOKIE_SIZE); - //Set login cookie and session login hash - ev.Server.SetCookie(LOGIN_COOKIE_NAME, loginString, "", "/", LoginCookieLifespan, CookieSameSite.SameSite, true, true); - ev.Session.LoginHash = loginString; - //If not set get from session storage - localAccount ??= ev.Session.HasLocalAccount(); - //Set the client identifier cookie to a value indicating a local account - ev.Server.SetCookie(LOGIN_COOKIE_IDENTIFIER, localAccount.Value ? "1" : "2", "", "/", LoginCookieLifespan, CookieSameSite.SameSite, false, true); + //Authorize the client + IClientAuthorization auth = GenerateAuthorization(entity, secInfo, user); + + //Set client token + response.Token = auth.SecurityToken.ClientToken; } /// - /// Invalidates the login status of the current connection and session (if session is loaded) + /// Regenerates the client authorization if the client has a currently valid authorization /// + /// + /// The new for the regenerated credentials + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void InvalidateLogin(this HttpEntity ev) + public static IClientAuthorization ReAuthorizeClient(this HttpEntity entity) { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - if (ev.Session.IsSet) - { - //Invalidate the session - ev.Session.Invalidate(); - } + //Get default provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + //Re-authorize the client + return prov.ReAuthorizeClient(entity); } /// - /// Determines if the current session login cookie matches the value stored in the current session (if the session is loaded) + /// Regenerates the client authorization if the client has a currently valid authorization /// - /// True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise - public static bool LoginCookieMatches(this HttpEntity ev) - { - //Sessions must be loaded - if (!ev.Session.IsSet) - { - return false; - } - //Try to get the login string from the request cookies - if (!ev.Server.RequestCookies.TryGetNonEmptyValue(LOGIN_COOKIE_NAME, out string? liCookie)) - { - return false; - } - /* - * Alloc buffer to do conversion and zero initial contents incase the - * payload size has been changed. - * - * Since the cookie size and the local copy should be the same size - * and equal to the LOGIN_COOKIE_SIZE constant, the buffer size should - * be 2 * LOGIN_COOKIE_SIZE, and it can be split in half and shared - * for both conversions - */ - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(2 * LOGIN_COOKIE_SIZE, true); - //Slice up buffers - Span cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE]; - Span sessionBuffer = buffer.Span.Slice(LOGIN_COOKIE_SIZE, LOGIN_COOKIE_SIZE); - //Convert cookie and session hash value - if (Convert.TryFromBase64String(liCookie, cookieBuffer, out _) - && Convert.TryFromBase64String(ev.Session.LoginHash, sessionBuffer, out _)) - { - //Do a fixed time equal (probably overkill, but should not matter too much) - if(CryptographicOperations.FixedTimeEquals(cookieBuffer, sessionBuffer)) - { - //If the user is "logged in" and the request is using the POST method, then we can update the cookie - if(ev.Server.Method == HttpMethod.POST && ev.Session.Created.Add(RegenIdPeriod) < ev.RequestedTimeUtc) - { - //Regen login token - ev.SetLogin(); - ev.Session.RegenID(); - } - - return true; - } - } - return false; + /// + /// The response message to return to the client + /// + /// The new for the regenerated credentials + public static void ReAuthorizeClient(this HttpEntity entity, WebMessage response) + { + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + //Re-authorize the client + IClientAuthorization auth = prov.ReAuthorizeClient(entity); + + //Store the client token in response message + response.Token = auth.SecurityToken.ClientToken; + + //Regen session id also + entity.Session.RegenID(); } - + /// - /// Determines if the client's login cookies need to be updated - /// to reflect its state with the current session's state - /// for the client + /// Attempts to encrypt the supplied data with session stored client information. The user must + /// be authorized /// - /// - public static void ReconcileCookies(this HttpEntity ev) + /// + /// The data to encrypt for the current client + /// The buffer to write encypted data to + /// + /// The number of bytes encrypted and written to the output buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ERRNO TryEncryptClientData(this HttpEntity entity, ReadOnlySpan data, Span output) { - //Only handle cookies if session is loaded and is a web based session - if (!ev.Session.IsSet || ev.Session.SessionType != SessionType.Web) - { - return; - } - if (ev.Session.IsNew) - { - //If either login cookies are set on a new session, clear them - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME) || ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite:CookieSameSite.SameSite, secure:true); - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } - } - //If the session is not supposed to be logged in, clear the login cookies if they were set - else if (string.IsNullOrEmpty(ev.Session.LoginHash)) + //Confirm session is loaded + if(!entity.Session.IsSet || entity.Session.IsNew) { - //If one of either cookie is not set - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_NAME)) - { - //Expire the login cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_NAME, sameSite: CookieSameSite.SameSite, secure: true); - } - if (ev.Server.RequestCookies.ContainsKey(LOGIN_COOKIE_IDENTIFIER)) - { - //Expire the identifier cookie - ev.Server.ExpireCookie(LOGIN_COOKIE_IDENTIFIER, sameSite: CookieSameSite.SameSite, secure: true); - } + return false; } + + //Use the default sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + return prov.TryEncryptClientData(entity, data, output); } /// - /// Gets the last time the session token was set + /// Attempts to encrypt the supplied data with session stored client information. The user must + /// be authorized /// - /// - /// The last time the token was updated/generated, or if not set - public static DateTimeOffset LastTokenUpgrade(this in SessionInfo session) + /// + /// Used for unauthorized connections to encrypt client data based on client security info + /// The data to encrypt for the current client + /// The buffer to write encypted data to + /// The number of bytes encrypted and written to the output buffer + /// + /// + public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan data, Span output) { - //Get the serialized time value - string timeString = session[TOKEN_UPDATE_TIME_ENTRY]; - return long.TryParse(timeString, out long time) ? DateTimeOffset.FromUnixTimeSeconds(time) : DateTimeOffset.MinValue; - } + _ = secInfo ?? throw new ArgumentNullException(nameof(secInfo)); - /// - /// Updates the last time the session token was set - /// - /// - /// The UTC time the last token was set - private static void LastTokenUpgrade(this in SessionInfo session, DateTimeOffset updated) - => session[TOKEN_UPDATE_TIME_ENTRY] = updated.ToUnixTimeSeconds().ToString(); + //Use the default sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + return prov.TryEncryptClientData(secInfo, data, output); + } /// - /// Stores the browser's id during a login process + /// Invalidates the login status of the current connection and session (if session is loaded) /// - /// - /// Browser id value to store + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBrowserID(in SessionInfo session, string browserId) => session[BROWSER_ID_ENTRY] = browserId; + public static void InvalidateLogin(this HttpEntity entity) + { + //Invalidate against the sec provider + IAccountSecurityProvider prov = entity.GetSecProviderOrThrow(); + + prov.InvalidateLogin(entity); + + //Invalidate the session also + entity.Session.Invalidate(); + } /// /// Gets the current browser's id if it was specified during login process @@ -728,7 +457,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// /// True for a local account, false otherwise [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; + public static void HasLocalAccount(this in SessionInfo session, bool value) => session[LOCAL_ACCOUNT_ENTRY] = value ? "1" : null; + /// /// Gets a value indicating if the session belongs to a local user account /// @@ -739,91 +469,6 @@ namespace VNLib.Plugins.Essentials.Accounts #endregion - #region Client Challenge - - /* - * Generates a secret that is used to compute the unique hmac digest of the - * current user's password. The digest is stored in the current session - * and used to compare future requests that require password re-authentication. - * The client will compute the digest of the user's password and send the digest - * instead of the user's password - */ - - /// - /// Generates a new password challenge for the current session and specified password - /// - /// - /// The user's password to compute the hash of - /// The raw derrivation key to send to the client - public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password) - { - ReadOnlySpan rawPass = password; - //Calculate the password buffer size required - int passByteCount = Encoding.UTF8.GetByteCount(rawPass); - //Allocate the buffer - using UnsafeMemoryHandle bufferHandle = MemoryUtil.UnsafeAlloc(passByteCount + 64, true); - //Slice buffers - Span utf8PassBytes = bufferHandle.Span[..passByteCount]; - Span hashBuffer = bufferHandle.Span[passByteCount..]; - //Encode the password into the buffer - _ = Encoding.UTF8.GetBytes(rawPass, utf8PassBytes); - try - { - //Get random secret buffer - byte[] secretKey = RandomHash.GetRandomBytes(SESSION_CHALLENGE_SIZE); - //Compute the digest - int count = HMACSHA512.HashData(secretKey, utf8PassBytes, hashBuffer); - //Store the user's password digest - session[CHALLENGE_HMAC_ENTRY] = VnEncoding.ToBase32String(hashBuffer[..count], false); - return secretKey; - } - finally - { - //Wipe buffer - RandomHash.GetRandomBytes(utf8PassBytes); - } - } - /// - /// Verifies the stored unique digest of the user's password against - /// the client derrived password - /// - /// - /// The base64 client derrived digest of the user's password to verify - /// True if formatting was correct and the derrived passwords match, false otherwise - /// - public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan base64PasswordDigest) - { - string base32Digest = session[CHALLENGE_HMAC_ENTRY]; - if (string.IsNullOrWhiteSpace(base32Digest)) - { - return false; - } - int bufSize = base32Digest.Length + base64PasswordDigest.Length; - //Alloc buffer - using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAlloc(bufSize); - //Split buffers - Span localBuf = buffer.Span[..base32Digest.Length]; - Span passBuf = buffer.Span[base32Digest.Length..]; - //Recover the stored base32 digest - ERRNO count = VnEncoding.TryFromBase32Chars(base32Digest, localBuf); - if (!count) - { - return false; - } - //Recover base64 bytes - if(!Convert.TryFromBase64Chars(base64PasswordDigest, passBuf, out int passBytesWritten)) - { - return false; - } - //Trim buffers - localBuf = localBuf[..(int)count]; - passBuf = passBuf[..passBytesWritten]; - //Compare and return - return CryptographicOperations.FixedTimeEquals(passBuf, localBuf); - } - - #endregion - #region Privilage Extensions /// /// Compares the users privilage level against the specified level diff --git a/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs b/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs new file mode 100644 index 0000000..aa09bf4 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs @@ -0,0 +1,56 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: AuthorzationCheckLevel.cs +* +* AuthorzationCheckLevel.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Specifies how critical the security check is for a user to access + /// a given resource + /// + public enum AuthorzationCheckLevel + { + /// + /// No authorization check is required. + /// + None, + /// + /// Is there any information that the client may have authorization. NOTE: Not a security check! + /// + Any, + /// + /// The authorization check is not considered criticial, just a basic confirmation + /// that the user should be logged it, but does not need to access secure + /// resources. + /// + Medium, + /// + /// The a full authorization check is required as the user may access + /// secure resouces. + /// + Critical + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs b/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs new file mode 100644 index 0000000..0d4aa58 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs @@ -0,0 +1,43 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: ClientSecurityToken.cs +* +* ClientSecurityToken.cs is part of VNLib.Plugins.Essentials which is part +* of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// A structure that contains the client/server information + /// for client/server authorization + /// + /// + /// The public portion of the token to send to the client + /// + /// + /// The secret portion of the token that is to be + /// stored on the server (usually in the client's session) + /// + public readonly record struct ClientSecurityToken(string ClientToken, string ServerToken) + { } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs b/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs new file mode 100644 index 0000000..c30796b --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs @@ -0,0 +1,90 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IAccountSecurityProvider.cs +* +* IAccountSecurityProvider.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils; +using VNLib.Plugins.Essentials.Users; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Provides account security to client connections. Providing authoirzation, + /// verification, and client data encryption. + /// + public interface IAccountSecurityProvider + { + /// + /// Generates a new authorization for the connection with its client security information + /// + /// The connection to authorize + /// The client security information required for authorization + /// The user object to authorize the connection for + /// The new authorization information for the connection + IClientAuthorization AuthorizeClient(HttpEntity entity, IClientSecInfo clientInfo, IUser user); + + /// + /// Regenerates the client's authorization status for a currently logged-in user + /// + /// The connection to re-authorize + /// The new containing the new authorization information + IClientAuthorization ReAuthorizeClient(HttpEntity entity); + + /// + /// Determines if the connection is considered authorized for the desired + /// security level + /// + /// The connection to determine the status of + /// The authorziation level to check for + /// True if the given connection meets the desired authorzation status + bool IsClientAuthorized(HttpEntity entity, AuthorzationCheckLevel level); + + /// + /// Encryptes data using the stored client's authorization information. + /// + /// The connection to encrypt data for + /// The data to encrypt + /// The buffer to write the encrypted data to + /// The number of bytes written to the output buffer, or o/false if the data could not be encrypted + ERRNO TryEncryptClientData(HttpEntity entity, ReadOnlySpan data, Span outputBuffer); + + /// + /// Attempts a one-time encryption of client data for a non-authorized user + /// based on the client's data. + /// + /// The client's credentials used to encrypt the message + /// The data to encrypt + /// The output buffer to write encrypted data to + /// The number of bytes written to the output buffer, 0/false if the operation failed + ERRNO TryEncryptClientData(IClientSecInfo clientSecInfo, ReadOnlySpan data, Span outputBuffer); + + /// + /// Invalidates a logged in connection + /// + /// The connection to invalidate the login status of + void InvalidateLogin(HttpEntity entity); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs b/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs new file mode 100644 index 0000000..02bc96e --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IClientAuthorization.cs +* +* IClientAuthorization.cs is part of VNLib.Plugins.Essentials which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Contains the client's minimum authorization variables + /// + public interface IClientAuthorization + { + /// + /// A security token that may be set as a cookie or used + /// + string? LoginSecurityString { get; } + + /// + /// The clients security token information + /// + ClientSecurityToken SecurityToken { get; } + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs b/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs new file mode 100644 index 0000000..6990191 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: AccountUtil.cs +* +* AccountUtil.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Exposed the required security information for a + /// to authorized a connection. + /// + public interface IClientSecInfo + { + /// + /// The clients public-key + /// + string PublicKey { get; } + + /// + /// The unique id the client provided to this server + /// + string ClientId { get; } + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs b/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs new file mode 100644 index 0000000..fc45727 --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IPasswordHashingProvider.cs +* +* IPasswordHashingProvider.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils; +using VNLib.Utils.Memory; + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// Represents a common abstraction for password hashing providers/libraries + /// + public interface IPasswordHashingProvider + { + /// + /// Verifies a password against its previously encoded hash. + /// + /// Previously hashed password + /// Raw password to compare against + /// true if bytes derrived from password match the hash, false otherwise + /// + bool Verify(ReadOnlySpan passHash, ReadOnlySpan password); + + /// + /// Verifies a password against its previously encoded hash. + /// + /// Previously hashed password in binary + /// Raw password to compare against the hash + /// true if bytes derrived from password match the hash, false otherwise + /// + bool Verify(ReadOnlySpan passHash, ReadOnlySpan password); + + /// + /// Hashes the specified character encoded password to it's secured hashed form. + /// + /// The character encoded password to encrypt + /// A containing the new password hash. + /// + PrivateString Hash(ReadOnlySpan password); + + /// + /// Hashes the specified binary encoded password to it's secured hashed form. + /// + /// The binary encoded password to encrypt + /// A containing the new password hash. + /// + PrivateString Hash(ReadOnlySpan password); + + /// + /// Exposes a lower level for producing a password hash and writing it to the output buffer + /// + /// The raw password to encrypt + /// The output buffer to write encoded data into + /// The number of bytes written to the hash buffer, or 0/false if the hashing operation failed + /// + ERRNO Hash(ReadOnlySpan password, Span hashOutput); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs b/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs index ebc616e..96bf261 100644 --- a/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs +++ b/lib/Plugins.Essentials/src/Accounts/LoginMessage.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -36,7 +36,7 @@ namespace VNLib.Plugins.Essentials.Accounts /// NOTE: This class derrives from /// and should be disposed properly /// - public class LoginMessage : PrivateStringManager + public class LoginMessage : PrivateStringManager, IClientSecInfo { /// /// A property @@ -80,7 +80,8 @@ namespace VNLib.Plugins.Essentials.Accounts /// The clients browser id if shared /// [JsonPropertyName("clientid")] - public string ClientID { get; set; } + public string ClientId { get; set; } + /// /// Initailzies a new and its parent /// base @@ -98,5 +99,10 @@ namespace VNLib.Plugins.Essentials.Accounts /// or access to will throw /// protected LoginMessage(int protectedElementSize = 1) : base(protectedElementSize) { } + + /* + * Support client security info + */ + string IClientSecInfo.PublicKey => ClientPublicKey; } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs b/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs new file mode 100644 index 0000000..3ad05ab --- /dev/null +++ b/lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: PasswordChallengeResult.cs +* +* PasswordChallengeResult.cs is part of VNLib.Plugins.Essentials which +* is part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +#nullable enable + +namespace VNLib.Plugins.Essentials.Accounts +{ + /// + /// A password pased client/server challenge + /// + /// The client portion of the password based challenge + /// The server potion of the password based challenge + public readonly record struct PasswordChallengeResult(string ClientData, string ServerData) + { } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs index 553b41c..db5b309 100644 --- a/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs +++ b/lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -31,14 +31,17 @@ using VNLib.Utils.Memory; namespace VNLib.Plugins.Essentials.Accounts { + /// /// Provides a structured password hashing system implementing the library /// with fixed time comparison /// - public sealed class PasswordHashing + public sealed class PasswordHashing : IPasswordHashingProvider { + private const int STACK_MAX_BUFF_SIZE = 64; + private readonly ISecretProvider _secret; - + private readonly uint TimeCost; private readonly uint MemoryCost; private readonly uint HashLen; @@ -71,53 +74,50 @@ namespace VNLib.Plugins.Essentials.Accounts SaltLen = saltLen; Parallelism = parallism < 1 ? (uint)Environment.ProcessorCount : parallism; } - - /// - /// Verifies a password against its previously encoded hash. - /// - /// Previously hashed password - /// Raw password to compare against - /// true if bytes derrived from password match the hash, false otherwise - /// - /// - /// - public bool Verify(PrivateString passHash, PrivateString password) - { - //Casting PrivateStrings to spans will reference the base string directly - return Verify((ReadOnlySpan)passHash, (ReadOnlySpan)password); - } - /// - /// Verifies a password against its previously encoded hash. - /// - /// Previously hashed password - /// Raw password to compare against - /// true if bytes derrived from password match the hash, false otherwise - /// - /// - /// + + /// + /// + /// public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) { if(passHash.IsEmpty || password.IsEmpty) { return false; } - //alloc secret buffer - using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); + + if(_secret.BufferSize < STACK_MAX_BUFF_SIZE) + { + //Alloc stack buffer + Span secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE]; + + return VerifyInternal(passHash, password, secretBuffer); + } + else + { + //Alloc heap buffer + using UnsafeMemoryHandle secretBuffer = MemoryUtil.UnsafeAlloc(_secret.BufferSize, true); + + return VerifyInternal(passHash, password, secretBuffer); + } + } + + private bool VerifyInternal(ReadOnlySpan passHash, ReadOnlySpan password, Span secretBuffer) + { try { //Get the secret from the callback - ERRNO count = _secret.GetSecret(secretBuffer.Span); + ERRNO count = _secret.GetSecret(secretBuffer); //Verify - return VnArgon2.Verify2id(password, passHash, secretBuffer.Span[..(int)count]); + return VnArgon2.Verify2id(password, passHash, secretBuffer[..(int)count]); } finally { //Erase secret buffer - MemoryUtil.InitializeBlock(secretBuffer.Span); + MemoryUtil.InitializeBlock(secretBuffer); } } - + /// /// Verifies a password against its hash. Partially exposes the Argon2 api. /// @@ -136,19 +136,8 @@ namespace VNLib.Plugins.Essentials.Accounts //Compare the hashed password to the specified hash and return results return CryptographicOperations.FixedTimeEquals(hash, hashBuf.Span); } - - /// - /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. - /// - /// Password to be hashed - /// - /// A of the hashed and encoded password - public PrivateString Hash(PrivateString password) => Hash((ReadOnlySpan)password); - /// - /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes. - /// - /// Password to be hashed + /// /// /// A of the hashed and encoded password public PrivateString Hash(ReadOnlySpan password) @@ -175,11 +164,8 @@ namespace VNLib.Plugins.Essentials.Accounts MemoryUtil.InitializeBlock(buffer.Span); } } - - /// - /// Hashes a specified password, with the initialized pepper, and salted with a CNG random bytes. - /// - /// Password to be hashed + + /// /// /// A of the hashed and encoded password public PrivateString Hash(ReadOnlySpan password) @@ -231,5 +217,76 @@ namespace VNLib.Plugins.Essentials.Accounts MemoryUtil.InitializeBlock(secretBuffer.Span); } } + + /// + /// NOT SUPPORTED! Use + /// instead to specify the salt that was used to encypt the original password + /// + /// + /// + /// + public bool Verify(ReadOnlySpan passHash, ReadOnlySpan password) + { + throw new NotSupportedException(); + } + + /// + /// + public ERRNO Hash(ReadOnlySpan password, Span hashOutput) + { + //Calc the min buffer size + int minBufferSize = SaltLen + _secret.BufferSize + (int)HashLen; + + //Alloc heap buffer + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(minBufferSize, true); + try + { + //Segment the buffer + HashBufferSegments segments = new(buffer.Span, _secret.BufferSize, SaltLen, (int)HashLen); + + //Fill the buffer with random bytes + RandomHash.GetRandomBytes(segments.SaltBuffer); + + //recover the secret + ERRNO count = _secret.GetSecret(segments.SecretBuffer); + + //Hash the password in binary and write the secret to the binary buffer + VnArgon2.Hash2id(password, segments.SaltBuffer, segments.SecretBuffer[..(int)count], segments.HashBuffer, TimeCost, MemoryCost, Parallelism); + + //Hash size is the desired hash size + return new((int)HashLen); + } + finally + { + MemoryUtil.InitializeBlock(buffer.Span); + } + } + + private readonly ref struct HashBufferSegments + { + public readonly Span SaltBuffer; + + public readonly Span SecretBuffer; + + public readonly Span HashBuffer; + + public HashBufferSegments(Span buffer, int secretSize, int saltSize, int hashSize) + { + //Salt buffer is begining segment + SaltBuffer = buffer[..saltSize]; + + //Shift to end of salt buffer + buffer = buffer[saltSize..]; + + //Store secret buffer + SecretBuffer = buffer[..secretSize]; + + //Shift to end of secret buffer + buffer = buffer[secretSize..]; + + //Store remaining size as hash buffer + HashBuffer = buffer[..hashSize]; + } + } } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs b/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs index bced960..c529028 100644 --- a/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs +++ b/lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -36,6 +36,12 @@ namespace VNLib.Plugins.Essentials.Endpoints /// public abstract class ProtectedWebEndpoint : UnprotectedWebEndpoint { + /// + /// Gets the minium required by a client to + /// access this endpoint + /// + protected virtual AuthorzationCheckLevel AuthLevel { get; } = AuthorzationCheckLevel.Critical; + /// protected override ERRNO PreProccess(HttpEntity entity) { @@ -43,14 +49,16 @@ namespace VNLib.Plugins.Essentials.Endpoints { return false; } - //The loggged in flag must be set, and the token must also match - if (!entity.LoginCookieMatches() || !entity.TokenMatches()) + + //Require full authorization to the resource + if (!entity.IsClientAuthorized(AuthLevel)) { //Return unauthorized status entity.CloseResponse(HttpStatusCode.Unauthorized); //A return value less than 0 signals a virtual skip event return -1; } + //Continue return true; } diff --git a/lib/Plugins.Essentials/src/EventProcessor.cs b/lib/Plugins.Essentials/src/EventProcessor.cs index d34cf95..ccaa1a1 100644 --- a/lib/Plugins.Essentials/src/EventProcessor.cs +++ b/lib/Plugins.Essentials/src/EventProcessor.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -38,18 +38,20 @@ using VNLib.Utils.Resources; using VNLib.Plugins.Essentials.Content; using VNLib.Plugins.Essentials.Sessions; using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Essentials.Accounts; #nullable enable namespace VNLib.Plugins.Essentials { + /// /// Provides an abstract base implementation of /// that breaks down simple processing procedures, routing, and session /// loading. /// - public abstract class EventProcessor : IWebRoot + public abstract class EventProcessor : IWebRoot, IWebProcessor { private static readonly AsyncLocal _currentProcessor = new(); @@ -70,6 +72,9 @@ namespace VNLib.Plugins.Essentials /// public abstract IEpProcessingOptions Options { get; } + /// + public abstract IReadOnlyDictionary Redirects { get; } + /// /// Event log provider /// @@ -112,23 +117,10 @@ namespace VNLib.Plugins.Essentials /// The selected file processing routine for the given request public abstract void PostProcessFile(HttpEntity entity, in FileProcessArgs chosenRoutine); - #region redirects - /// - public IReadOnlyDictionary Redirects => _redirects; - - private Dictionary _redirects = new(); + #region security - /// - /// Initializes 301 redirects table from a collection of redirects - /// - /// A collection of redirects - public void SetRedirects(IEnumerable redirs) - { - //To dictionary - Dictionary r = redirs.ToDictionary(r => r.Url, r => r, StringComparer.OrdinalIgnoreCase); - //Swap - _ = Interlocked.Exchange(ref _redirects, r); - } + /// + public abstract IAccountSecurityProvider AccountSecurity { get; } #endregion @@ -204,7 +196,7 @@ namespace VNLib.Plugins.Essentials /// A "lookup table" that represents virtual endpoints to be processed when an /// incomming connection matches its path parameter /// - private Dictionary> VirtualEndpoints = new(); + private IReadOnlyDictionary> VirtualEndpoints = new Dictionary>(); /* @@ -271,7 +263,7 @@ namespace VNLib.Plugins.Essentials { _ = eps ?? throw new ArgumentNullException(nameof(eps)); //Call remove on path - RemoveVirtualEndpoint(eps.Select(static s => s.Path).ToArray()); + RemoveEndpoint(eps.Select(static s => s.Path).ToArray()); } /// @@ -281,9 +273,10 @@ namespace VNLib.Plugins.Essentials /// /// /// - public void RemoveVirtualEndpoint(params string[] paths) + public void RemoveEndpoint(params string[] paths) { _ = paths ?? throw new ArgumentNullException(nameof(paths)); + //Make sure all endpoints specify a path if (paths.Any(static e => string.IsNullOrWhiteSpace(e))) { diff --git a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs index 4fd77a6..4179f74 100644 --- a/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs +++ b/lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -322,10 +322,10 @@ namespace VNLib.Plugins.Essentials.Extensions /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan data) + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data) { //Get a memory stream using UTF8 encoding - CloseResponse(ev, code, type, in data, ev.Server.Encoding); + CloseResponse(ev, code, type, data, ev.Server.Encoding); } /// @@ -338,7 +338,7 @@ namespace VNLib.Plugins.Essentials.Extensions /// The encoding type to use when converting the buffer /// This method will store an encoded copy as a memory stream, so be careful with large buffers [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan data, Encoding encoding) + public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan data, Encoding encoding) { if (data.IsEmpty) { @@ -479,7 +479,7 @@ namespace VNLib.Plugins.Essentials.Extensions try { //Deserialize and return the object - obj = value.AsJsonObject(options); + obj = JsonSerializer.Deserialize(value, options); return true; } catch(JsonException je) @@ -543,7 +543,7 @@ namespace VNLib.Plugins.Essentials.Extensions try { //Beware this will buffer the entire file object before it attmepts to de-serialize it - return VnEncoding.JSONDeserializeFromBinary(file.FileData, options); + return JsonSerializer.Deserialize(file.FileData, options); } catch (JsonException je) { diff --git a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs index d7f73a3..332e3d6 100644 --- a/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs +++ b/lib/Plugins.Essentials/src/Extensions/HttpCookie.cs @@ -37,11 +37,33 @@ namespace VNLib.Plugins.Essentials.Extensions /// The cookie value public readonly record struct HttpCookie (string Name, string Value) { + /// + /// The length of time the cookie is valid for + /// public readonly TimeSpan ValidFor { get; init; } = TimeSpan.MaxValue; - public readonly string Domain { get; init; } = ""; - public readonly string Path { get; init; } = "/"; + /// + /// The cookie's domain parameter. If null, is not set in the + /// Set-Cookie header. + /// + public readonly string? Domain { get; init; } = null; + /// + /// The cookies path parameter. If null, is not + /// set in the Set-Cookie header. + /// + public readonly string? Path { get; init; } = "/"; + /// + /// The cookie's same-site parameter. Default is + /// public readonly CookieSameSite SameSite { get; init; } = CookieSameSite.None; + /// + /// Sets the cookie's HttpOnly parameter. Default is false. When false, does not + /// set the HttpOnly paramter in the Set-Cookie header. + /// public readonly bool HttpOnly { get; init; } = false; + /// + /// Sets the cookie's Secure parameter. Default is false. When false, does not + /// set the Secure parameter in the Set-Cookie header. + /// public readonly bool Secure { get; init; } = false; /// diff --git a/lib/Plugins.Essentials/src/HttpEntity.cs b/lib/Plugins.Essentials/src/HttpEntity.cs index 63e61f7..f2f9387 100644 --- a/lib/Plugins.Essentials/src/HttpEntity.cs +++ b/lib/Plugins.Essentials/src/HttpEntity.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -121,7 +121,7 @@ namespace VNLib.Plugins.Essentials /// /// The requested web root. Provides additional site information /// - public readonly EventProcessor RequestedRoot; + public readonly IWebProcessor RequestedRoot; /// /// If the request has query arguments they are stored in key value format /// diff --git a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs index de79327..13dcd37 100644 --- a/lib/Plugins.Essentials/src/IEpProcessingOptions.cs +++ b/lib/Plugins.Essentials/src/IEpProcessingOptions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -27,6 +27,8 @@ using System.IO; using System.Net; using System.Collections.Generic; +using VNLib.Net.Http; + #nullable enable namespace VNLib.Plugins.Essentials @@ -62,5 +64,9 @@ namespace VNLib.Plugins.Essentials /// A for how long a connection may remain open before all operations are cancelled /// TimeSpan ExecutionTimeout { get; } + /// + /// HTTP level "hard" 301 redirects + /// + IReadOnlyDictionary HardRedirects { get; } } } \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/IWebProcessorInfo.cs b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs new file mode 100644 index 0000000..93a9211 --- /dev/null +++ b/lib/Plugins.Essentials/src/IWebProcessorInfo.cs @@ -0,0 +1,82 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials +* File: IWebProcessor.cs +* +* IWebProcessor.cs is part of VNLib.Plugins.Essentials which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + + +#nullable enable + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Accounts; + +namespace VNLib.Plugins.Essentials +{ + /// + /// Abstractions for methods and information for processors + /// + public interface IWebProcessor : IWebRoot + { + /// + /// The filesystem entrypoint path for the site + /// + string Directory { get; } + + /// + /// Gets the EP processing options + /// + IEpProcessingOptions Options { get; } + + /// + /// The shared that provides + /// user account security operations + /// + IAccountSecurityProvider AccountSecurity { get; } + + /// + /// + /// Called when the server intends to process a file and requires translation from a + /// uri path to a usable filesystem path + /// + /// + /// NOTE: This function must be thread-safe! + /// + /// + /// The path requested by the request + /// The translated and filtered filesystem path used to identify the file resource + string TranslateResourcePath(string requestPath); + + /// + /// Finds the file specified by the request and the server root the user has requested. + /// Determines if it exists, has permissions to access it, and allowed file attributes. + /// Also finds default files and files without extensions + /// + bool FindResourceInRoot(string resourcePath, bool fullyQualified, out string path); + + /// + /// Determines if a requested resource exists within the and is allowed to be accessed. + /// + /// The path to the resource + /// An out parameter that is set to the absolute path to the existing and accessable resource + /// True if the resource exists and is allowed to be accessed + bool FindResourceInRoot(string resourcePath, out string path); + } +} \ No newline at end of file diff --git a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs index 892a24c..11ab61a 100644 --- a/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs +++ b/lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -22,13 +22,12 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System; using System.Net; -using System.Text; using VNLib.Net.Http; -using VNLib.Utils; -using VNLib.Utils.IO; -using VNLib.Utils.Memory.Caching; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Extensions; namespace VNLib.Plugins.Essentials.Oauth @@ -74,11 +73,6 @@ namespace VNLib.Plugins.Essentials.Oauth public static class OauthHttpExtensions { - private static ThreadLocalObjectStorage SbRental { get; } = ObjectRental.CreateThreadLocal(Constructor, null, ReturnFunc); - - private static StringBuilder Constructor() => new(64); - private static void ReturnFunc(StringBuilder sb) => sb.Clear(); - /// /// Closes the current response with a json error message with the message details /// @@ -86,134 +80,53 @@ namespace VNLib.Plugins.Essentials.Oauth /// The http status code /// The short error /// The error description message - public static void CloseResponseError(this HttpEntity ev, HttpStatusCode code, ErrorType error, string description) + public static void CloseResponseError(this IHttpEvent ev, HttpStatusCode code, ErrorType error, ReadOnlySpan description) { //See if the response accepts json if (ev.Server.Accepts(ContentType.Json)) { - //Use a stringbuilder to create json result for the error description - StringBuilder sb = SbRental.Rent(); - sb.Append("{\"error\":\""); - switch (error) - { - case ErrorType.InvalidRequest: - sb.Append("invalid_request"); - break; - case ErrorType.InvalidClient: - sb.Append("invalid_client"); - break; - case ErrorType.UnauthorizedClient: - sb.Append("unauthorized_client"); - break; - case ErrorType.InvalidToken: - sb.Append("invalid_token"); - break; - case ErrorType.UnsupportedResponseType: - sb.Append("unsupported_response_type"); - break; - case ErrorType.InvalidScope: - sb.Append("invalid_scope"); - break; - case ErrorType.ServerError: - sb.Append("server_error"); - break; - case ErrorType.TemporarilyUnabavailable: - sb.Append("temporarily_unavailable"); - break; - default: - sb.Append("error"); - break; - } - sb.Append("\",\"error_description\":\""); - sb.Append(description); - sb.Append("\"}"); - //Close the response with the json data - ev.CloseResponse(code, ContentType.Json, sb.ToString()); - //Return the builder - SbRental.Return(sb); - } - //Otherwise set the error code in the wwwauth header - else - { - //Set the error result in the header - ev.Server.Headers[HttpResponseHeader.WwwAuthenticate] = error switch - { - ErrorType.InvalidRequest => $"Bearer error=\"invalid_request\"", - ErrorType.UnauthorizedClient => $"Bearer error=\"unauthorized_client\"", - ErrorType.UnsupportedResponseType => $"Bearer error=\"unsupported_response_type\"", - ErrorType.InvalidScope => $"Bearer error=\"invalid_scope\"", - ErrorType.ServerError => $"Bearer error=\"server_error\"", - ErrorType.TemporarilyUnabavailable => $"Bearer error=\"temporarily_unavailable\"", - ErrorType.InvalidClient => $"Bearer error=\"invalid_client\"", - ErrorType.InvalidToken => $"Bearer error=\"invalid_token\"", - _ => $"Bearer error=\"error\"", - }; - //Close the response with the status code - ev.CloseResponse(code); - } - } - /// - /// Closes the current response with a json error message with the message details - /// - /// - /// The http status code - /// The short error - /// The error description message - public static void CloseResponseError(this IHttpEvent ev, HttpStatusCode code, ErrorType error, string description) - { - //See if the response accepts json - if (ev.Server.Accepts(ContentType.Json)) - { - //Use a stringbuilder to create json result for the error description - StringBuilder sb = SbRental.Rent(); - sb.Append("{\"error\":\""); + //Alloc char buffer to write output to, nearest page should give us enough room + using UnsafeMemoryHandle buffer = MemoryUtil.UnsafeAllocNearestPage(description.Length + 64); + ForwardOnlyWriter writer = new(buffer.Span); + + //Build the error message string + writer.Append("{\"error\":\""); switch (error) { case ErrorType.InvalidRequest: - sb.Append("invalid_request"); + writer.Append("invalid_request"); break; case ErrorType.InvalidClient: - sb.Append("invalid_client"); + writer.Append("invalid_client"); break; case ErrorType.UnauthorizedClient: - sb.Append("unauthorized_client"); + writer.Append("unauthorized_client"); break; case ErrorType.InvalidToken: - sb.Append("invalid_token"); + writer.Append("invalid_token"); break; case ErrorType.UnsupportedResponseType: - sb.Append("unsupported_response_type"); + writer.Append("unsupported_response_type"); break; case ErrorType.InvalidScope: - sb.Append("invalid_scope"); + writer.Append("invalid_scope"); break; case ErrorType.ServerError: - sb.Append("server_error"); + writer.Append("server_error"); break; case ErrorType.TemporarilyUnabavailable: - sb.Append("temporarily_unavailable"); + writer.Append("temporarily_unavailable"); break; default: - sb.Append("error"); + writer.Append("error"); break; } - sb.Append("\",\"error_description\":\""); - sb.Append(description); - sb.Append("\"}"); + writer.Append("\",\"error_description\":\""); + writer.Append(description); + writer.Append("\"}"); - VnMemoryStream vms = VnEncoding.GetMemoryStream(sb.ToString(), ev.Server.Encoding); - try - { - //Close the response with the json data - ev.CloseResponse(code, ContentType.Json, vms); - } - catch - { - vms.Dispose(); - throw; - } - //Return the builder - SbRental.Return(sb); + //Close the response with the json data + ev.CloseResponse(code, ContentType.Json, writer.AsSpan()); } //Otherwise set the error code in the wwwauth header else diff --git a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs index c8cab0d..46e6ec8 100644 --- a/lib/Plugins.Essentials/src/Sessions/SessionBase.cs +++ b/lib/Plugins.Essentials/src/Sessions/SessionBase.cs @@ -26,9 +26,7 @@ using System; using System.Net; using System.Runtime.CompilerServices; -using VNLib.Net.Http; using VNLib.Utils; -using VNLib.Utils.Async; namespace VNLib.Plugins.Essentials.Sessions { @@ -36,7 +34,7 @@ namespace VNLib.Plugins.Essentials.Sessions /// Provides a base class for the interface for exclusive use within a multithreaded /// context /// - public abstract class SessionBase : AsyncExclusiveResource, ISession + public abstract class SessionBase : ISession { protected const ulong MODIFIED_MSK = 0b0000000000000001UL; protected const ulong IS_NEW_MSK = 0b0000000000000010UL; @@ -68,24 +66,16 @@ namespace VNLib.Plugins.Essentials.Sessions } /// - public virtual string SessionID { get; protected set; } + public abstract string SessionID { get; } /// - public virtual DateTimeOffset Created { get; protected set; } + public abstract DateTimeOffset Created { get; set; } /// /// public string this[string index] { - get - { - Check(); - return IndexerGet(index); - } - set - { - Check(); - IndexerSet(index, value); - } + get => IndexerGet(index); + set => IndexerSet(index, value); } /// public virtual IPAddress UserIP diff --git a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs index 816fa94..a5d7c4d 100644 --- a/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs +++ b/lib/Plugins.Essentials/src/Sessions/SessionInfo.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials @@ -24,14 +24,15 @@ using System; using System.Net; +using System.Text.Json; using System.Security.Authentication; using System.Runtime.CompilerServices; using VNLib.Utils; using VNLib.Net.Http; -using VNLib.Utils.Extensions; using static VNLib.Plugins.Essentials.Statics; + /* * SessionInfo is a structure since it is only meant used in * an HttpEntity context, so it may be allocated as part of @@ -43,6 +44,8 @@ using static VNLib.Plugins.Essentials.Statics; #pragma warning disable CA1051 // Do not declare visible instance fields +#nullable enable + namespace VNLib.Plugins.Essentials.Sessions { /// @@ -55,13 +58,46 @@ namespace VNLib.Plugins.Essentials.Sessions /// public readonly struct SessionInfo : IObjectStorage, IEquatable { + /* + * Store status flags as a 1 byte enum + */ + [Flags] + private enum SessionFlags : byte + { + None = 0x00, + IsSet = 0x01, + IpMatch = 0x02, + IsCrossOrigin = 0x04, + CrossOriginMatch = 0x08, + } + + private readonly ISession UserSession; + private readonly SessionFlags _flags; + /// /// A value indicating if the current instance has been initiailzed /// with a session. Otherwise properties are undefied /// - public readonly bool IsSet; + public readonly bool IsSet + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IsSet); + } - private readonly ISession UserSession; + /// + /// The origin header specified during session creation + /// + public readonly Uri? SpecifiedOrigin; + + /// + /// Was the session Initialy established on a secure connection? + /// + public readonly SslProtocols SecurityProcol; + + /// + /// Session stored User-Agent + /// + public readonly string? UserAgent; /// /// Key that identifies the current session. (Identical to cookie::sessionid) @@ -71,51 +107,52 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] get => UserSession.SessionID; } - /// - /// Session stored User-Agent - /// - public readonly string UserAgent; + /// /// If the stored IP and current user's IP matches /// - public readonly bool IPMatch; + public readonly bool IPMatch + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IpMatch); + } + /// /// If the current connection and stored session have matching cross origin domains /// - public readonly bool CrossOriginMatch; - /// - /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid and will throw an exception when accessed - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Invalidate(bool all = false) => UserSession.Invalidate(all); - /// - /// Marks the session ID to be regenerated during closing event - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RegenID() => UserSession.RegenID(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T GetObject(string key) => this[key].AsJsonObject(SR_OPTIONS); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void SetObject(string key, T obj) => this[key] = obj?.ToJsonString(SR_OPTIONS); + public readonly bool CrossOriginMatch + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.CrossOriginMatch); + } /// /// Was the original session cross origin? /// - public readonly bool CrossOrigin; + public readonly bool CrossOrigin + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _flags.HasFlag(SessionFlags.IsCrossOrigin); + } + /// - /// The origin header specified during session creation + /// Was this session just created on this connection? /// - public readonly Uri SpecifiedOrigin; + public readonly bool IsNew + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => UserSession.IsNew; + } + /// /// The time the session was created /// - public readonly DateTimeOffset Created; - /// - /// Was this session just created on this connection? - /// - public readonly bool IsNew; + public readonly DateTimeOffset Created + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => UserSession.Created; + } + /// /// Gets or sets the session's login hash, if set to a non-empty/null value, will trigger an upgrade on close /// @@ -126,6 +163,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.SetLoginToken(value); } + /// /// Gets or sets the session's login token, if set to a non-empty/null value, will trigger an upgrade on close /// @@ -136,6 +174,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.Token = value; } + /// /// /// Gets or sets the user-id for the current session. @@ -151,6 +190,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.UserID = value; } + /// /// Privilages associated with user specified during login /// @@ -161,6 +201,7 @@ namespace VNLib.Plugins.Essentials.Sessions [MethodImpl(MethodImplOptions.AggressiveInlining)] set => UserSession.Privilages = value; } + /// /// The IP address belonging to the client /// @@ -168,11 +209,8 @@ namespace VNLib.Plugins.Essentials.Sessions { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => UserSession.UserIP; - } - /// - /// Was the session Initialy established on a secure connection? - /// - public readonly SslProtocols SecurityProcol; + } + /// /// A value specifying the type of the backing session /// @@ -182,6 +220,27 @@ namespace VNLib.Plugins.Essentials.Sessions get => UserSession.SessionType; } + /// + /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid, no data + /// will be saved to the session store when the session closes + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Invalidate(bool all = false) => UserSession.Invalidate(all); + /// + /// Marks the session ID to be regenerated during closing event + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RegenID() => UserSession.RegenID(); + +#nullable disable + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T GetObject(string key) => JsonSerializer.Deserialize(this[key], SR_OPTIONS); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetObject(string key, T obj) => this[key] = obj == null ? null: JsonSerializer.Serialize(obj, SR_OPTIONS); +#nullable enable + /// /// Accesses the session's general storage /// @@ -198,37 +257,45 @@ namespace VNLib.Plugins.Essentials.Sessions internal SessionInfo(ISession session, IConnectionInfo ci, IPAddress trueIp) { UserSession = session; - //Calculate and store - IsNew = session.IsNew; - Created = session.Created; - //Ip match - IPMatch = trueIp.Equals(session.UserIP); + + SessionFlags flags = SessionFlags.IsSet; + + //Set ip match flag if current ip and stored ip match + flags |= trueIp.Equals(session.UserIP) ? SessionFlags.IpMatch : SessionFlags.None; + //If the session is new, we can store intial security variables if (session.IsNew) { session.InitNewSession(ci); + //Since all values will be the same as the connection, cache the connection values UserAgent = ci.UserAgent; SpecifiedOrigin = ci.Origin; - CrossOrigin = ci.CrossOrigin; SecurityProcol = ci.SecurityProtocol; + + flags |= ci.CrossOrigin ? SessionFlags.IsCrossOrigin : SessionFlags.None; } else { //Load/decode stored variables UserAgent = session.GetUserAgent(); SpecifiedOrigin = session.GetOriginUri(); - CrossOrigin = session.IsCrossOrigin(); SecurityProcol = session.GetSecurityProtocol(); + + flags |= session.IsCrossOrigin() ? SessionFlags.IsCrossOrigin : SessionFlags.None; } - CrossOriginMatch = ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin); - IsSet = true; + + //Set cross origin orign match flags, if the stored origin, and connection origin + flags |= ci.Origin != null && ci.Origin.Equals(SpecifiedOrigin) ? SessionFlags.CrossOriginMatch : SessionFlags.None; + + //store flags + _flags = flags; } /// public bool Equals(SessionInfo other) => SessionID.Equals(other.SessionID, StringComparison.Ordinal); /// - public override bool Equals(object obj) => obj is SessionInfo si && Equals(si); + public override bool Equals(object? obj) => obj is SessionInfo si && Equals(si); /// public override int GetHashCode() => SessionID.GetHashCode(StringComparison.Ordinal); /// -- cgit