aboutsummaryrefslogtreecommitdiff
path: root/lib/Plugins.Essentials/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:28 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-03-09 01:48:28 -0500
commit5ddef0fcb742e77b99a0e17015d2eea0a1d4131a (patch)
treec1c88284b11b70d9f373215d8d54e8a168cc5700 /lib/Plugins.Essentials/src
parentdab71d5597fdfbe71f6ac310a240835716e952a5 (diff)
Omega cache, session, and account provider complete overhaul
Diffstat (limited to 'lib/Plugins.Essentials/src')
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AccountUtils.cs807
-rw-r--r--lib/Plugins.Essentials/src/Accounts/AuthorzationCheckLevel.cs56
-rw-r--r--lib/Plugins.Essentials/src/Accounts/ClientSecurityToken.cs43
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IAccountSecurityProvider.cs90
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IClientAuthorization.cs45
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IClientSecInfo.cs46
-rw-r--r--lib/Plugins.Essentials/src/Accounts/IPasswordHashingProvider.cs80
-rw-r--r--lib/Plugins.Essentials/src/Accounts/LoginMessage.cs12
-rw-r--r--lib/Plugins.Essentials/src/Accounts/PasswordChallengeResult.cs38
-rw-r--r--lib/Plugins.Essentials/src/Accounts/PasswordHashing.cs157
-rw-r--r--lib/Plugins.Essentials/src/Endpoints/ProtectedWebEndpoint.cs14
-rw-r--r--lib/Plugins.Essentials/src/EventProcessor.cs35
-rw-r--r--lib/Plugins.Essentials/src/Extensions/EssentialHttpEventExtensions.cs12
-rw-r--r--lib/Plugins.Essentials/src/Extensions/HttpCookie.cs26
-rw-r--r--lib/Plugins.Essentials/src/HttpEntity.cs4
-rw-r--r--lib/Plugins.Essentials/src/IEpProcessingOptions.cs8
-rw-r--r--lib/Plugins.Essentials/src/IWebProcessorInfo.cs82
-rw-r--r--lib/Plugins.Essentials/src/Oauth/OauthHttpExtensions.cs137
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionBase.cs20
-rw-r--r--lib/Plugins.Essentials/src/Sessions/SessionInfo.cs165
20 files changed, 1032 insertions, 845 deletions
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
/// </summary>
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;
/// <summary>
- /// The maximum time in seconds for a login message to be considered valid
- /// </summary>
- public const double MAX_TIME_DIFF_SECS = 10.00;
- /// <summary>
/// The size in bytes of the random passwords generated when invoking the <see cref="SetRandomPasswordAsync(PasswordHashing, IUserManager, IUser, int)"/>
/// </summary>
public const int RANDOM_PASS_SIZE = 128;
- /// <summary>
- /// The name of the header that will identify a client's identiy
- /// </summary>
- public const string LOGIN_TOKEN_HEADER = "X-Web-Token";
+
/// <summary>
/// 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
/// </summary>
public const string LOCAL_ACCOUNT_ORIGIN = "local";
- /// <summary>
- /// The size (in bytes) of the challenge secret
- /// </summary>
- public const int CHALLENGE_SIZE = 64;
- /// <summary>
- /// The size (in bytes) of the sesssion long user-password challenge
- /// </summary>
- 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;
- /// <summary>
- /// The name of the login cookie set when a user logs in
- /// </summary>
- public const string LOGIN_COOKIE_NAME = "VNLogin";
- /// <summary>
- /// The name of the login client identifier cookie (cookie that is set fir client to use to determine if the user is logged in)
- /// </summary>
- 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);
-
- /// <summary>
- /// The client data encryption padding.
- /// </summary>
- public static readonly RSAEncryptionPadding ClientEncryptonPadding = RSAEncryptionPadding.OaepSHA256;
-
- /// <summary>
- /// The size (in bytes) of the web-token hash size
- /// </summary>
- private static readonly int TokenHashSize = (SHA384.Create().HashSize / 8);
/// <summary>
/// Speical character regual expresion for basic checks
@@ -154,7 +103,7 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <exception cref="VnArgon2Exception"></exception>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
- public static async Task<ERRNO> SetRandomPasswordAsync(this PasswordHashing passHashing, IUserManager manager, IUser user, int size = RANDOM_PASS_SIZE)
+ public static async Task<ERRNO> 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
-
/// <summary>
- /// 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
/// </summary>
- /// <param name="ev">The connection and session to log-in</param>
- /// <param name="loginMessage">The message of the client to set the log-in status of</param>
- /// <param name="user">The user to log-in</param>
- /// <returns>The encrypted base64 token secret data to send to the client</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- public static string GenerateAuthorization(this HttpEntity ev, LoginMessage loginMessage, IUser user)
+ /// <param name="hashing"></param>
+ /// <param name="size">The size (in bytes) of the new random password</param>
+ /// <returns>A <see cref="PrivateString"/> that contains the new password hash</returns>
+ 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<byte>.Shared.Rent(size);
+ try
+ {
+ Span<byte> span = randBuffer.AsSpan(0, size);
- /// <summary>
- /// Runs necessary operations to grant authorization to the specified user of a given session and user with provided variables
- /// </summary>
- /// <param name="ev">The connection and session to log-in</param>
- /// <param name="base64PubKey">The clients base64 public key</param>
- /// <param name="clientId">The browser/client id</param>
- /// <param name="user">The user to log-in</param>
- /// <returns>The encrypted base64 token secret data to send to the client</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- /// <exception cref="InvalidOperationException"></exception>
- 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<byte>.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
+ /// <summary>
+ /// 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.
+ /// </summary>
+ /// <param name="manager"></param>
+ /// <param name="userId">The id of the user to check the password against</param>
+ /// <param name="rawPassword">The raw password of the user to compare hashes against</param>
+ /// <param name="hashing">The password hashing tools</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that completes with the value of the password hashing match.</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ public static async Task<bool> VerifyPasswordAsync(this IUserManager manager, string userId, PrivateString rawPassword, IPasswordHashingProvider hashing, CancellationToken cancellation)
{
- public readonly Span<byte> Buffer { private get; init; }
- public readonly Span<byte> SignatureBuffer => Buffer[..64];
-
-
-
- public int ClientPbkWritten;
- public readonly Span<byte> ClientPublicKeyBuffer => Buffer.Slice(64, 1024);
- public readonly ReadOnlySpan<byte> 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<byte> ClientEncOutputBuffer => Buffer[(64 + 1024)..];
- public readonly ReadOnlySpan<byte> EncryptedOutput => ClientEncOutputBuffer[..ClientEncBytesWritten];
+ return user != null && hashing.Verify(user.PassHash.ToReadOnlySpan(), rawPassword.ToReadOnlySpan());
}
-
+
/// <summary>
- /// 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
+ /// <see cref="PasswordHashing"/> instance
/// </summary>
- /// <param name="base64clientPublicKey">The user's public key credential</param>
- /// <param name="base64Digest">The base64 encoded digest of the secret that was encrypted</param>
- /// <param name="base64ClientData">The client's user-agent header value</param>
- /// <returns>A string representing a unique signed token for a given login context</returns>
- /// <exception cref="OutOfMemoryException"></exception>
- /// <exception cref="CryptographicException"></exception>
- private static void TryGenerateToken(string base64clientPublicKey, out string base64Digest, out string base64ClientData)
+ /// <param name="user"></param>
+ /// <param name="rawPassword"></param>
+ /// <param name="hashing">The <see cref="IPasswordHashingProvider"/> provider instance</param>
+ /// <returns>True if the password </returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool VerifyPassword(this IUser user, PrivateString rawPassword, IPasswordHashingProvider hashing)
{
- //Temporary work buffer
- using IMemoryHandle<byte> buffer = MemoryUtil.SafeAlloc<byte>(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);
}
/// <summary>
- /// Determines if the client sent a token header, and it maches against the current session
+ /// Verifies a password against its previously encoded hash.
/// </summary>
- /// <returns>true if the client set the token header, the session is loaded, and the token matches the session, false otherwise</returns>
- public static bool TokenMatches(this HttpEntity ev)
+ /// <param name="provider"></param>
+ /// <param name="passHash">Previously hashed password</param>
+ /// <param name="password">Raw password to compare against</param>
+ /// <returns>True if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ [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<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(TokenHashSize * 2, true);
- //Slice up buffers
- Span<byte> headerBuffer = buffer.Span[..TokenHashSize];
- Span<byte> 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<char>)passHash, (ReadOnlySpan<char>)password);
}
/// <summary>
- /// 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.
/// </summary>
- /// <returns>The base64 of the newly encrypted secret</returns>
- public static string? RegenerateClientToken(this HttpEntity ev)
+ /// <param name="provider"></param>
+ /// <param name="password">Password to be hashed</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
+ [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<char>)password);
}
- /// <summary>
- /// Tries to encrypt the specified data using the stored public key and store the encrypted data into
- /// the output buffer.
- /// </summary>
- /// <param name="session"></param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// stored.
- /// </returns>
- /// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(this in SessionInfo session, ReadOnlySpan<byte> data, in Span<byte> 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");
}
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="base64PubKey">A base64 encoded public key used to encrypt client data</param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// specified.
- /// </returns>
- /// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(ReadOnlySpan<char> base64PubKey, ReadOnlySpan<byte> data, in Span<byte> outputBuffer)
+ /// <param name="entity"></param>
+ /// <param name="mode">The authoziation level</param>
+ /// <returns>True if the connection has the desired authorization status</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ [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<byte> pubKeyBuffer = MemoryUtil.UnsafeAlloc<byte>(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);
}
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="rawPubKey">The raw SKI public key</param>
- /// <param name="data">Data to encrypt</param>
- /// <param name="outputBuffer">The buffer to store encrypted data in</param>
- /// <returns>
- /// The number of encrypted bytes written to the output buffer,
- /// or false (0) if the operation failed, or if no credential is
- /// specified.
- /// </returns>
+ /// <param name="entity">The connection and session to log-in</param>
+ /// <param name="secInfo">The clients login security information</param>
+ /// <param name="user">The user to log-in</param>
+ /// <returns>The encrypted base64 token secret data to send to the client</returns>
+ /// <exception cref="OutOfMemoryException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
/// <exception cref="CryptographicException"></exception>
- public static ERRNO TryEncryptClientData(ReadOnlySpan<byte> rawPubKey, ReadOnlySpan<byte> data, in Span<byte> outputBuffer)
+ /// <exception cref="InvalidOperationException"></exception>
+ 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;
- }
- /// <summary>
- /// Stores the clients public key specified during login
- /// </summary>
- /// <param name="session"></param>
- /// <param name="base64PubKey"></param>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static void SetBrowserPubKey(in SessionInfo session, string base64PubKey) => session[CLIENT_PUB_KEY_ENTRY] = base64PubKey;
+ IAccountSecurityProvider provider = entity.GetSecProviderOrThrow();
- /// <summary>
- /// Gets the clients stored public key that was specified during login
- /// </summary>
- /// <returns>The base64 encoded public key string specified at login</returns>
- [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;
+ }
/// <summary>
- /// Stores the login key as a cookie in the current session as long as the session exists
- /// </summary>/
- /// <param name="ev">The event to log-in</param>
- /// <param name="localAccount">Does the session belong to a local user account</param>
+ /// Generates a client authorization from the supplied security info
+ /// using the default <see cref="IAccountSecurityProvider"/> and
+ /// stored the required variables in the <paramref name="response"/>
+ /// response <see cref="WebMessage"/>
+ /// </summary>
+ /// <param name="entity"></param>
+ /// <param name="secInfo">The client's <see cref="IClientSecInfo"/> used to authorize the client</param>
+ /// <param name="user">The user requesting the authenticated use</param>
+ /// <param name="response">The response to store variables in</param>
+ /// <exception cref="NotSupportedException"></exception>
[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;
}
/// <summary>
- /// 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
/// </summary>
+ /// <param name="entity"></param>
+ /// <returns>The new <see cref="IClientAuthorization"/> for the regenerated credentials</returns>
+ /// <exception cref="NotSupportedException"></exception>
[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);
}
/// <summary>
- /// 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
/// </summary>
- /// <returns>True if the session is active, the cookie was properly received, and the cookie value matches the session. False otherwise</returns>
- 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<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(2 * LOGIN_COOKIE_SIZE, true);
- //Slice up buffers
- Span<byte> cookieBuffer = buffer.Span[..LOGIN_COOKIE_SIZE];
- Span<byte> 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;
+ /// <param name="entity"></param>
+ /// <param name="response">The response message to return to the client</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>The new <see cref="IClientAuthorization"/> for the regenerated credentials</returns>
+ 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();
}
-
+
/// <summary>
- /// 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
/// </summary>
- /// <param name="ev"></param>
- public static void ReconcileCookies(this HttpEntity ev)
+ /// <param name="entity"></param>
+ /// <param name="data">The data to encrypt for the current client</param>
+ /// <param name="output">The buffer to write encypted data to</param>
+ /// <exception cref="NotSupportedException"></exception>
+ /// <returns>The number of bytes encrypted and written to the output buffer</returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ERRNO TryEncryptClientData(this HttpEntity entity, ReadOnlySpan<byte> data, Span<byte> 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);
}
/// <summary>
- /// 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
/// </summary>
- /// <param name="session"></param>
- /// <returns>The last time the token was updated/generated, or <see cref="DateTimeOffset.MinValue"/> if not set</returns>
- public static DateTimeOffset LastTokenUpgrade(this in SessionInfo session)
+ /// <param name="entity"></param>
+ /// <param name="secInfo">Used for unauthorized connections to encrypt client data based on client security info</param>
+ /// <param name="data">The data to encrypt for the current client</param>
+ /// <param name="output">The buffer to write encypted data to</param>
+ /// <returns>The number of bytes encrypted and written to the output buffer</returns>
+ /// <exception cref="ArgumentNullException"></exception>
+ /// <exception cref="NotSupportedException"></exception>
+ public static ERRNO TryEncryptClientData(this HttpEntity entity, IClientSecInfo secInfo, ReadOnlySpan<byte> data, Span<byte> 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));
- /// <summary>
- /// Updates the last time the session token was set
- /// </summary>
- /// <param name="session"></param>
- /// <param name="updated">The UTC time the last token was set</param>
- 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);
+ }
/// <summary>
- /// Stores the browser's id during a login process
+ /// Invalidates the login status of the current connection and session (if session is loaded)
/// </summary>
- /// <param name="session"></param>
- /// <param name="browserId">Browser id value to store</param>
+ /// <exception cref="NotSupportedException"></exception>
[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();
+ }
/// <summary>
/// Gets the current browser's id if it was specified during login process
@@ -728,7 +457,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// <param name="session"></param>
/// <param name="value">True for a local account, false otherwise</param>
[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;
+
/// <summary>
/// Gets a value indicating if the session belongs to a local user account
/// </summary>
@@ -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
- */
-
- /// <summary>
- /// Generates a new password challenge for the current session and specified password
- /// </summary>
- /// <param name="session"></param>
- /// <param name="password">The user's password to compute the hash of</param>
- /// <returns>The raw derrivation key to send to the client</returns>
- public static byte[] GenPasswordChallenge(this in SessionInfo session, PrivateString password)
- {
- ReadOnlySpan<char> rawPass = password;
- //Calculate the password buffer size required
- int passByteCount = Encoding.UTF8.GetByteCount(rawPass);
- //Allocate the buffer
- using UnsafeMemoryHandle<byte> bufferHandle = MemoryUtil.UnsafeAlloc<byte>(passByteCount + 64, true);
- //Slice buffers
- Span<byte> utf8PassBytes = bufferHandle.Span[..passByteCount];
- Span<byte> 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);
- }
- }
- /// <summary>
- /// Verifies the stored unique digest of the user's password against
- /// the client derrived password
- /// </summary>
- /// <param name="session"></param>
- /// <param name="base64PasswordDigest">The base64 client derrived digest of the user's password to verify</param>
- /// <returns>True if formatting was correct and the derrived passwords match, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- public static bool VerifyChallenge(this in SessionInfo session, ReadOnlySpan<char> base64PasswordDigest)
- {
- string base32Digest = session[CHALLENGE_HMAC_ENTRY];
- if (string.IsNullOrWhiteSpace(base32Digest))
- {
- return false;
- }
- int bufSize = base32Digest.Length + base64PasswordDigest.Length;
- //Alloc buffer
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc<byte>(bufSize);
- //Split buffers
- Span<byte> localBuf = buffer.Span[..base32Digest.Length];
- Span<byte> 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
/// <summary>
/// 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
+{
+ /// <summary>
+ /// Specifies how critical the security check is for a user to access
+ /// a given resource
+ /// </summary>
+ public enum AuthorzationCheckLevel
+ {
+ /// <summary>
+ /// No authorization check is required.
+ /// </summary>
+ None,
+ /// <summary>
+ /// Is there any information that the client may have authorization. NOTE: Not a security check!
+ /// </summary>
+ Any,
+ /// <summary>
+ /// 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.
+ /// </summary>
+ Medium,
+ /// <summary>
+ /// The a full authorization check is required as the user may access
+ /// secure resouces.
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// A structure that contains the client/server information
+ /// for client/server authorization
+ /// </summary>
+ /// <param name="ClientToken">
+ /// The public portion of the token to send to the client
+ /// </param>
+ /// <param name="ServerToken">
+ /// The secret portion of the token that is to be
+ /// stored on the server (usually in the client's session)
+ /// </param>
+ 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
+{
+ /// <summary>
+ /// Provides account security to client connections. Providing authoirzation,
+ /// verification, and client data encryption.
+ /// </summary>
+ public interface IAccountSecurityProvider
+ {
+ /// <summary>
+ /// Generates a new authorization for the connection with its client security information
+ /// </summary>
+ /// <param name="entity">The connection to authorize</param>
+ /// <param name="clientInfo">The client security information required for authorization</param>
+ /// <param name="user">The user object to authorize the connection for</param>
+ /// <returns>The new authorization information for the connection</returns>
+ IClientAuthorization AuthorizeClient(HttpEntity entity, IClientSecInfo clientInfo, IUser user);
+
+ /// <summary>
+ /// Regenerates the client's authorization status for a currently logged-in user
+ /// </summary>
+ /// <param name="entity">The connection to re-authorize</param>
+ /// <returns>The new <see cref="IClientAuthorization"/> containing the new authorization information</returns>
+ IClientAuthorization ReAuthorizeClient(HttpEntity entity);
+
+ /// <summary>
+ /// Determines if the connection is considered authorized for the desired
+ /// security level
+ /// </summary>
+ /// <param name="entity">The connection to determine the status of</param>
+ /// <param name="level">The authorziation level to check for</param>
+ /// <returns>True if the given connection meets the desired authorzation status</returns>
+ bool IsClientAuthorized(HttpEntity entity, AuthorzationCheckLevel level);
+
+ /// <summary>
+ /// Encryptes data using the stored client's authorization information.
+ /// </summary>
+ /// <param name="entity">The connection to encrypt data for</param>
+ /// <param name="data">The data to encrypt</param>
+ /// <param name="outputBuffer">The buffer to write the encrypted data to</param>
+ /// <returns>The number of bytes written to the output buffer, or o/false if the data could not be encrypted</returns>
+ ERRNO TryEncryptClientData(HttpEntity entity, ReadOnlySpan<byte> data, Span<byte> outputBuffer);
+
+ /// <summary>
+ /// Attempts a one-time encryption of client data for a non-authorized user
+ /// based on the client's <see cref="IClientSecInfo"/> data.
+ /// </summary>
+ /// <param name="clientSecInfo">The client's <see cref="IClientSecInfo"/> credentials used to encrypt the message</param>
+ /// <param name="data">The data to encrypt</param>
+ /// <param name="outputBuffer">The output buffer to write encrypted data to</param>
+ /// <returns>The number of bytes written to the output buffer, 0/false if the operation failed</returns>
+ ERRNO TryEncryptClientData(IClientSecInfo clientSecInfo, ReadOnlySpan<byte> data, Span<byte> outputBuffer);
+
+ /// <summary>
+ /// Invalidates a logged in connection
+ /// </summary>
+ /// <param name="entity">The connection to invalidate the login status of</param>
+ 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
+{
+ /// <summary>
+ /// Contains the client's minimum authorization variables
+ /// </summary>
+ public interface IClientAuthorization
+ {
+ /// <summary>
+ /// A security token that may be set as a cookie or used
+ /// </summary>
+ string? LoginSecurityString { get; }
+
+ /// <summary>
+ /// The clients security token information
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Exposed the required security information for a <see cref="IAccountSecurityProvider"/>
+ /// to authorized a connection.
+ /// </summary>
+ public interface IClientSecInfo
+ {
+ /// <summary>
+ /// The clients public-key
+ /// </summary>
+ string PublicKey { get; }
+
+ /// <summary>
+ /// The unique id the client provided to this server
+ /// </summary>
+ 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
+{
+ /// <summary>
+ /// Represents a common abstraction for password hashing providers/libraries
+ /// </summary>
+ public interface IPasswordHashingProvider
+ {
+ /// <summary>
+ /// Verifies a password against its previously encoded hash.
+ /// </summary>
+ /// <param name="passHash">Previously hashed password</param>
+ /// <param name="password">Raw password to compare against</param>
+ /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ bool Verify(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password);
+
+ /// <summary>
+ /// Verifies a password against its previously encoded hash.
+ /// </summary>
+ /// <param name="passHash">Previously hashed password in binary</param>
+ /// <param name="password">Raw password to compare against the hash</param>
+ /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password);
+
+ /// <summary>
+ /// Hashes the specified character encoded password to it's secured hashed form.
+ /// </summary>
+ /// <param name="password">The character encoded password to encrypt</param>
+ /// <returns>A <see cref="PrivateString"/> containing the new password hash.</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ PrivateString Hash(ReadOnlySpan<char> password);
+
+ /// <summary>
+ /// Hashes the specified binary encoded password to it's secured hashed form.
+ /// </summary>
+ /// <param name="password">The binary encoded password to encrypt</param>
+ /// <returns>A <see cref="PrivateString"/> containing the new password hash.</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ PrivateString Hash(ReadOnlySpan<byte> password);
+
+ /// <summary>
+ /// Exposes a lower level for producing a password hash and writing it to the output buffer
+ /// </summary>
+ /// <param name="password">The raw password to encrypt</param>
+ /// <param name="hashOutput">The output buffer to write encoded data into</param>
+ /// <returns>The number of bytes written to the hash buffer, or 0/false if the hashing operation failed</returns>
+ /// <exception cref="NotSupportedException"></exception>
+ ERRNO Hash(ReadOnlySpan<byte> password, Span<byte> 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 <see cref="PrivateStringManager"/>
/// and should be disposed properly
/// </remarks>
- public class LoginMessage : PrivateStringManager
+ public class LoginMessage : PrivateStringManager, IClientSecInfo
{
/// <summary>
/// A property
@@ -80,7 +80,8 @@ namespace VNLib.Plugins.Essentials.Accounts
/// The clients browser id if shared
/// </summary>
[JsonPropertyName("clientid")]
- public string ClientID { get; set; }
+ public string ClientId { get; set; }
+
/// <summary>
/// Initailzies a new <see cref="LoginMessage"/> and its parent <see cref="PrivateStringManager"/>
/// base
@@ -98,5 +99,10 @@ namespace VNLib.Plugins.Essentials.Accounts
/// or access to <see cref="Password"/> will throw
/// </remarks>
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
+{
+ /// <summary>
+ /// A password pased client/server challenge
+ /// </summary>
+ /// <param name="ClientData">The client portion of the password based challenge</param>
+ /// <param name="ServerData">The server potion of the password based challenge</param>
+ 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
{
+
/// <summary>
/// Provides a structured password hashing system implementing the <seealso cref="VnArgon2"/> library
/// with fixed time comparison
/// </summary>
- 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;
}
-
- /// <summary>
- /// Verifies a password against its previously encoded hash.
- /// </summary>
- /// <param name="passHash">Previously hashed password</param>
- /// <param name="password">Raw password to compare against</param>
- /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <exception cref="VnArgon2PasswordFormatException"></exception>
- public bool Verify(PrivateString passHash, PrivateString password)
- {
- //Casting PrivateStrings to spans will reference the base string directly
- return Verify((ReadOnlySpan<char>)passHash, (ReadOnlySpan<char>)password);
- }
- /// <summary>
- /// Verifies a password against its previously encoded hash.
- /// </summary>
- /// <param name="passHash">Previously hashed password</param>
- /// <param name="password">Raw password to compare against</param>
- /// <returns>true if bytes derrived from password match the hash, false otherwise</returns>
- /// <exception cref="FormatException"></exception>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <exception cref="VnArgon2PasswordFormatException"></exception>
+
+ ///<inheritdoc/>
+ ///<exception cref="VnArgon2Exception"></exception>
+ ///<exception cref="VnArgon2PasswordFormatException"></exception>
public bool Verify(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password)
{
if(passHash.IsEmpty || password.IsEmpty)
{
return false;
}
- //alloc secret buffer
- using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAlloc<byte>(_secret.BufferSize, true);
+
+ if(_secret.BufferSize < STACK_MAX_BUFF_SIZE)
+ {
+ //Alloc stack buffer
+ Span<byte> secretBuffer = stackalloc byte[STACK_MAX_BUFF_SIZE];
+
+ return VerifyInternal(passHash, password, secretBuffer);
+ }
+ else
+ {
+ //Alloc heap buffer
+ using UnsafeMemoryHandle<byte> secretBuffer = MemoryUtil.UnsafeAlloc<byte>(_secret.BufferSize, true);
+
+ return VerifyInternal(passHash, password, secretBuffer);
+ }
+ }
+
+ private bool VerifyInternal(ReadOnlySpan<char> passHash, ReadOnlySpan<char> password, Span<byte> 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);
}
}
-
+
/// <summary>
/// Verifies a password against its hash. Partially exposes the Argon2 api.
/// </summary>
@@ -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);
}
-
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
- /// <exception cref="VnArgon2Exception"></exception>
- /// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
- public PrivateString Hash(PrivateString password) => Hash((ReadOnlySpan<char>)password);
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
+ /// <inheritdoc/>
/// <exception cref="VnArgon2Exception"></exception>
/// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
public PrivateString Hash(ReadOnlySpan<char> password)
@@ -175,11 +164,8 @@ namespace VNLib.Plugins.Essentials.Accounts
MemoryUtil.InitializeBlock(buffer.Span);
}
}
-
- /// <summary>
- /// Hashes a specified password, with the initialized pepper, and salted with a CNG random bytes.
- /// </summary>
- /// <param name="password">Password to be hashed</param>
+
+ /// <inheritdoc/>
/// <exception cref="VnArgon2Exception"></exception>
/// <returns>A <see cref="PrivateString"/> of the hashed and encoded password</returns>
public PrivateString Hash(ReadOnlySpan<byte> password)
@@ -231,5 +217,76 @@ namespace VNLib.Plugins.Essentials.Accounts
MemoryUtil.InitializeBlock(secretBuffer.Span);
}
}
+
+ /// <summary>
+ /// NOT SUPPORTED! Use <see cref="Verify(ReadOnlySpan{byte}, ReadOnlySpan{byte}, ReadOnlySpan{byte})"/>
+ /// instead to specify the salt that was used to encypt the original password
+ /// </summary>
+ /// <param name="passHash"></param>
+ /// <param name="password"></param>
+ /// <exception cref="NotSupportedException"></exception>
+ public bool Verify(ReadOnlySpan<byte> passHash, ReadOnlySpan<byte> password)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///<inheritdoc/>
+ ///<exception cref="VnArgon2Exception"></exception>
+ public ERRNO Hash(ReadOnlySpan<byte> password, Span<byte> hashOutput)
+ {
+ //Calc the min buffer size
+ int minBufferSize = SaltLen + _secret.BufferSize + (int)HashLen;
+
+ //Alloc heap buffer
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage<byte>(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<byte> SaltBuffer;
+
+ public readonly Span<byte> SecretBuffer;
+
+ public readonly Span<byte> HashBuffer;
+
+ public HashBufferSegments(Span<byte> 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
/// </summary>
public abstract class ProtectedWebEndpoint : UnprotectedWebEndpoint
{
+ /// <summary>
+ /// Gets the minium <see cref="AuthorzationCheckLevel"/> required by a client to
+ /// access this endpoint
+ /// </summary>
+ protected virtual AuthorzationCheckLevel AuthLevel { get; } = AuthorzationCheckLevel.Critical;
+
///<inheritdoc/>
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
{
+
/// <summary>
/// Provides an abstract base implementation of <see cref="IWebRoot"/>
/// that breaks down simple processing procedures, routing, and session
/// loading.
/// </summary>
- public abstract class EventProcessor : IWebRoot
+ public abstract class EventProcessor : IWebRoot, IWebProcessor
{
private static readonly AsyncLocal<EventProcessor?> _currentProcessor = new();
@@ -70,6 +72,9 @@ namespace VNLib.Plugins.Essentials
/// </summary>
public abstract IEpProcessingOptions Options { get; }
+ ///<inheritdoc/>
+ public abstract IReadOnlyDictionary<string, Redirect> Redirects { get; }
+
/// <summary>
/// Event log provider
/// </summary>
@@ -112,23 +117,10 @@ namespace VNLib.Plugins.Essentials
/// <param name="chosenRoutine">The selected file processing routine for the given request</param>
public abstract void PostProcessFile(HttpEntity entity, in FileProcessArgs chosenRoutine);
- #region redirects
- ///<inheritdoc/>
- public IReadOnlyDictionary<string, Redirect> Redirects => _redirects;
-
- private Dictionary<string, Redirect> _redirects = new();
+ #region security
- /// <summary>
- /// Initializes 301 redirects table from a collection of redirects
- /// </summary>
- /// <param name="redirs">A collection of redirects</param>
- public void SetRedirects(IEnumerable<Redirect> redirs)
- {
- //To dictionary
- Dictionary<string, Redirect> r = redirs.ToDictionary(r => r.Url, r => r, StringComparer.OrdinalIgnoreCase);
- //Swap
- _ = Interlocked.Exchange(ref _redirects, r);
- }
+ ///<inheritdoc/>
+ 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
/// </summary>
- private Dictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new();
+ private IReadOnlyDictionary<string, IVirtualEndpoint<HttpEntity>> VirtualEndpoints = new Dictionary<string, IVirtualEndpoint<HttpEntity>>();
/*
@@ -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());
}
/// <summary>
@@ -281,9 +273,10 @@ namespace VNLib.Plugins.Essentials
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="InvalidOperationException"></exception>
- 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
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="ContentTypeUnacceptableException"></exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data)
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<char> 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);
}
/// <summary>
@@ -338,7 +338,7 @@ namespace VNLib.Plugins.Essentials.Extensions
/// <param name="encoding">The encoding type to use when converting the buffer</param>
/// <remarks>This method will store an encoded copy as a memory stream, so be careful with large buffers</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, in ReadOnlySpan<char> data, Encoding encoding)
+ public static void CloseResponse(this IHttpEvent ev, HttpStatusCode code, ContentType type, ReadOnlySpan<char> data, Encoding encoding)
{
if (data.IsEmpty)
{
@@ -479,7 +479,7 @@ namespace VNLib.Plugins.Essentials.Extensions
try
{
//Deserialize and return the object
- obj = value.AsJsonObject<T>(options);
+ obj = JsonSerializer.Deserialize<T>(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<T>(file.FileData, options);
+ return JsonSerializer.Deserialize<T>(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
/// <param name="Value">The cookie value</param>
public readonly record struct HttpCookie (string Name, string Value)
{
+ /// <summary>
+ /// The length of time the cookie is valid for
+ /// </summary>
public readonly TimeSpan ValidFor { get; init; } = TimeSpan.MaxValue;
- public readonly string Domain { get; init; } = "";
- public readonly string Path { get; init; } = "/";
+ /// <summary>
+ /// The cookie's domain parameter. If null, is not set in the
+ /// Set-Cookie header.
+ /// </summary>
+ public readonly string? Domain { get; init; } = null;
+ /// <summary>
+ /// The cookies path parameter. If null, is not
+ /// set in the Set-Cookie header.
+ /// </summary>
+ public readonly string? Path { get; init; } = "/";
+ /// <summary>
+ /// The cookie's same-site parameter. Default is <see cref="CookieSameSite.None"/>
+ /// </summary>
public readonly CookieSameSite SameSite { get; init; } = CookieSameSite.None;
+ /// <summary>
+ /// Sets the cookie's HttpOnly parameter. Default is false. When false, does not
+ /// set the HttpOnly paramter in the Set-Cookie header.
+ /// </summary>
public readonly bool HttpOnly { get; init; } = false;
+ /// <summary>
+ /// Sets the cookie's Secure parameter. Default is false. When false, does not
+ /// set the Secure parameter in the Set-Cookie header.
+ /// </summary>
public readonly bool Secure { get; init; } = false;
/// <summary>
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
/// <summary>
/// The requested web root. Provides additional site information
/// </summary>
- public readonly EventProcessor RequestedRoot;
+ public readonly IWebProcessor RequestedRoot;
/// <summary>
/// If the request has query arguments they are stored in key value format
/// </summary>
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 <see cref="TimeSpan"/> for how long a connection may remain open before all operations are cancelled
/// </summary>
TimeSpan ExecutionTimeout { get; }
+ /// <summary>
+ /// HTTP level "hard" 301 redirects
+ /// </summary>
+ IReadOnlyDictionary<string, Redirect> 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
+{
+ /// <summary>
+ /// Abstractions for methods and information for processors
+ /// </summary>
+ public interface IWebProcessor : IWebRoot
+ {
+ /// <summary>
+ /// The filesystem entrypoint path for the site
+ /// </summary>
+ string Directory { get; }
+
+ /// <summary>
+ /// Gets the EP processing options
+ /// </summary>
+ IEpProcessingOptions Options { get; }
+
+ /// <summary>
+ /// The shared <see cref="IAccountSecurityProvider"/> that provides
+ /// user account security operations
+ /// </summary>
+ IAccountSecurityProvider AccountSecurity { get; }
+
+ /// <summary>
+ /// <para>
+ /// Called when the server intends to process a file and requires translation from a
+ /// uri path to a usable filesystem path
+ /// </para>
+ /// <para>
+ /// NOTE: This function must be thread-safe!
+ /// </para>
+ /// </summary>
+ /// <param name="requestPath">The path requested by the request </param>
+ /// <returns>The translated and filtered filesystem path used to identify the file resource</returns>
+ string TranslateResourcePath(string requestPath);
+
+ /// <summary>
+ /// 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
+ /// </summary>
+ bool FindResourceInRoot(string resourcePath, bool fullyQualified, out string path);
+
+ /// <summary>
+ /// Determines if a requested resource exists within the <see cref="EventProcessor"/> and is allowed to be accessed.
+ /// </summary>
+ /// <param name="resourcePath">The path to the resource</param>
+ /// <param name="path">An out parameter that is set to the absolute path to the existing and accessable resource</param>
+ /// <returns>True if the resource exists and is allowed to be accessed</returns>
+ 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<StringBuilder> SbRental { get; } = ObjectRental.CreateThreadLocal(Constructor, null, ReturnFunc);
-
- private static StringBuilder Constructor() => new(64);
- private static void ReturnFunc(StringBuilder sb) => sb.Clear();
-
/// <summary>
/// Closes the current response with a json error message with the message details
/// </summary>
@@ -86,134 +80,53 @@ namespace VNLib.Plugins.Essentials.Oauth
/// <param name="code">The http status code</param>
/// <param name="error">The short error</param>
/// <param name="description">The error description message</param>
- 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<char> 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);
- }
- }
- /// <summary>
- /// Closes the current response with a json error message with the message details
- /// </summary>
- /// <param name="ev"></param>
- /// <param name="code">The http status code</param>
- /// <param name="error">The short error</param>
- /// <param name="description">The error description message</param>
- 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<char> buffer = MemoryUtil.UnsafeAllocNearestPage<char>(description.Length + 64);
+ ForwardOnlyWriter<char> 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 <see cref="ISession"/> interface for exclusive use within a multithreaded
/// context
/// </summary>
- public abstract class SessionBase : AsyncExclusiveResource<IHttpEvent>, 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
}
///<inheritdoc/>
- public virtual string SessionID { get; protected set; }
+ public abstract string SessionID { get; }
///<inheritdoc/>
- public virtual DateTimeOffset Created { get; protected set; }
+ public abstract DateTimeOffset Created { get; set; }
///<inheritdoc/>
///<exception cref="ObjectDisposedException"></exception>
public string this[string index]
{
- get
- {
- Check();
- return IndexerGet(index);
- }
- set
- {
- Check();
- IndexerSet(index, value);
- }
+ get => IndexerGet(index);
+ set => IndexerSet(index, value);
}
///<inheritdoc/>
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
{
/// <summary>
@@ -55,13 +58,46 @@ namespace VNLib.Plugins.Essentials.Sessions
/// </remarks>
public readonly struct SessionInfo : IObjectStorage, IEquatable<SessionInfo>
{
+ /*
+ * 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;
+
/// <summary>
/// A value indicating if the current instance has been initiailzed
/// with a session. Otherwise properties are undefied
/// </summary>
- public readonly bool IsSet;
+ public readonly bool IsSet
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IsSet);
+ }
- private readonly ISession UserSession;
+ /// <summary>
+ /// The origin header specified during session creation
+ /// </summary>
+ public readonly Uri? SpecifiedOrigin;
+
+ /// <summary>
+ /// Was the session Initialy established on a secure connection?
+ /// </summary>
+ public readonly SslProtocols SecurityProcol;
+
+ /// <summary>
+ /// Session stored User-Agent
+ /// </summary>
+ public readonly string? UserAgent;
/// <summary>
/// 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;
}
- /// <summary>
- /// Session stored User-Agent
- /// </summary>
- public readonly string UserAgent;
+
/// <summary>
/// If the stored IP and current user's IP matches
/// </summary>
- public readonly bool IPMatch;
+ public readonly bool IPMatch
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IpMatch);
+ }
+
/// <summary>
/// If the current connection and stored session have matching cross origin domains
/// </summary>
- public readonly bool CrossOriginMatch;
- /// <summary>
- /// Flags the session as invalid. IMPORTANT: the user's session data is no longer valid and will throw an exception when accessed
- /// </summary>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void Invalidate(bool all = false) => UserSession.Invalidate(all);
- /// <summary>
- /// Marks the session ID to be regenerated during closing event
- /// </summary>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void RegenID() => UserSession.RegenID();
- ///<inheritdoc/>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public T GetObject<T>(string key) => this[key].AsJsonObject<T>(SR_OPTIONS);
- ///<inheritdoc/>
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public void SetObject<T>(string key, T obj) => this[key] = obj?.ToJsonString(SR_OPTIONS);
+ public readonly bool CrossOriginMatch
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.CrossOriginMatch);
+ }
/// <summary>
/// Was the original session cross origin?
/// </summary>
- public readonly bool CrossOrigin;
+ public readonly bool CrossOrigin
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _flags.HasFlag(SessionFlags.IsCrossOrigin);
+ }
+
/// <summary>
- /// The origin header specified during session creation
+ /// Was this session just created on this connection?
/// </summary>
- public readonly Uri SpecifiedOrigin;
+ public readonly bool IsNew
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.IsNew;
+ }
+
/// <summary>
/// The time the session was created
/// </summary>
- public readonly DateTimeOffset Created;
- /// <summary>
- /// Was this session just created on this connection?
- /// </summary>
- public readonly bool IsNew;
+ public readonly DateTimeOffset Created
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => UserSession.Created;
+ }
+
/// <summary>
/// Gets or sets the session's login hash, if set to a non-empty/null value, will trigger an upgrade on close
/// </summary>
@@ -126,6 +163,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.SetLoginToken(value);
}
+
/// <summary>
/// Gets or sets the session's login token, if set to a non-empty/null value, will trigger an upgrade on close
/// </summary>
@@ -136,6 +174,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.Token = value;
}
+
/// <summary>
/// <para>
/// 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;
}
+
/// <summary>
/// Privilages associated with user specified during login
/// </summary>
@@ -161,6 +201,7 @@ namespace VNLib.Plugins.Essentials.Sessions
[MethodImpl(MethodImplOptions.AggressiveInlining)]
set => UserSession.Privilages = value;
}
+
/// <summary>
/// The IP address belonging to the client
/// </summary>
@@ -168,11 +209,8 @@ namespace VNLib.Plugins.Essentials.Sessions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => UserSession.UserIP;
- }
- /// <summary>
- /// Was the session Initialy established on a secure connection?
- /// </summary>
- public readonly SslProtocols SecurityProcol;
+ }
+
/// <summary>
/// A value specifying the type of the backing session
/// </summary>
@@ -183,6 +221,27 @@ namespace VNLib.Plugins.Essentials.Sessions
}
/// <summary>
+ /// 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
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Invalidate(bool all = false) => UserSession.Invalidate(all);
+ /// <summary>
+ /// Marks the session ID to be regenerated during closing event
+ /// </summary>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void RegenID() => UserSession.RegenID();
+
+#nullable disable
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T GetObject<T>(string key) => JsonSerializer.Deserialize<T>(this[key], SR_OPTIONS);
+ ///<inheritdoc/>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetObject<T>(string key, T obj) => this[key] = obj == null ? null: JsonSerializer.Serialize(obj, SR_OPTIONS);
+#nullable enable
+
+ /// <summary>
/// Accesses the session's general storage
/// </summary>
/// <param name="index">Key for specifie data</param>
@@ -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;
}
///<inheritdoc/>
public bool Equals(SessionInfo other) => SessionID.Equals(other.SessionID, StringComparison.Ordinal);
///<inheritdoc/>
- public override bool Equals(object obj) => obj is SessionInfo si && Equals(si);
+ public override bool Equals(object? obj) => obj is SessionInfo si && Equals(si);
///<inheritdoc/>
public override int GetHashCode() => SessionID.GetHashCode(StringComparison.Ordinal);
///<inheritdoc/>